diff options
Diffstat (limited to 'spec')
519 files changed, 11214 insertions, 3262 deletions
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 8166657f674..4caf8b46519 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -43,6 +43,16 @@ describe Admin::ProjectsController do end end + describe 'GET /projects.json' do + render_views + + before do + get :index, format: :json + end + + it { is_expected.to respond_with(:success) } + end + describe 'GET /projects/:id' do render_views diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index 2975205e09c..649441f4917 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -2,4 +2,30 @@ require 'spec_helper' describe Dashboard::ProjectsController do it_behaves_like 'authenticates sessionless user', :index, :atom + + context 'json requests' do + render_views + + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET /projects.json' do + before do + get :index, format: :json + end + + it { is_expected.to respond_with(:success) } + end + + describe 'GET /starred.json' do + before do + get :starred, format: :json + end + + it { is_expected.to respond_with(:success) } + end + end end diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb index d57367e931e..7e20ddca249 100644 --- a/spec/controllers/explore/projects_controller_spec.rb +++ b/spec/controllers/explore/projects_controller_spec.rb @@ -1,6 +1,36 @@ require 'spec_helper' describe Explore::ProjectsController do + describe 'GET #index.json' do + render_views + + before do + get :index, format: :json + end + + it { is_expected.to respond_with(:success) } + end + + describe 'GET #trending.json' do + render_views + + before do + get :trending, format: :json + end + + it { is_expected.to respond_with(:success) } + end + + describe 'GET #starred.json' do + render_views + + before do + get :starred, format: :json + end + + it { is_expected.to respond_with(:success) } + end + describe 'GET #trending' do context 'sorting by update date' do let(:project1) { create(:project, :public, updated_at: 3.days.ago) } diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb new file mode 100644 index 00000000000..c19a752b07b --- /dev/null +++ b/spec/controllers/graphql_controller_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GraphqlController do + before do + stub_feature_flags(graphql: true) + end + + describe 'POST #execute' do + context 'when user is logged in' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'returns 200 when user can access API' do + post :execute + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns access denied template when user cannot access API' do + # User cannot access API in a couple of cases + # * When user is internal(like ghost users) + # * When user is blocked + expect(Ability).to receive(:allowed?).with(user, :access_api, :global).and_return(false) + + post :execute + + expect(response.status).to eq(403) + expect(response).to render_template('errors/access_denied') + end + end + + context 'when user is not logged in' do + it 'returns 200' do + post :execute + + expect(response).to have_gitlab_http_status(200) + end + end + end +end diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index e0da23ca0b8..06c6f49f7cc 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -113,6 +113,33 @@ describe OmniauthCallbacksController, type: :controller do expect(request.env['warden']).to be_authenticated end + context 'when user has no linked provider' do + let(:user) { create(:user) } + + before do + sign_in user + end + + it 'links identity' do + expect do + post provider + user.reload + end.to change { user.identities.count }.by(1) + end + + context 'and is not allowed to link the provider' do + before do + allow_any_instance_of(IdentityProviderPolicy).to receive(:can?).with(:link).and_return(false) + end + + it 'returns 403' do + post provider + + expect(response).to have_gitlab_http_status(403) + end + end + end + shared_context 'sign_up' do let(:user) { double(email: 'new@example.com') } diff --git a/spec/controllers/projects/git_http_controller_spec.rb b/spec/controllers/projects/git_http_controller_spec.rb new file mode 100644 index 00000000000..bf099e8deeb --- /dev/null +++ b/spec/controllers/projects/git_http_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::GitHttpController do + describe 'HEAD #info_refs' do + it 'returns 403' do + project = create(:project, :public, :repository) + + head :info_refs, params: { namespace_id: project.namespace.to_param, project_id: project.path + '.git' } + + expect(response.status).to eq(403) + end + end +end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 79f97aa4170..c8fa93a74ee 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -86,6 +86,10 @@ describe Projects::MergeRequestsController do end describe 'as json' do + before do + expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original + end + context 'with basic serializer param' do it 'renders basic MR entity as json' do go(serializer: 'basic', format: :json) diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 0b0f5117784..deecb7fefe9 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -413,6 +413,37 @@ describe Projects::NotesController do end end end + + context 'when creating a note with quick actions' do + context 'with commands that return changes' do + let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" } + + it 'includes changes in commands_changes ' do + post :create, params: request_params.merge(note: { note: note_text }, format: :json) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['commands_changes']).to include('emoji_award', 'time_estimate', 'spend_time') + expect(json_response['commands_changes']).not_to include('target_project', 'title') + end + end + + context 'with commands that do not return changes' do + let(:issue) { create(:issue, project: project) } + let(:other_project) { create(:project) } + let(:note_text) { "/move #{other_project.full_path}\n/title AAA" } + + before do + other_project.add_developer(user) + end + + it 'does not include changes in commands_changes' do + post :create, params: request_params.merge(note: { note: note_text }, target_type: 'issue', target_id: issue.id, format: :json) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['commands_changes']).not_to include('target_project', 'title') + end + end + end end describe 'PUT update' do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index fd151e8a298..c1baf88778d 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -15,7 +15,7 @@ describe RegistrationsController do context 'when send_user_confirmation_email is false' do it 'signs the user in' do - allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) + stub_application_setting(send_user_confirmation_email: false) expect { post(:create, params: user_params) }.not_to change { ActionMailer::Base.deliveries.size } expect(subject.current_user).not_to be_nil @@ -24,7 +24,7 @@ describe RegistrationsController do context 'when send_user_confirmation_email is true' do it 'does not authenticate user and sends confirmation email' do - allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) + stub_application_setting(send_user_confirmation_email: true) post(:create, params: user_params) @@ -35,7 +35,7 @@ describe RegistrationsController do context 'when signup_enabled? is false' do it 'redirects to sign_in' do - allow_any_instance_of(ApplicationSetting).to receive(:signup_enabled?).and_return(false) + stub_application_setting(signup_enabled: false) expect { post(:create, params: user_params) }.not_to change(User, :count) expect(response).to redirect_to(new_user_session_path) diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 0b3e67b4987..067391c1179 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -75,6 +75,10 @@ FactoryBot.define do status 'created' end + trait :preparing do + status 'preparing' + end + trait :scheduled do schedulable status 'scheduled' diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index ee5d27355f1..aa5ccbda6cd 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -50,6 +50,14 @@ FactoryBot.define do failure_reason :config_error end + trait :created do + status :created + end + + trait :preparing do + status :preparing + end + trait :blocked do status :manual end diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb index a2e5f4862db..1cc3c0e03d8 100644 --- a/spec/factories/clusters/clusters.rb +++ b/spec/factories/clusters/clusters.rb @@ -12,7 +12,7 @@ FactoryBot.define do cluster_type { Clusters::Cluster.cluster_types[:project_type] } before(:create) do |cluster, evaluator| - cluster.projects << create(:project, :repository) + cluster.projects << create(:project, :repository) unless cluster.projects.present? end end @@ -20,7 +20,7 @@ FactoryBot.define do cluster_type { Clusters::Cluster.cluster_types[:group_type] } before(:create) do |cluster, evalutor| - cluster.groups << create(:group) + cluster.groups << create(:group) unless cluster.groups.present? end end diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb index a002ab28519..186c7c8027c 100644 --- a/spec/factories/clusters/providers/gcp.rb +++ b/spec/factories/clusters/providers/gcp.rb @@ -28,5 +28,9 @@ FactoryBot.define do gcp.make_errored('Something wrong') end end + + trait :abac_enabled do + legacy_abac true + end end end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 381bf07f6a0..848a31e96c1 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -33,6 +33,10 @@ FactoryBot.define do status 'pending' end + trait :preparing do + status 'preparing' + end + trait :created do status 'created' end diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index a73f330a7a9..abf0e6bccb7 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -46,10 +46,26 @@ FactoryBot.define do target_branch "improve/awesome" end + trait :merged_last_month do + merged + + after(:build) do |merge_request| + merge_request.build_metrics.merged_at = 1.month.ago + end + end + trait :closed do state :closed end + trait :closed_last_month do + closed + + after(:build) do |merge_request| + merge_request.build_metrics.latest_closed_at = 1.month.ago + end + end + trait :opened do state :opened end @@ -101,9 +117,20 @@ FactoryBot.define do end end + trait :with_legacy_detached_merge_request_pipeline do + after(:create) do |merge_request| + merge_request.merge_request_pipelines << create(:ci_pipeline, + source: :merge_request_event, + merge_request: merge_request, + project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.source_branch_sha) + end + end + trait :with_detached_merge_request_pipeline do - after(:build) do |merge_request| - merge_request.merge_request_pipelines << build(:ci_pipeline, + after(:create) do |merge_request| + merge_request.merge_request_pipelines << create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, project: merge_request.source_project, @@ -119,7 +146,7 @@ FactoryBot.define do target_sha { target_branch_sha } end - after(:build) do |merge_request, evaluator| + after(:create) do |merge_request, evaluator| merge_request.merge_request_pipelines << create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, diff --git a/spec/factories/suggestions.rb b/spec/factories/suggestions.rb index 307523cc061..b1427e0211f 100644 --- a/spec/factories/suggestions.rb +++ b/spec/factories/suggestions.rb @@ -16,5 +16,11 @@ FactoryBot.define do applied true commit_id { RepoHelpers.sample_commit.id } end + + trait :content_from_repo do + after(:build) do |suggestion, evaluator| + suggestion.from_content = suggestion.fetch_from_content + end + end end end diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb index 0a9c4bcaf12..d2e46d15730 100644 --- a/spec/features/clusters/cluster_detail_page_spec.rb +++ b/spec/features/clusters/cluster_detail_page_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' describe 'Clusterable > Show page' do let(:current_user) { create(:user) } + let(:cluster_ingress_help_text_selector) { '.js-ingress-domain-help-text' } + let(:hide_modifier_selector) { '.hide' } before do sign_in(current_user) @@ -35,7 +37,7 @@ describe 'Clusterable > Show page' do it 'shows help text with the domain as an alternative to custom domain' do within '#cluster-integration' do - expect(page).to have_content('Alternatively 192.168.1.100.nip.io can be used instead of a custom domain') + expect(find(cluster_ingress_help_text_selector)).not_to match_css(hide_modifier_selector) end end end @@ -45,18 +47,86 @@ describe 'Clusterable > Show page' do visit cluster_path within '#cluster-integration' do - expect(page).not_to have_content('can be used instead of a custom domain.') + expect(find(cluster_ingress_help_text_selector)).to match_css(hide_modifier_selector) end end end end + shared_examples 'editing a GCP cluster' do + before do + clusterable.add_maintainer(current_user) + visit cluster_path + end + + it 'is not able to edit the name, API url, CA certificate nor token' do + within('#js-cluster-details') do + cluster_name_field = find('.cluster-name') + api_url_field = find('#cluster_platform_kubernetes_attributes_api_url') + ca_certificate_field = find('#cluster_platform_kubernetes_attributes_ca_cert') + token_field = find('#cluster_platform_kubernetes_attributes_token') + + expect(cluster_name_field).to be_readonly + expect(api_url_field).to be_readonly + expect(ca_certificate_field).to be_readonly + expect(token_field).to be_readonly + end + end + + it 'displays GKE information' do + within('#advanced-settings-section') do + expect(page).to have_content('Google Kubernetes Engine') + expect(page).to have_content('Manage your Kubernetes cluster by visiting') + end + end + end + + shared_examples 'editing a user-provided cluster' do + before do + clusterable.add_maintainer(current_user) + visit cluster_path + end + + it 'is able to edit the name, API url, CA certificate and token' do + within('#js-cluster-details') do + cluster_name_field = find('#cluster_name') + api_url_field = find('#cluster_platform_kubernetes_attributes_api_url') + ca_certificate_field = find('#cluster_platform_kubernetes_attributes_ca_cert') + token_field = find('#cluster_platform_kubernetes_attributes_token') + + expect(cluster_name_field).not_to be_readonly + expect(api_url_field).not_to be_readonly + expect(ca_certificate_field).not_to be_readonly + expect(token_field).not_to be_readonly + end + end + + it 'does not display GKE information' do + within('#advanced-settings-section') do + expect(page).not_to have_content('Google Kubernetes Engine') + expect(page).not_to have_content('Manage your Kubernetes cluster by visiting') + end + end + end + context 'when clusterable is a project' do it_behaves_like 'editing domain' do let(:clusterable) { create(:project) } let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) } let(:cluster_path) { project_cluster_path(clusterable, cluster) } end + + it_behaves_like 'editing a GCP cluster' do + let(:clusterable) { create(:project) } + let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) } + let(:cluster_path) { project_cluster_path(clusterable, cluster) } + end + + it_behaves_like 'editing a user-provided cluster' do + let(:clusterable) { create(:project) } + let(:cluster) { create(:cluster, :provided_by_user, :project, projects: [clusterable]) } + let(:cluster_path) { project_cluster_path(clusterable, cluster) } + end end context 'when clusterable is a group' do @@ -65,5 +135,17 @@ describe 'Clusterable > Show page' do let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) } let(:cluster_path) { group_cluster_path(clusterable, cluster) } end + + it_behaves_like 'editing a GCP cluster' do + let(:clusterable) { create(:group) } + let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) } + let(:cluster_path) { group_cluster_path(clusterable, cluster) } + end + + it_behaves_like 'editing a user-provided cluster' do + let(:clusterable) { create(:group) } + let(:cluster) { create(:cluster, :provided_by_user, :group, groups: [clusterable]) } + let(:cluster_path) { group_cluster_path(clusterable, cluster) } + end end end diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 986f3823275..8eb413bdd8d 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -278,12 +278,7 @@ describe 'GFM autocomplete', :js do end end - # This context has just one example in each contexts in order to improve spec performance. - context 'labels', :quarantine do - let!(:backend) { create(:label, project: project, title: 'backend') } - let!(:bug) { create(:label, project: project, title: 'bug') } - let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') } - + context 'labels' do it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do create(:label, project: project, title: label_xss_title) @@ -298,83 +293,6 @@ describe 'GFM autocomplete', :js do expect(find('.atwho-view-ul').text).to have_content('alert label') end end - - context 'when no labels are assigned' do - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/label ~". - type(note, '/label ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show no labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(not_shown: [backend, bug, feature_proposal]) - end - end - - context 'when some labels are assigned' do - before do - issue.labels << [backend] - end - - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show only unset labels on "/label ~". - type(note, '/label ~') - expect_labels(shown: [bug, feature_proposal], not_shown: [backend]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show only set labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(shown: [backend], not_shown: [bug, feature_proposal]) - end - end - - context 'when all labels are assigned' do - before do - issue.labels << [backend, bug, feature_proposal] - end - - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show no labels on "/label ~". - type(note, '/label ~') - expect_labels(not_shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - end - end end shared_examples 'autocomplete suggestions' do diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb index b5e7c3954e2..362f8a468ec 100644 --- a/spec/features/issues/user_uses_quick_actions_spec.rb +++ b/spec/features/issues/user_uses_quick_actions_spec.rb @@ -3,8 +3,41 @@ require 'rails_helper' describe 'Issues > User uses quick actions', :js do include Spec::Support::Helpers::Features::NotesHelpers - it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do + context "issuable common quick actions" do + let(:new_url_opts) { {} } + let(:maintainer) { create(:user) } + let(:project) { create(:project, :public) } + let!(:label_bug) { create(:label, project: project, title: 'bug') } + let!(:label_feature) { create(:label, project: project, title: 'feature') } + let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } let(:issuable) { create(:issue, project: project) } + let(:source_issuable) { create(:issue, project: project, milestone: milestone, labels: [label_bug, label_feature])} + + it_behaves_like 'assign quick action', :issue + it_behaves_like 'unassign quick action', :issue + it_behaves_like 'close quick action', :issue + it_behaves_like 'reopen quick action', :issue + it_behaves_like 'title quick action', :issue + it_behaves_like 'todo quick action', :issue + it_behaves_like 'done quick action', :issue + it_behaves_like 'subscribe quick action', :issue + it_behaves_like 'unsubscribe quick action', :issue + it_behaves_like 'lock quick action', :issue + it_behaves_like 'unlock quick action', :issue + it_behaves_like 'milestone quick action', :issue + it_behaves_like 'remove_milestone quick action', :issue + it_behaves_like 'label quick action', :issue + it_behaves_like 'unlabel quick action', :issue + it_behaves_like 'relabel quick action', :issue + it_behaves_like 'award quick action', :issue + it_behaves_like 'estimate quick action', :issue + it_behaves_like 'remove_estimate quick action', :issue + it_behaves_like 'spend quick action', :issue + it_behaves_like 'remove_time_spent quick action', :issue + it_behaves_like 'shrug quick action', :issue + it_behaves_like 'tableflip quick action', :issue + it_behaves_like 'copy_metadata quick action', :issue + it_behaves_like 'issuable time tracker', :issue end describe 'issue-only commands' do @@ -15,37 +48,17 @@ describe 'Issues > User uses quick actions', :js do project.add_maintainer(user) sign_in(user) visit project_issue_path(project, issue) + wait_for_all_requests end after do wait_for_requests end - describe 'time tracking' do - let(:issue) { create(:issue, project: project) } - - before do - visit project_issue_path(project, issue) - end - - it_behaves_like 'issuable time tracker' - end - describe 'adding a due date from note' do let(:issue) { create(:issue, project: project) } - context 'when the current user can update the due date' do - it 'does not create a note, and sets the due date accordingly' do - add_note("/due 2016-08-28") - - expect(page).not_to have_content '/due 2016-08-28' - expect(page).to have_content 'Commands applied' - - issue.reload - - expect(issue.due_date).to eq Date.new(2016, 8, 28) - end - end + it_behaves_like 'due quick action available and date can be added' context 'when the current user cannot update the due date' do let(:guest) { create(:user) } @@ -56,35 +69,14 @@ describe 'Issues > User uses quick actions', :js do visit project_issue_path(project, issue) end - it 'does not create a note, and sets the due date accordingly' do - add_note("/due 2016-08-28") - - expect(page).not_to have_content 'Commands applied' - - issue.reload - - expect(issue.due_date).to be_nil - end + it_behaves_like 'due quick action not available' end end describe 'removing a due date from note' do let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) } - context 'when the current user can update the due date' do - it 'does not create a note, and removes the due date accordingly' do - expect(issue.due_date).to eq Date.new(2016, 8, 28) - - add_note("/remove_due_date") - - expect(page).not_to have_content '/remove_due_date' - expect(page).to have_content 'Commands applied' - - issue.reload - - expect(issue.due_date).to be_nil - end - end + it_behaves_like 'remove_due_date action available and due date can be removed' context 'when the current user cannot update the due date' do let(:guest) { create(:user) } @@ -95,15 +87,7 @@ describe 'Issues > User uses quick actions', :js do visit project_issue_path(project, issue) end - it 'does not create a note, and sets the due date accordingly' do - add_note("/remove_due_date") - - expect(page).not_to have_content 'Commands applied' - - issue.reload - - expect(issue.due_date).to eq Date.new(2016, 8, 28) - end + it_behaves_like 'remove_due_date action not available' end end @@ -200,6 +184,7 @@ describe 'Issues > User uses quick actions', :js do gitlab_sign_out sign_in(user) visit project_issue_path(project, issue) + wait_for_requests end it 'moves the issue' do @@ -221,6 +206,7 @@ describe 'Issues > User uses quick actions', :js do gitlab_sign_out sign_in(user) visit project_issue_path(project, issue) + wait_for_requests end it 'does not move the issue' do @@ -238,6 +224,7 @@ describe 'Issues > User uses quick actions', :js do gitlab_sign_out sign_in(user) visit project_issue_path(project, issue) + wait_for_requests end it 'does not move the issue' do diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 9bc340ed4bb..51508b78649 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -497,12 +497,21 @@ describe 'Issues' do it 'allows user to unselect themselves', :js do issue2 = create(:issue, project: project, author: user) + visit project_issue_path(project, issue2) + def close_dropdown_menu_if_visible + find('.dropdown-menu-toggle', visible: :all).tap do |toggle| + toggle.click if toggle.visible? + end + end + page.within '.assignee' do click_link 'Edit' click_link user.name + close_dropdown_menu_if_visible + page.within '.value .author' do expect(page).to have_content user.name end @@ -510,6 +519,8 @@ describe 'Issues' do click_link 'Edit' click_link user.name + close_dropdown_menu_if_visible + page.within '.value .assign-yourself' do expect(page).to have_content "No assignee" end diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb index 8c2599615cb..2f7d359575e 100644 --- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb +++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb @@ -5,9 +5,7 @@ describe 'Merge request > User scrolls to note on load', :js do let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project, author: user) } let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } - let(:resolved_note) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project) } let(:fragment_id) { "#note_#{note.id}" } - let(:collapsed_fragment_id) { "#note_#{resolved_note.id}" } before do sign_in(user) @@ -45,13 +43,35 @@ describe 'Merge request > User scrolls to note on load', :js do end end - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - xit 'expands collapsed notes' do - visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}" - note_element = find(collapsed_fragment_id) - note_container = note_element.ancestor('.timeline-content') + context 'resolved notes' do + let(:collapsed_fragment_id) { "#note_#{resolved_note.id}" } - expect(note_element.visible?).to eq true - expect(note_container.find('.line_content.noteable_line.old', match: :first).visible?).to eq true + context 'when diff note' do + let(:resolved_note) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project) } + + it 'expands collapsed notes' do + visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}" + + note_element = find(collapsed_fragment_id) + diff_container = note_element.ancestor('.diff-content') + + expect(note_element.visible?).to eq(true) + expect(diff_container.visible?).to eq(true) + end + end + + context 'when non-diff note' do + let(:non_diff_discussion) { create(:discussion_note_on_merge_request, :resolved, noteable: merge_request, project: project) } + let(:resolved_note) { create(:discussion_note_on_merge_request, :resolved, noteable: merge_request, project: project, in_reply_to: non_diff_discussion) } + + it 'expands collapsed replies' do + visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}" + + note_element = find(collapsed_fragment_id) + + expect(note_element.visible?).to eq(true) + expect(note_element.sibling('.replies-toggle')[:class]).to include('expanded') + end + end end end diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb index 97b2aa82fce..28f88718ec1 100644 --- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb +++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' -describe 'Merge request > User sees merge request pipelines', :js do +describe 'Merge request > User sees pipelines triggered by merge request', :js do include ProjectForksHelper include TestReportsHelper @@ -47,7 +47,7 @@ describe 'Merge request > User sees merge request pipelines', :js do .execute(:push) end - let!(:merge_request_pipeline) do + let!(:detached_merge_request_pipeline) do Ci::CreatePipelineService.new(project, user, ref: 'feature') .execute(:merge_request_event, merge_request: merge_request) end @@ -60,16 +60,16 @@ describe 'Merge request > User sees merge request pipelines', :js do end end - it 'sees branch pipelines and merge request pipelines in correct order' do + it 'sees branch pipelines and detached merge request pipelines in correct order' do page.within('.ci-table') do expect(page).to have_selector('.ci-pending', count: 2) - expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}") + expect(first('.js-pipeline-url-link')).to have_content("##{detached_merge_request_pipeline.id}") end end - it 'sees the latest merge request pipeline as the head pipeline' do + it 'sees the latest detached merge request pipeline as the head pipeline' do page.within('.ci-widget-content') do - expect(page).to have_content("##{merge_request_pipeline.id}") + expect(page).to have_content("##{detached_merge_request_pipeline.id}") end end @@ -79,7 +79,7 @@ describe 'Merge request > User sees merge request pipelines', :js do .execute(:push) end - let!(:merge_request_pipeline_2) do + let!(:detached_merge_request_pipeline_2) do Ci::CreatePipelineService.new(project, user, ref: 'feature') .execute(:merge_request_event, merge_request: merge_request) end @@ -92,15 +92,15 @@ describe 'Merge request > User sees merge request pipelines', :js do end end - it 'sees branch pipelines and merge request pipelines in correct order' do + it 'sees branch pipelines and detached merge request pipelines in correct order' do page.within('.ci-table') do expect(page).to have_selector('.ci-pending', count: 4) expect(all('.js-pipeline-url-link')[0]) - .to have_content("##{merge_request_pipeline_2.id}") + .to have_content("##{detached_merge_request_pipeline_2.id}") expect(all('.js-pipeline-url-link')[1]) - .to have_content("##{merge_request_pipeline.id}") + .to have_content("##{detached_merge_request_pipeline.id}") expect(all('.js-pipeline-url-link')[2]) .to have_content("##{push_pipeline_2.id}") @@ -110,25 +110,25 @@ describe 'Merge request > User sees merge request pipelines', :js do end end - it 'sees merge request tag for merge request pipelines' do + it 'sees detached tag for detached merge request pipelines' do page.within('.ci-table') do expect(all('.pipeline-tags')[0]) - .to have_content("merge request") + .to have_content("detached") expect(all('.pipeline-tags')[1]) - .to have_content("merge request") + .to have_content("detached") expect(all('.pipeline-tags')[2]) - .not_to have_content("merge request") + .not_to have_content("detached") expect(all('.pipeline-tags')[3]) - .not_to have_content("merge request") + .not_to have_content("detached") end end - it 'sees the latest merge request pipeline as the head pipeline' do + it 'sees the latest detached merge request pipeline as the head pipeline' do page.within('.ci-widget-content') do - expect(page).to have_content("##{merge_request_pipeline_2.id}") + expect(page).to have_content("##{detached_merge_request_pipeline_2.id}") end end end @@ -140,16 +140,16 @@ describe 'Merge request > User sees merge request pipelines', :js do wait_for_requests end - context 'when merge request pipeline is pending' do + context 'when detached merge request pipeline is pending' do it 'waits the head pipeline' do expect(page).to have_content('to be merged automatically when the pipeline succeeds') expect(page).to have_link('Cancel automatic merge') end end - context 'when merge request pipeline succeeds' do + context 'when detached merge request pipeline succeeds' do before do - merge_request_pipeline.succeed! + detached_merge_request_pipeline.succeed! wait_for_requests end @@ -218,7 +218,7 @@ describe 'Merge request > User sees merge request pipelines', :js do .execute(:push) end - let!(:merge_request_pipeline) do + let!(:detached_merge_request_pipeline) do Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature') .execute(:merge_request_event, merge_request: merge_request) end @@ -236,16 +236,16 @@ describe 'Merge request > User sees merge request pipelines', :js do end end - it 'sees branch pipelines and merge request pipelines in correct order' do + it 'sees branch pipelines and detached merge request pipelines in correct order' do page.within('.ci-table') do expect(page).to have_selector('.ci-pending', count: 2) - expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}") + expect(first('.js-pipeline-url-link')).to have_content("##{detached_merge_request_pipeline.id}") end end - it 'sees the latest merge request pipeline as the head pipeline' do + it 'sees the latest detached merge request pipeline as the head pipeline' do page.within('.ci-widget-content') do - expect(page).to have_content("##{merge_request_pipeline.id}") + expect(page).to have_content("##{detached_merge_request_pipeline.id}") end end @@ -261,7 +261,7 @@ describe 'Merge request > User sees merge request pipelines', :js do .execute(:push) end - let!(:merge_request_pipeline_2) do + let!(:detached_merge_request_pipeline_2) do Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature') .execute(:merge_request_event, merge_request: merge_request) end @@ -274,15 +274,15 @@ describe 'Merge request > User sees merge request pipelines', :js do end end - it 'sees branch pipelines and merge request pipelines in correct order' do + it 'sees branch pipelines and detached merge request pipelines in correct order' do page.within('.ci-table') do expect(page).to have_selector('.ci-pending', count: 4) expect(all('.js-pipeline-url-link')[0]) - .to have_content("##{merge_request_pipeline_2.id}") + .to have_content("##{detached_merge_request_pipeline_2.id}") expect(all('.js-pipeline-url-link')[1]) - .to have_content("##{merge_request_pipeline.id}") + .to have_content("##{detached_merge_request_pipeline.id}") expect(all('.js-pipeline-url-link')[2]) .to have_content("##{push_pipeline_2.id}") @@ -292,25 +292,25 @@ describe 'Merge request > User sees merge request pipelines', :js do end end - it 'sees merge request tag for merge request pipelines' do + it 'sees detached tag for detached merge request pipelines' do page.within('.ci-table') do expect(all('.pipeline-tags')[0]) - .to have_content("merge request") + .to have_content("detached") expect(all('.pipeline-tags')[1]) - .to have_content("merge request") + .to have_content("detached") expect(all('.pipeline-tags')[2]) - .not_to have_content("merge request") + .not_to have_content("detached") expect(all('.pipeline-tags')[3]) - .not_to have_content("merge request") + .not_to have_content("detached") end end - it 'sees the latest merge request pipeline as the head pipeline' do + it 'sees the latest detached merge request pipeline as the head pipeline' do page.within('.ci-widget-content') do - expect(page).to have_content("##{merge_request_pipeline_2.id}") + expect(page).to have_content("##{detached_merge_request_pipeline_2.id}") end end @@ -328,16 +328,16 @@ describe 'Merge request > User sees merge request pipelines', :js do wait_for_requests end - context 'when merge request pipeline is pending' do + context 'when detached merge request pipeline is pending' do it 'waits the head pipeline' do expect(page).to have_content('to be merged automatically when the pipeline succeeds') expect(page).to have_link('Cancel automatic merge') end end - context 'when merge request pipeline succeeds' do + context 'when detached merge request pipeline succeeds' do before do - merge_request_pipeline.succeed! + detached_merge_request_pipeline.succeed! wait_for_requests end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index afb978d7c45..2609546990d 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -145,6 +145,119 @@ describe 'Merge request > User sees merge widget', :js do end end + context 'when merge request has a branch pipeline as the head pipeline' do + let!(:pipeline) do + create(:ci_pipeline, + ref: merge_request.source_branch, + sha: merge_request.source_branch_sha, + project: merge_request.source_project) + end + + before do + merge_request.update_head_pipeline + visit project_merge_request_path(project, merge_request) + end + + it 'shows head pipeline information' do + within '.ci-widget-content' do + expect(page).to have_content("Pipeline ##{pipeline.id} pending " \ + "for #{pipeline.short_sha} " \ + "on #{pipeline.ref}") + end + end + end + + context 'when merge request has a detached merge request pipeline as the head pipeline' do + let(:merge_request) do + create(:merge_request, + :with_detached_merge_request_pipeline, + source_project: source_project, + target_project: target_project) + end + + let!(:pipeline) do + merge_request.all_pipelines.last + end + + let(:source_project) { project } + let(:target_project) { project } + + before do + merge_request.update_head_pipeline + visit project_merge_request_path(project, merge_request) + end + + it 'shows head pipeline information' do + within '.ci-widget-content' do + expect(page).to have_content("Pipeline ##{pipeline.id} pending " \ + "for #{pipeline.short_sha} " \ + "on #{merge_request.to_reference} " \ + "with #{merge_request.source_branch}") + end + end + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + + it 'shows head pipeline information' do + within '.ci-widget-content' do + expect(page).to have_content("Pipeline ##{pipeline.id} pending " \ + "for #{pipeline.short_sha} " \ + "on #{merge_request.to_reference} " \ + "with #{merge_request.source_branch}") + end + end + end + end + + context 'when merge request has a merge request pipeline as the head pipeline' do + let(:merge_request) do + create(:merge_request, + :with_merge_request_pipeline, + source_project: source_project, + target_project: target_project, + merge_sha: merge_sha) + end + + let!(:pipeline) do + merge_request.all_pipelines.last + end + + let(:source_project) { project } + let(:target_project) { project } + let(:merge_sha) { project.commit.sha } + + before do + merge_request.update_head_pipeline + visit project_merge_request_path(project, merge_request) + end + + it 'shows head pipeline information' do + within '.ci-widget-content' do + expect(page).to have_content("Pipeline ##{pipeline.id} pending " \ + "for #{pipeline.short_sha} " \ + "on #{merge_request.to_reference} " \ + "with #{merge_request.source_branch} " \ + "into #{merge_request.target_branch}") + end + end + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + let(:merge_sha) { source_project.commit.sha } + + it 'shows head pipeline information' do + within '.ci-widget-content' do + expect(page).to have_content("Pipeline ##{pipeline.id} pending " \ + "for #{pipeline.short_sha} " \ + "on #{merge_request.to_reference} " \ + "with #{merge_request.source_branch} " \ + "into #{merge_request.target_branch}") + end + end + end + end + context 'view merge request with MWBS button' do before do commit_status = create(:commit_status, project: project, status: 'pending') diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb index b81478a481f..a2b5859bd1e 100644 --- a/spec/features/merge_request/user_uses_quick_actions_spec.rb +++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb @@ -9,9 +9,41 @@ describe 'Merge request > User uses quick actions', :js do let(:merge_request) { create(:merge_request, source_project: project) } let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } - it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do + context "issuable common quick actions" do + let!(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } } + let(:maintainer) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let!(:label_bug) { create(:label, project: project, title: 'bug') } + let!(:label_feature) { create(:label, project: project, title: 'feature') } + let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } let(:issuable) { create(:merge_request, source_project: project) } - let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } } + let(:source_issuable) { create(:issue, project: project, milestone: milestone, labels: [label_bug, label_feature])} + + it_behaves_like 'assign quick action', :merge_request + it_behaves_like 'unassign quick action', :merge_request + it_behaves_like 'close quick action', :merge_request + it_behaves_like 'reopen quick action', :merge_request + it_behaves_like 'title quick action', :merge_request + it_behaves_like 'todo quick action', :merge_request + it_behaves_like 'done quick action', :merge_request + it_behaves_like 'subscribe quick action', :merge_request + it_behaves_like 'unsubscribe quick action', :merge_request + it_behaves_like 'lock quick action', :merge_request + it_behaves_like 'unlock quick action', :merge_request + it_behaves_like 'milestone quick action', :merge_request + it_behaves_like 'remove_milestone quick action', :merge_request + it_behaves_like 'label quick action', :merge_request + it_behaves_like 'unlabel quick action', :merge_request + it_behaves_like 'relabel quick action', :merge_request + it_behaves_like 'award quick action', :merge_request + it_behaves_like 'estimate quick action', :merge_request + it_behaves_like 'remove_estimate quick action', :merge_request + it_behaves_like 'spend quick action', :merge_request + it_behaves_like 'remove_time_spent quick action', :merge_request + it_behaves_like 'shrug quick action', :merge_request + it_behaves_like 'tableflip quick action', :merge_request + it_behaves_like 'copy_metadata quick action', :merge_request + it_behaves_like 'issuable time tracker', :merge_request end describe 'merge-request-only commands' do @@ -24,20 +56,12 @@ describe 'Merge request > User uses quick actions', :js do project.add_maintainer(user) end - describe 'time tracking' do - before do - sign_in(user) - visit project_merge_request_path(project, merge_request) - end - - it_behaves_like 'issuable time tracker' - end - describe 'toggling the WIP prefix in the title from note' do context 'when the current user can toggle the WIP prefix' do before do sign_in(user) visit project_merge_request_path(project, merge_request) + wait_for_requests end it 'adds the WIP: prefix to the title' do @@ -135,11 +159,16 @@ describe 'Merge request > User uses quick actions', :js do visit project_merge_request_path(project, merge_request) end - it 'does not recognize the command nor create a note' do - add_note('/due 2016-08-28') + it_behaves_like 'due quick action not available' + end - expect(page).not_to have_content '/due 2016-08-28' + describe 'removing a due date from note' do + before do + sign_in(user) + visit project_merge_request_path(project, merge_request) end + + it_behaves_like 'remove_due_date action not available' end describe '/target_branch command in merge request' do diff --git a/spec/features/merge_request/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb index 71022c6bb08..849fab62fc6 100644 --- a/spec/features/merge_request/user_views_open_merge_request_spec.rb +++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb @@ -13,7 +13,7 @@ describe 'User views an open merge request' do end it 'renders both the title and the description' do - node = find('.wiki h1 a#user-content-description-header') + node = find('.md h1 a#user-content-description-header') expect(node[:href]).to end_with('#description-header') # Work around a weird Capybara behavior where calling `parent` on a node diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index 6e349395017..adac59b89ef 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -122,4 +122,32 @@ describe 'Milestone' do expect(page).to have_selector('.popover') end end + + describe 'reopen closed milestones' do + before do + create(:milestone, :closed, project: project) + end + + describe 'group milestones page' do + it 'reopens the milestone' do + visit group_milestones_path(group, { state: 'closed' }) + + click_link 'Reopen Milestone' + + expect(page).not_to have_selector('.status-box-closed') + expect(page).to have_selector('.status-box-open') + end + end + + describe 'project milestones page' do + it 'reopens the milestone' do + visit project_milestones_path(project, { state: 'closed' }) + + click_link 'Reopen Milestone' + + expect(page).not_to have_selector('.status-box-closed') + expect(page).to have_selector('.status-box-open') + end + end + end end diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb index 5f630c9ffa4..a1fcd4024c0 100644 --- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb +++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb @@ -19,6 +19,12 @@ describe "User browses artifacts" do visit(browse_project_job_artifacts_path(project, job)) end + it "renders a link to the job in the breadcrumbs" do + page.within('.js-breadcrumbs-list') do + expect(page).to have_link("##{job.id}", href: project_job_path(project, job)) + end + end + it "shows artifacts" do expect(page).not_to have_selector(".build-sidebar") diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb index dee81898928..4ac4e8f0fcb 100644 --- a/spec/features/projects/badges/pipeline_badge_spec.rb +++ b/spec/features/projects/badges/pipeline_badge_spec.rb @@ -41,6 +41,25 @@ describe 'Pipeline Badge' do end end + context 'when the pipeline is preparing' do + let!(:job) { create(:ci_build, status: 'created', pipeline: pipeline) } + + before do + # Prevent skipping directly to 'pending' + allow(Ci::BuildPrepareWorker).to receive(:perform_async) + allow(job).to receive(:prerequisites).and_return([double]) + end + + it 'displays the preparing badge' do + job.enqueue + + visit pipeline_project_badges_path(project, ref: ref, format: :svg) + + expect(page.status_code).to eq(200) + expect_badge('preparing') + end + end + context 'when the pipeline is running' do it 'shows displays so on the badge' do create(:ci_build, pipeline: pipeline, name: 'second build', status_event: 'run') diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb index 3090f1a2131..fe71cb7661a 100644 --- a/spec/features/projects/environments/environment_spec.rb +++ b/spec/features/projects/environments/environment_spec.rb @@ -319,7 +319,7 @@ describe 'Environment' do yield - GitPushService.new(project, user, params).execute + Git::BranchPushService.new(project, user, params).execute end end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 65ce872363f..224375daf71 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -2,6 +2,9 @@ require 'spec_helper' require 'tempfile' describe 'Jobs', :clean_gitlab_redis_shared_state do + include Gitlab::Routing + include ProjectForksHelper + let(:user) { create(:user) } let(:user_access_level) { :developer } let(:project) { create(:project, :repository) } @@ -121,6 +124,112 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end + context 'pipeline info block', :js do + it 'shows pipeline id and source branch' do + visit project_job_path(project, job) + + within '.js-pipeline-info' do + expect(page).to have_content("Pipeline ##{pipeline.id} for #{pipeline.ref}") + end + end + + context 'when pipeline is detached merge request pipeline' do + let(:merge_request) do + create(:merge_request, + :with_detached_merge_request_pipeline, + target_project: target_project, + source_project: source_project) + end + + let(:source_project) { project } + let(:target_project) { project } + let(:pipeline) { merge_request.all_pipelines.last } + let(:job) { create(:ci_build, pipeline: pipeline) } + + it 'shows merge request iid and source branch' do + visit project_job_path(project, job) + + within '.js-pipeline-info' do + expect(page).to have_content("for !#{pipeline.merge_request.iid} " \ + "with #{pipeline.merge_request.source_branch}") + expect(page).to have_link("!#{pipeline.merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(pipeline.merge_request.source_branch, + href: project_commits_path(project, merge_request.source_branch)) + end + end + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + let(:target_project) { project } + + it 'shows merge request iid and source branch' do + visit project_job_path(source_project, job) + + within '.js-pipeline-info' do + expect(page).to have_content("for !#{pipeline.merge_request.iid} " \ + "with #{pipeline.merge_request.source_branch}") + expect(page).to have_link("!#{pipeline.merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(pipeline.merge_request.source_branch, + href: project_commits_path(source_project, merge_request.source_branch)) + end + end + end + end + + context 'when pipeline is merge request pipeline' do + let(:merge_request) do + create(:merge_request, + :with_merge_request_pipeline, + target_project: target_project, + source_project: source_project) + end + + let(:source_project) { project } + let(:target_project) { project } + let(:pipeline) { merge_request.all_pipelines.last } + let(:job) { create(:ci_build, pipeline: pipeline) } + + it 'shows merge request iid and source branch' do + visit project_job_path(project, job) + + within '.js-pipeline-info' do + expect(page).to have_content("for !#{pipeline.merge_request.iid} " \ + "with #{pipeline.merge_request.source_branch} " \ + "into #{pipeline.merge_request.target_branch}") + expect(page).to have_link("!#{pipeline.merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(pipeline.merge_request.source_branch, + href: project_commits_path(project, merge_request.source_branch)) + expect(page).to have_link(pipeline.merge_request.target_branch, + href: project_commits_path(project, merge_request.target_branch)) + end + end + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + let(:target_project) { project } + + it 'shows merge request iid and source branch' do + visit project_job_path(source_project, job) + + within '.js-pipeline-info' do + expect(page).to have_content("for !#{pipeline.merge_request.iid} " \ + "with #{pipeline.merge_request.source_branch} " \ + "into #{pipeline.merge_request.target_branch}") + expect(page).to have_link("!#{pipeline.merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(pipeline.merge_request.source_branch, + href: project_commits_path(source_project, merge_request.source_branch)) + expect(page).to have_link(pipeline.merge_request.target_branch, + href: project_commits_path(project, merge_request.target_branch)) + end + end + end + end + end + context 'sidebar', :js do let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline, name: '<img src=x onerror=alert(document.domain)>') } diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 36b8c15b8b6..9fdf78baa1e 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' describe 'Pipeline', :js do + include RoutesHelpers + include ProjectForksHelper + let(:project) { create(:project) } let(:user) { create(:user) } let(:role) { :developer } @@ -21,6 +24,11 @@ describe 'Pipeline', :js do pipeline: pipeline, stage: 'test', name: 'test') end + let!(:build_preparing) do + create(:ci_build, :preparing, + pipeline: pipeline, stage: 'deploy', name: 'prepare') + end + let!(:build_running) do create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') @@ -72,6 +80,15 @@ describe 'Pipeline', :js do expect(page).to have_link(pipeline.ref) end + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for #{pipeline.ref} ") + expect(page).to have_link(pipeline.ref, + href: project_commits_path(pipeline.project, pipeline.ref)) + end + end + it_behaves_like 'showing user status' do let(:user_with_status) { pipeline.user } @@ -97,6 +114,24 @@ describe 'Pipeline', :js do end end + context 'when pipeline has preparing builds' do + it 'shows a preparing icon and a cancel action' do + page.within('#ci-badge-prepare') do + expect(page).to have_selector('.js-ci-status-icon-preparing') + expect(page).to have_selector('.js-icon-cancel') + expect(page).to have_content('prepare') + end + end + + it 'cancels the preparing build and shows retry button' do + find('#ci-badge-deploy .ci-action-icon-container').click + + page.within('#ci-badge-deploy') do + expect(page).to have_css('.js-icon-retry') + end + end + end + context 'when pipeline has successful builds' do it 'shows the success icon and a retry action for the successful build' do page.within('#ci-badge-build') do @@ -254,6 +289,113 @@ describe 'Pipeline', :js do expect(page).to have_content(pipeline.ref) end end + + context 'when pipeline is detached merge request pipeline' do + let(:source_project) { project } + let(:target_project) { project } + + let(:merge_request) do + create(:merge_request, + :with_detached_merge_request_pipeline, + source_project: source_project, + target_project: target_project) + end + + let(:pipeline) do + merge_request.all_pipelines.last + end + + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for !#{merge_request.iid} " \ + "with #{merge_request.source_branch}") + expect(page).to have_link("!#{merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(merge_request.source_branch, + href: project_commits_path(merge_request.source_project, merge_request.source_branch)) + end + end + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + + before do + visit project_pipeline_path(source_project, pipeline) + end + + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for !#{merge_request.iid} " \ + "with #{merge_request.source_branch}") + expect(page).to have_link("!#{merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(merge_request.source_branch, + href: project_commits_path(merge_request.source_project, merge_request.source_branch)) + end + end + end + end + + context 'when pipeline is merge request pipeline' do + let(:source_project) { project } + let(:target_project) { project } + + let(:merge_request) do + create(:merge_request, + :with_merge_request_pipeline, + source_project: source_project, + target_project: target_project, + merge_sha: project.commit.id) + end + + let(:pipeline) do + merge_request.all_pipelines.last + end + + before do + pipeline.update(user: user) + end + + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for !#{merge_request.iid} " \ + "with #{merge_request.source_branch} " \ + "into #{merge_request.target_branch}") + expect(page).to have_link("!#{merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(merge_request.source_branch, + href: project_commits_path(merge_request.source_project, merge_request.source_branch)) + expect(page).to have_link(merge_request.target_branch, + href: project_commits_path(merge_request.target_project, merge_request.target_branch)) + end + end + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + + before do + visit project_pipeline_path(source_project, pipeline) + end + + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for !#{merge_request.iid} " \ + "with #{merge_request.source_branch} " \ + "into #{merge_request.target_branch}") + expect(page).to have_link("!#{merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(merge_request.source_branch, + href: project_commits_path(merge_request.source_project, merge_request.source_branch)) + expect(page).to have_link(merge_request.target_branch, + href: project_commits_path(merge_request.target_project, merge_request.target_branch)) + end + end + end + end end context 'when user does not have access to read jobs' do @@ -686,9 +828,9 @@ describe 'Pipeline', :js do visit project_pipeline_path(project, pipeline) end - it 'contains badge that indicates merge request pipeline' do + it 'contains badge that indicates detached merge request pipeline' do page.within(all('.well-segment')[1]) do - expect(page).to have_content 'merge request' + expect(page).to have_content 'detached' end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 88d7c9ef8bd..7ca3b3d8edd 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Pipelines', :js do + include ProjectForksHelper + let(:project) { create(:project) } context 'when user is logged in' do @@ -165,6 +167,99 @@ describe 'Pipelines', :js do end end + context 'when pipeline is detached merge request pipeline' do + let(:merge_request) do + create(:merge_request, + :with_detached_merge_request_pipeline, + source_project: source_project, + target_project: target_project) + end + + let!(:pipeline) { merge_request.all_pipelines.first } + let(:source_project) { project } + let(:target_project) { project } + + before do + visit project_pipelines_path(source_project) + end + + shared_examples_for 'showing detached merge request pipeline information' do + it 'shows detached tag for the pipeline' do + within '.pipeline-tags' do + expect(page).to have_content('detached') + end + end + + it 'shows the link of the merge request' do + within '.branch-commit' do + expect(page).to have_link(merge_request.iid, + href: project_merge_request_path(project, merge_request)) + end + end + + it 'does not show the ref of the pipeline' do + within '.branch-commit' do + expect(page).not_to have_link(pipeline.ref) + end + end + end + + it_behaves_like 'showing detached merge request pipeline information' + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + + it_behaves_like 'showing detached merge request pipeline information' + end + end + + context 'when pipeline is merge request pipeline' do + let(:merge_request) do + create(:merge_request, + :with_merge_request_pipeline, + source_project: source_project, + target_project: target_project, + merge_sha: target_project.commit.sha) + end + + let!(:pipeline) { merge_request.all_pipelines.first } + let(:source_project) { project } + let(:target_project) { project } + + before do + visit project_pipelines_path(source_project) + end + + shared_examples_for 'Correct merge request pipeline information' do + it 'does not show detached tag for the pipeline' do + within '.pipeline-tags' do + expect(page).not_to have_content('detached') + end + end + + it 'shows the link of the merge request' do + within '.branch-commit' do + expect(page).to have_link(merge_request.iid, + href: project_merge_request_path(project, merge_request)) + end + end + + it 'does not show the ref of the pipeline' do + within '.branch-commit' do + expect(page).not_to have_link(pipeline.ref) + end + end + end + + it_behaves_like 'Correct merge request pipeline information' + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + + it_behaves_like 'Correct merge request pipeline information' + end + end + context 'when pipeline has configuration errors' do let(:pipeline) do create(:ci_pipeline, :invalid, project: project) @@ -282,6 +377,30 @@ describe 'Pipelines', :js do end context 'for generic statuses' do + context 'when preparing' do + let!(:pipeline) do + create(:ci_empty_pipeline, + status: 'preparing', project: project) + end + + let!(:status) do + create(:generic_commit_status, + :preparing, pipeline: pipeline) + end + + before do + visit_project_pipelines + end + + it 'is cancelable' do + expect(page).to have_selector('.js-pipelines-cancel-button') + end + + it 'shows the pipeline as preparing' do + expect(page).to have_selector('.ci-preparing') + end + end + context 'when running' do let!(:running) do create(:generic_commit_status, diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index b1a7f167977..efb7b01f5ad 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -136,7 +136,7 @@ describe "User creates wiki page" do click_button("Create page") end - page.within ".wiki" do + page.within ".md" do expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4") end end diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb new file mode 100644 index 00000000000..3725143291d --- /dev/null +++ b/spec/features/search/user_searches_for_users_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe 'User searches for users' do + context 'when on the dashboard' do + it 'finds the user' do + create(:user, username: 'gob_bluth', name: 'Gob Bluth') + + sign_in(create(:user)) + + visit dashboard_projects_path + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Users 1') + + click_on('Users 1') + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + end + end + + context 'when on the project page' do + it 'finds the user belonging to the project' do + project = create(:project) + + user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth') + create(:project_member, :developer, user: user1, project: project) + + user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth') + create(:project_member, :developer, user: user2, project: project) + + create(:user, username: 'gob_2018', name: 'George Oscar Bluth') + + sign_in(user1) + + visit projects_path(project) + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + + expect(page).not_to have_content('Michael Bluth') + expect(page).not_to have_content('@michael_bluth') + + expect(page).not_to have_content('George Oscar Bluth') + expect(page).not_to have_content('@gob_2018') + end + end + + context 'when on the group page' do + it 'finds the user belonging to the group' do + group = create(:group) + + user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth') + create(:group_member, :developer, user: user1, group: group) + + user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth') + create(:group_member, :developer, user: user2, group: group) + + create(:user, username: 'gob_2018', name: 'George Oscar Bluth') + + sign_in(user1) + + visit group_path(group) + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + + expect(page).not_to have_content('Michael Bluth') + expect(page).not_to have_content('@michael_bluth') + + expect(page).not_to have_content('George Oscar Bluth') + expect(page).not_to have_content('@gob_2018') + end + end +end diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index eeacaf5f72a..fc6726985ae 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -70,7 +70,7 @@ describe 'Comments on personal snippets', :js do fill_in 'note[note]', with: 'This is **awesome**!' find('.js-md-preview-button').click - page.within('.new-note .md-preview') do + page.within('.new-note .md-preview-holder') do expect(page).to have_content('This is awesome!') expect(page).to have_selector('strong') end diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb index 879c46d7c4e..1c97d5ec5b4 100644 --- a/spec/features/snippets/user_creates_snippet_spec.rb +++ b/spec/features/snippets/user_creates_snippet_spec.rb @@ -37,7 +37,7 @@ describe 'User creates snippet', :js do dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') find('.js-md-preview-button').click - page.within('#new_personal_snippet .md-preview') do + page.within('#new_personal_snippet .md-preview-holder') do expect(page).to have_content('My Snippet') link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb index 8d567e925ef..bdbbe645779 100644 --- a/spec/features/tags/master_deletes_tag_spec.rb +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -37,7 +37,7 @@ describe 'Maintainer deletes tag' do context 'when pre-receive hook fails', :js do before do allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag) - .and_raise(Gitlab::Git::PreReceiveError, 'Do not delete tags') + .and_raise(Gitlab::Git::PreReceiveError, 'GitLab: Do not delete tags') end it 'shows the error message' do diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index 6fe840dccf6..33d9c10f5e8 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -79,7 +79,7 @@ describe 'Task Lists' do visit_issue(project, issue) wait_for_requests - expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector(".md .task-list .task-list-item .task-list-item-checkbox") expect(page).to have_selector('a.btn-close') end @@ -87,14 +87,14 @@ describe 'Task Lists' do visit_issue(project, issue) wait_for_requests - expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector(".md .task-list .task-list-item .task-list-item-checkbox") logout(:user) login_as(user2) visit current_path wait_for_requests - expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector(".md .task-list .task-list-item .task-list-item-checkbox") end it 'provides a summary on Issues#index' do @@ -231,7 +231,7 @@ describe 'Task Lists' do container = '.detail-page-description .description.js-task-list-container' expect(page).to have_selector(container) - expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox") + expect(page).to have_selector("#{container} .md .task-list .task-list-item .task-list-item-checkbox") expect(page).to have_selector("#{container} .js-task-list-field") expect(page).to have_selector('form.js-issuable-update') expect(page).to have_selector('a.btn-close') diff --git a/spec/features/user_opens_link_to_comment.rb b/spec/features/user_opens_link_to_comment.rb new file mode 100644 index 00000000000..f1e07e55799 --- /dev/null +++ b/spec/features/user_opens_link_to_comment.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'User opens link to comment', :js do + let(:project) { create(:project, :public) } + let(:note) { create(:note_on_issue, project: project) } + + context 'authenticated user' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'switches to all activity and does not show error message' do + create(:user_preference, user: user, issue_notes_filter: UserPreference::NOTES_FILTERS[:only_activity]) + + visit Gitlab::UrlBuilder.build(note) + + expect(page.find('#discussion-filter-dropdown')).to have_content('Show all activity') + expect(page).not_to have_content('Something went wrong while fetching comments') + end + end + + context 'anonymous user' do + it 'does not show error message' do + visit Gitlab::UrlBuilder.build(note) + + expect(page).not_to have_content('Something went wrong while fetching comments') + end + end +end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index ad856bd062e..9d5780d29b0 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -434,16 +434,22 @@ describe 'Login' do context 'within the grace period' do it 'redirects to two-factor configuration page' do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - - gitlab_sign_in(user) - - expect(current_path).to eq profile_two_factor_auth_path - expect(page).to have_content( - 'The group settings for Group 1 and Group 2 require you to enable ' \ - 'Two-Factor Authentication for your account. You need to do this ' \ - 'before ') + Timecop.freeze do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + + gitlab_sign_in(user) + + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content( + 'The group settings for Group 1 and Group 2 require you to enable '\ + 'Two-Factor Authentication for your account. '\ + 'You can leave Group 1 and leave Group 2. '\ + 'You need to do this '\ + 'before '\ + "#{(Time.zone.now + 2.days).strftime("%a, %d %b %Y %H:%M:%S %z")}" + ) + end end it 'allows skipping two-factor configuration', :js do @@ -500,7 +506,8 @@ describe 'Login' do expect(current_path).to eq profile_two_factor_auth_path expect(page).to have_content( 'The group settings for Group 1 and Group 2 require you to enable ' \ - 'Two-Factor Authentication for your account.' + 'Two-Factor Authentication for your account. '\ + 'You can leave Group 1 and leave Group 2.' ) end end diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb index d6d95906f5e..f8fcc2d0e40 100644 --- a/spec/finders/group_projects_finder_spec.rb +++ b/spec/finders/group_projects_finder_spec.rb @@ -1,26 +1,7 @@ require 'spec_helper' describe GroupProjectsFinder do - let(:group) { create(:group) } - let(:subgroup) { create(:group, parent: group) } - let(:current_user) { create(:user) } - let(:options) { {} } - - let(:finder) { described_class.new(group: group, current_user: current_user, options: options) } - - let!(:public_project) { create(:project, :public, group: group, path: '1') } - let!(:private_project) { create(:project, :private, group: group, path: '2') } - let!(:shared_project_1) { create(:project, :public, path: '3') } - let!(:shared_project_2) { create(:project, :private, path: '4') } - let!(:shared_project_3) { create(:project, :internal, path: '5') } - let!(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) } - let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) } - - before do - shared_project_1.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) - shared_project_2.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) - shared_project_3.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) - end + include_context 'GroupProjectsFinder context' subject { finder.execute } @@ -144,6 +125,24 @@ describe GroupProjectsFinder do end end + describe 'with an admin current user' do + let(:current_user) { create(:admin) } + + context "only shared" do + let(:options) { { only_shared: true } } + it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) } + end + + context "only owned" do + let(:options) { { only_owned: true } } + it { is_expected.to eq([private_project, public_project]) } + end + + context "all" do + it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) } + end + end + describe "no user" do context "only shared" do let(:options) { { only_shared: true } } diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 55efab7dec3..00b6cad1a66 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -1,45 +1,10 @@ require 'spec_helper' describe IssuesFinder do - set(:user) { create(:user) } - set(:user2) { create(:user) } - set(:group) { create(:group) } - set(:subgroup) { create(:group, parent: group) } - set(:project1) { create(:project, group: group) } - set(:project2) { create(:project) } - set(:project3) { create(:project, group: subgroup) } - set(:milestone) { create(:milestone, project: project1) } - set(:label) { create(:label, project: project2) } - set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) } - set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) } - set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) } - set(:issue4) { create(:issue, project: project3) } - set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) } - set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) } - set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) } + include_context 'IssuesFinder context' describe '#execute' do - let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } - let!(:label_link) { create(:label_link, label: label, target: issue2) } - let(:search_user) { user } - let(:params) { {} } - let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } - - before(:context) do - project1.add_maintainer(user) - project2.add_developer(user) - project2.add_developer(user2) - project3.add_developer(user) - - issue1 - issue2 - issue3 - issue4 - - award_emoji1 - award_emoji2 - award_emoji3 - end + include_context 'IssuesFinder#execute context' context 'scope: all' do let(:scope) { 'all' } @@ -56,6 +21,21 @@ describe IssuesFinder do end end + context 'filtering by assignee usernames' do + set(:user3) { create(:user) } + let(:params) { { assignee_username: [user2.username, user3.username] } } + + before do + project2.add_developer(user3) + + issue3.assignees = [user2, user3] + end + + it 'returns issues assigned to those users' do + expect(issues).to contain_exactly(issue3) + end + end + context 'filtering by no assignee' do let(:params) { { assignee_id: 'None' } } @@ -220,6 +200,7 @@ describe IssuesFinder do let(:yesterday) { Date.today - 1.day } let(:tomorrow) { Date.today + 1.day } let(:two_days_ago) { Date.today - 2.days } + let(:three_days_ago) { Date.today - 3.days } let(:milestones) do [ @@ -227,6 +208,8 @@ describe IssuesFinder do create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago), create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday), create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow), + create(:milestone, :closed, project: project_started_1_and_2, title: '4.0', start_date: three_days_ago), + create(:milestone, :closed, project: project_started_8, title: '6.0', start_date: three_days_ago), create(:milestone, project: project_started_8, title: '7.0'), create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday), create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow) @@ -640,6 +623,16 @@ describe IssuesFinder do expect(subject).to include(public_issue, confidential_issue) end end + + context 'for an admin' do + let(:admin_user) { create(:user, :admin) } + + subject { described_class.new(admin_user, params).with_confidentiality_access_check } + + it 'returns all issues' do + expect(subject).to include(public_issue, confidential_issue) + end + end end context 'when searching within a specific project' do @@ -707,6 +700,22 @@ describe IssuesFinder do subject end end + + context 'for an admin' do + let(:admin_user) { create(:user, :admin) } + + subject { described_class.new(admin_user, params).with_confidentiality_access_check } + + it 'returns all issues' do + expect(subject).to include(public_issue, confidential_issue) + end + + it 'does not filter by confidentiality' do + expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything) + + subject + end + end end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index f1178b07eec..56136eb84bc 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -1,72 +1,24 @@ require 'spec_helper' describe MergeRequestsFinder do - include ProjectForksHelper - - # We need to explicitly permit Gitaly N+1s because of the specs that use - # :request_store. Gitaly N+1 detection is only enabled when :request_store is, - # but we don't care about potential N+1s when we're just creating several - # projects in the setup phase. - def create_project_without_n_plus_1(*args) - Gitlab::GitalyClient.allow_n_plus_1_calls do - create(:project, :public, *args) - end - end - context "multiple projects with merge requests" do - let(:user) { create :user } - let(:user2) { create :user } - - let(:group) { create(:group) } - let(:subgroup) { create(:group, parent: group) } - let(:project1) { create_project_without_n_plus_1(group: group) } - let(:project2) do - Gitlab::GitalyClient.allow_n_plus_1_calls do - fork_project(project1, user) - end - end - let(:project3) do - Gitlab::GitalyClient.allow_n_plus_1_calls do - p = fork_project(project1, user) - p.update!(archived: true) - p - end - end - let(:project4) { create_project_without_n_plus_1(:repository, group: subgroup) } - let(:project5) { create_project_without_n_plus_1(group: subgroup) } - let(:project6) { create_project_without_n_plus_1(group: subgroup) } - - let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } - let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } - let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } - let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } - let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') } - let!(:merge_request6) { create(:merge_request, :simple, author: user, source_project: project5, target_project: project5, title: 'WIP: thing') } - let!(:merge_request7) { create(:merge_request, :simple, author: user, source_project: project6, target_project: project6, title: 'wip thing') } - let!(:merge_request8) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project1, title: '[wip] thing') } - let!(:merge_request9) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project2, title: 'wip: thing') } - - before do - project1.add_maintainer(user) - project2.add_developer(user) - project3.add_developer(user) - project2.add_developer(user2) - project4.add_developer(user) - project5.add_developer(user) - project6.add_developer(user) - end + include_context 'MergeRequestsFinder multiple projects with merge requests context' describe '#execute' do it 'filters by scope' do params = { scope: 'authored', state: 'opened' } + merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(7) + + expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5) end it 'filters by project' do params = { project_id: project1.id, scope: 'authored', state: 'opened' } + merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(2) + + expect(merge_requests).to contain_exactly(merge_request1) end it 'filters by commit sha' do @@ -79,24 +31,15 @@ describe MergeRequestsFinder do end context 'filtering by group' do - it 'includes all merge requests when user has access' do - params = { group_id: group.id } - - merge_requests = described_class.new(user, params).execute - - expect(merge_requests.size).to eq(3) - end - - it 'excludes merge requests from projects the user does not have access to' do - private_project = create_project_without_n_plus_1(:private, group: group) - private_mr = create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project) + it 'includes all merge requests when user has access exceluding merge requests from projects the user does not have access to' do + private_project = allow_gitaly_n_plus_1 { create(:project, :private, group: group) } + private_project.add_guest(user) + create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project) params = { group_id: group.id } - private_project.add_guest(user) merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(3) - expect(merge_requests).not_to include(private_mr) + expect(merge_requests).to contain_exactly(merge_request1, merge_request2) end it 'filters by group including subgroups', :nested_groups do @@ -104,14 +47,16 @@ describe MergeRequestsFinder do merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(6) + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request5) end end it 'filters by non_archived' do params = { non_archived: true } + merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(8) + + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request5) end it 'filters by iid' do @@ -146,41 +91,45 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request3) end - it 'filters by wip' do - params = { wip: 'yes' } + describe 'WIP state' do + let!(:wip_merge_request1) { create(:merge_request, :simple, author: user, source_project: project5, target_project: project5, title: 'WIP: thing') } + let!(:wip_merge_request2) { create(:merge_request, :simple, author: user, source_project: project6, target_project: project6, title: 'wip thing') } + let!(:wip_merge_request3) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project1, title: '[wip] thing') } + let!(:wip_merge_request4) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project2, title: 'wip: thing') } - merge_requests = described_class.new(user, params).execute + it 'filters by wip' do + params = { wip: 'yes' } - expect(merge_requests).to contain_exactly(merge_request4, merge_request5, merge_request6, merge_request7, merge_request8, merge_request9) - end + merge_requests = described_class.new(user, params).execute - it 'filters by not wip' do - params = { wip: 'no' } + expect(merge_requests).to contain_exactly(merge_request4, merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4) + end - merge_requests = described_class.new(user, params).execute + it 'filters by not wip' do + params = { wip: 'no' } - expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3) - end + merge_requests = described_class.new(user, params).execute - it 'returns all items if no valid wip param exists' do - params = { wip: '' } + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3) + end - merge_requests = described_class.new(user, params).execute + it 'returns all items if no valid wip param exists' do + params = { wip: '' } - expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5, merge_request6, merge_request7, merge_request8, merge_request9) - end + merge_requests = described_class.new(user, params).execute - it 'adds wip to scalar params' do - scalar_params = described_class.scalar_params + expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4) + end + + it 'adds wip to scalar params' do + scalar_params = described_class.scalar_params - expect(scalar_params).to include(:wip, :assignee_id) + expect(scalar_params).to include(:wip, :assignee_id) + end end context 'filtering by group milestone' do - let!(:group) { create(:group, :public) } let(:group_milestone) { create(:milestone, group: group) } - let!(:group_member) { create(:group_member, group: group, user: user) } - let(:params) { { milestone_title: group_milestone.title } } before do project2.update(namespace: group) @@ -188,7 +137,9 @@ describe MergeRequestsFinder do merge_request3.update(milestone: group_milestone) end - it 'returns issues assigned to that group milestone' do + it 'returns merge requests assigned to that group milestone' do + params = { milestone_title: group_milestone.title } + merge_requests = described_class.new(user, params).execute expect(merge_requests).to contain_exactly(merge_request2, merge_request3) @@ -285,7 +236,7 @@ describe MergeRequestsFinder do it 'returns the number of rows for the default state' do finder = described_class.new(user) - expect(finder.row_count).to eq(7) + expect(finder.row_count).to eq(3) end it 'returns the number of rows for a given state' do diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 134fb5f2c04..93287f3e9b8 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe SnippetsFinder do include Gitlab::Allowable - using RSpec::Parameterized::TableSyntax describe '#initialize' do it 'raises ArgumentError when a project and author are given' do @@ -14,174 +13,142 @@ describe SnippetsFinder do end end - context 'filter by scope' do - let(:user) { create :user } - let!(:snippet1) { create(:personal_snippet, :private, author: user) } - let!(:snippet2) { create(:personal_snippet, :internal, author: user) } - let!(:snippet3) { create(:personal_snippet, :public, author: user) } - - it "returns all snippets for 'all' scope" do - snippets = described_class.new(user, scope: :all).execute - - expect(snippets).to include(snippet1, snippet2, snippet3) - end - - it "returns all snippets for 'are_private' scope" do - snippets = described_class.new(user, scope: :are_private).execute + describe '#execute' do + set(:user) { create(:user) } + set(:private_personal_snippet) { create(:personal_snippet, :private, author: user) } + set(:internal_personal_snippet) { create(:personal_snippet, :internal, author: user) } + set(:public_personal_snippet) { create(:personal_snippet, :public, author: user) } - expect(snippets).to include(snippet1) - expect(snippets).not_to include(snippet2, snippet3) - end + context 'filter by scope' do + it "returns all snippets for 'all' scope" do + snippets = described_class.new(user, scope: :all).execute - it "returns all snippets for 'are_internal' scope" do - snippets = described_class.new(user, scope: :are_internal).execute + expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) + end - expect(snippets).to include(snippet2) - expect(snippets).not_to include(snippet1, snippet3) - end + it "returns all snippets for 'are_private' scope" do + snippets = described_class.new(user, scope: :are_private).execute - it "returns all snippets for 'are_private' scope" do - snippets = described_class.new(user, scope: :are_public).execute + expect(snippets).to contain_exactly(private_personal_snippet) + end - expect(snippets).to include(snippet3) - expect(snippets).not_to include(snippet1, snippet2) - end - end + it "returns all snippets for 'are_internal' scope" do + snippets = described_class.new(user, scope: :are_internal).execute - context 'filter by author' do - let(:user) { create :user } - let(:user1) { create :user } - let!(:snippet1) { create(:personal_snippet, :private, author: user) } - let!(:snippet2) { create(:personal_snippet, :internal, author: user) } - let!(:snippet3) { create(:personal_snippet, :public, author: user) } + expect(snippets).to contain_exactly(internal_personal_snippet) + end - it "returns all public and internal snippets" do - snippets = described_class.new(user1, author: user).execute + it "returns all snippets for 'are_private' scope" do + snippets = described_class.new(user, scope: :are_public).execute - expect(snippets).to include(snippet2, snippet3) - expect(snippets).not_to include(snippet1) + expect(snippets).to contain_exactly(public_personal_snippet) + end end - it "returns internal snippets" do - snippets = described_class.new(user, author: user, scope: :are_internal).execute + context 'filter by author' do + it 'returns all public and internal snippets' do + snippets = described_class.new(create(:user), author: user).execute - expect(snippets).to include(snippet2) - expect(snippets).not_to include(snippet1, snippet3) - end + expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet) + end - it "returns private snippets" do - snippets = described_class.new(user, author: user, scope: :are_private).execute + it 'returns internal snippets' do + snippets = described_class.new(user, author: user, scope: :are_internal).execute - expect(snippets).to include(snippet1) - expect(snippets).not_to include(snippet2, snippet3) - end + expect(snippets).to contain_exactly(internal_personal_snippet) + end - it "returns public snippets" do - snippets = described_class.new(user, author: user, scope: :are_public).execute + it 'returns private snippets' do + snippets = described_class.new(user, author: user, scope: :are_private).execute - expect(snippets).to include(snippet3) - expect(snippets).not_to include(snippet1, snippet2) - end + expect(snippets).to contain_exactly(private_personal_snippet) + end - it "returns all snippets" do - snippets = described_class.new(user, author: user).execute + it 'returns public snippets' do + snippets = described_class.new(user, author: user, scope: :are_public).execute - expect(snippets).to include(snippet1, snippet2, snippet3) - end + expect(snippets).to contain_exactly(public_personal_snippet) + end - it "returns only public snippets if unauthenticated user" do - snippets = described_class.new(nil, author: user).execute + it 'returns all snippets' do + snippets = described_class.new(user, author: user).execute - expect(snippets).to include(snippet3) - expect(snippets).not_to include(snippet2, snippet1) - end + expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) + end - it 'returns all snippets for an admin' do - admin = create(:user, :admin) - snippets = described_class.new(admin, author: user).execute + it 'returns only public snippets if unauthenticated user' do + snippets = described_class.new(nil, author: user).execute - expect(snippets).to include(snippet1, snippet2, snippet3) - end - end + expect(snippets).to contain_exactly(public_personal_snippet) + end - context 'filter by project' do - let(:user) { create :user } - let(:group) { create :group, :public } - let(:project1) { create(:project, :public, group: group) } + it 'returns all snippets for an admin' do + admin = create(:user, :admin) + snippets = described_class.new(admin, author: user).execute - before do - @snippet1 = create(:project_snippet, :private, project: project1) - @snippet2 = create(:project_snippet, :internal, project: project1) - @snippet3 = create(:project_snippet, :public, project: project1) + expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) + end end - it "returns public snippets for unauthorized user" do - snippets = described_class.new(nil, project: project1).execute + context 'project snippets' do + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, group: group) } + let!(:private_project_snippet) { create(:project_snippet, :private, project: project) } + let!(:internal_project_snippet) { create(:project_snippet, :internal, project: project) } + let!(:public_project_snippet) { create(:project_snippet, :public, project: project) } - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet1, @snippet2) - end + it 'returns public personal and project snippets for unauthorized user' do + snippets = described_class.new(nil, project: project).execute - it "returns public and internal snippets for non project members" do - snippets = described_class.new(user, project: project1).execute + expect(snippets).to contain_exactly(public_project_snippet) + end - expect(snippets).to include(@snippet2, @snippet3) - expect(snippets).not_to include(@snippet1) - end + it 'returns public and internal snippets for non project members' do + snippets = described_class.new(user, project: project).execute - it "returns public snippets for non project members" do - snippets = described_class.new(user, project: project1, scope: :are_public).execute + expect(snippets).to contain_exactly(internal_project_snippet, public_project_snippet) + end - expect(snippets).to include(@snippet3) - expect(snippets).not_to include(@snippet1, @snippet2) - end + it 'returns public snippets for non project members' do + snippets = described_class.new(user, project: project, scope: :are_public).execute - it "returns internal snippets for non project members" do - snippets = described_class.new(user, project: project1, scope: :are_internal).execute + expect(snippets).to contain_exactly(public_project_snippet) + end - expect(snippets).to include(@snippet2) - expect(snippets).not_to include(@snippet1, @snippet3) - end + it 'returns internal snippets for non project members' do + snippets = described_class.new(user, project: project, scope: :are_internal).execute - it "does not return private snippets for non project members" do - snippets = described_class.new(user, project: project1, scope: :are_private).execute + expect(snippets).to contain_exactly(internal_project_snippet) + end - expect(snippets).not_to include(@snippet1, @snippet2, @snippet3) - end + it 'does not return private snippets for non project members' do + snippets = described_class.new(user, project: project, scope: :are_private).execute - it "returns all snippets for project members" do - project1.add_developer(user) + expect(snippets).to be_empty + end - snippets = described_class.new(user, project: project1).execute + it 'returns all snippets for project members' do + project.add_developer(user) - expect(snippets).to include(@snippet1, @snippet2, @snippet3) - end + snippets = described_class.new(user, project: project).execute - it "returns private snippets for project members" do - project1.add_developer(user) + expect(snippets).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet) + end - snippets = described_class.new(user, project: project1, scope: :are_private).execute + it 'returns private snippets for project members' do + project.add_developer(user) - expect(snippets).to include(@snippet1) - end + snippets = described_class.new(user, project: project, scope: :are_private).execute - it 'returns all snippets for an admin' do - admin = create(:user, :admin) - snippets = described_class.new(admin, project: project1).execute + expect(snippets).to contain_exactly(private_project_snippet) + end - expect(snippets).to include(@snippet1, @snippet2, @snippet3) - end - end + it 'returns all snippets for an admin' do + admin = create(:user, :admin) + snippets = described_class.new(admin, project: project).execute - describe '#execute' do - let(:project) { create(:project, :public) } - let!(:project_snippet) { create(:project_snippet, :public, project: project) } - let!(:personal_snippet) { create(:personal_snippet, :public) } - let(:user) { create(:user) } - subject(:finder) { described_class.new(user) } - - it 'returns project- and personal snippets' do - expect(finder.execute).to contain_exactly(project_snippet, personal_snippet) + expect(snippets).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet) + end end context 'when the user cannot read cross project' do @@ -191,7 +158,7 @@ describe SnippetsFinder do end it 'returns only personal snippets when the user cannot read cross project' do - expect(finder.execute).to contain_exactly(personal_snippet) + expect(described_class.new(user).execute).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet) end end end diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb index fecf97dc641..d71d3c99272 100644 --- a/spec/finders/users_finder_spec.rb +++ b/spec/finders/users_finder_spec.rb @@ -2,10 +2,7 @@ require 'spec_helper' describe UsersFinder do describe '#execute' do - let!(:user1) { create(:user, username: 'johndoe') } - let!(:user2) { create(:user, :blocked, username: 'notsorandom') } - let!(:external_user) { create(:user, :external) } - let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') } + include_context 'UsersFinder#execute filter by project context' context 'with a normal user' do let(:user) { create(:user) } @@ -13,43 +10,43 @@ describe UsersFinder do it 'returns all users' do users = described_class.new(user).execute - expect(users).to contain_exactly(user, user1, user2, omniauth_user) + expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user) end it 'filters by username' do users = described_class.new(user, username: 'johndoe').execute - expect(users).to contain_exactly(user1) + expect(users).to contain_exactly(normal_user) end it 'filters by username (case insensitive)' do users = described_class.new(user, username: 'joHNdoE').execute - expect(users).to contain_exactly(user1) + expect(users).to contain_exactly(normal_user) end it 'filters by search' do users = described_class.new(user, search: 'orando').execute - expect(users).to contain_exactly(user2) + expect(users).to contain_exactly(blocked_user) end it 'filters by blocked users' do users = described_class.new(user, blocked: true).execute - expect(users).to contain_exactly(user2) + expect(users).to contain_exactly(blocked_user) end it 'filters by active users' do users = described_class.new(user, active: true).execute - expect(users).to contain_exactly(user, user1, omniauth_user) + expect(users).to contain_exactly(user, normal_user, omniauth_user) end it 'returns no external users' do users = described_class.new(user, external: true).execute - expect(users).to contain_exactly(user, user1, user2, omniauth_user) + expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user) end it 'filters by created_at' do @@ -69,7 +66,7 @@ describe UsersFinder do custom_attributes: { foo: 'bar' } ).execute - expect(users).to contain_exactly(user, user1, user2, omniauth_user) + expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user) end end @@ -85,20 +82,20 @@ describe UsersFinder do it 'returns all users' do users = described_class.new(admin).execute - expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user) + expect(users).to contain_exactly(admin, normal_user, blocked_user, external_user, omniauth_user) end it 'filters by custom attributes' do - create :user_custom_attribute, user: user1, key: 'foo', value: 'foo' - create :user_custom_attribute, user: user1, key: 'bar', value: 'bar' - create :user_custom_attribute, user: user2, key: 'foo', value: 'foo' + create :user_custom_attribute, user: normal_user, key: 'foo', value: 'foo' + create :user_custom_attribute, user: normal_user, key: 'bar', value: 'bar' + create :user_custom_attribute, user: blocked_user, key: 'foo', value: 'foo' users = described_class.new( admin, custom_attributes: { foo: 'foo', bar: 'bar' } ).execute - expect(users).to contain_exactly(user1) + expect(users).to contain_exactly(normal_user) end end end diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml index 046215e4c93..054dc27cda6 100644 --- a/spec/frontend/.eslintrc.yml +++ b/spec/frontend/.eslintrc.yml @@ -2,8 +2,13 @@ env: jest/globals: true plugins: -- jest + - jest settings: import/resolver: jest: - jestConfigFile: "jest.config.js" + jestConfigFile: 'jest.config.js' +globals: + getJSONFixture: false + loadFixtures: false + preloadFixtures: false + setFixtures: false diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/frontend/behaviors/secret_values_spec.js index 5aaab093c0c..5aaab093c0c 100644 --- a/spec/javascripts/behaviors/secret_values_spec.js +++ b/spec/frontend/behaviors/secret_values_spec.js diff --git a/spec/javascripts/blob/blob_fork_suggestion_spec.js b/spec/frontend/blob/blob_fork_suggestion_spec.js index 9b81b7e6f92..9b81b7e6f92 100644 --- a/spec/javascripts/blob/blob_fork_suggestion_spec.js +++ b/spec/frontend/blob/blob_fork_suggestion_spec.js diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/frontend/boards/modal_store_spec.js index 3257a3fb8a3..3257a3fb8a3 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/frontend/boards/modal_store_spec.js diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/frontend/cycle_analytics/limit_warning_component_spec.js index 13e9fe00a00..13e9fe00a00 100644 --- a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js +++ b/spec/frontend/cycle_analytics/limit_warning_component_spec.js diff --git a/spec/javascripts/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js index 984b3026209..984b3026209 100644 --- a/spec/javascripts/diffs/components/diff_stats_spec.js +++ b/spec/frontend/diffs/components/diff_stats_spec.js diff --git a/spec/javascripts/diffs/components/edit_button_spec.js b/spec/frontend/diffs/components/edit_button_spec.js index ccdae4cb312..ccdae4cb312 100644 --- a/spec/javascripts/diffs/components/edit_button_spec.js +++ b/spec/frontend/diffs/components/edit_button_spec.js diff --git a/spec/javascripts/diffs/components/hidden_files_warning_spec.js b/spec/frontend/diffs/components/hidden_files_warning_spec.js index 5bf5ddd27bd..5bf5ddd27bd 100644 --- a/spec/javascripts/diffs/components/hidden_files_warning_spec.js +++ b/spec/frontend/diffs/components/hidden_files_warning_spec.js diff --git a/spec/javascripts/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js index e45d34bf9d5..e45d34bf9d5 100644 --- a/spec/javascripts/diffs/components/no_changes_spec.js +++ b/spec/frontend/diffs/components/no_changes_spec.js diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js new file mode 100644 index 00000000000..1067a53906a --- /dev/null +++ b/spec/frontend/environment.js @@ -0,0 +1,33 @@ +/* eslint-disable import/no-commonjs */ + +const { ErrorWithStack } = require('jest-util'); +const JSDOMEnvironment = require('jest-environment-jsdom'); + +class CustomEnvironment extends JSDOMEnvironment { + constructor(config, context) { + super(config, context); + + Object.assign(context.console, { + error(...args) { + throw new ErrorWithStack( + `Unexpected call of console.error() with:\n\n${args.join(', ')}`, + this.error, + ); + }, + + warn(...args) { + throw new ErrorWithStack( + `Unexpected call of console.warn() with:\n\n${args.join(', ')}`, + this.warn, + ); + }, + }); + + const { testEnvironmentOptions } = config; + this.global.gon = { + ee: testEnvironmentOptions.IS_EE, + }; + } +} + +module.exports = CustomEnvironment; diff --git a/spec/javascripts/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js index 503af3920a8..503af3920a8 100644 --- a/spec/javascripts/error_tracking/components/error_tracking_list_spec.js +++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js diff --git a/spec/javascripts/error_tracking/store/mutation_spec.js b/spec/frontend/error_tracking/store/mutation_spec.js index 8117104bdbc..8117104bdbc 100644 --- a/spec/javascripts/error_tracking/store/mutation_spec.js +++ b/spec/frontend/error_tracking/store/mutation_spec.js diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/frontend/filtered_search/filtered_search_token_keys_spec.js index d1fea18dea8..d1fea18dea8 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/frontend/filtered_search/filtered_search_token_keys_spec.js diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js index ea7c146fa4f..ea7c146fa4f 100644 --- a/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js +++ b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js diff --git a/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js b/spec/frontend/filtered_search/stores/recent_searches_store_spec.js index 56bb82ae941..56bb82ae941 100644 --- a/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js +++ b/spec/frontend/filtered_search/stores/recent_searches_store_spec.js diff --git a/spec/javascripts/frequent_items/store/getters_spec.js b/spec/frontend/frequent_items/store/getters_spec.js index 1cd12eb6832..1cd12eb6832 100644 --- a/spec/javascripts/frequent_items/store/getters_spec.js +++ b/spec/frontend/frequent_items/store/getters_spec.js diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 27c679c749b..ed12af925f1 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -6,17 +6,26 @@ import GfmAutoComplete from '~/gfm_auto_complete'; import 'jquery.caret'; import 'at.js'; +import { TEST_HOST } from 'helpers/test_constants'; +import { setTestTimeout } from 'helpers/timeout'; +import { getJSONFixture } from 'helpers/fixtures'; + +setTestTimeout(500); + +const labelsFixture = getJSONFixture('autocomplete_sources/labels.json'); + describe('GfmAutoComplete', () => { const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ fetchData: () => {}, }); let atwhoInstance; - let items; let sorterValue; describe('DefaultOptions.sorter', () => { describe('assets loading', () => { + let items; + beforeEach(() => { jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(true); @@ -61,7 +70,7 @@ describe('GfmAutoComplete', () => { atwhoInstance = { setting: {} }; const query = 'query'; - items = []; + const items = []; const searchKey = 'searchKey'; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey); @@ -250,4 +259,90 @@ describe('GfmAutoComplete', () => { ).toBe('<li><small>grp/proj#5</small> Some Issue</li>'); }); }); + + describe('labels', () => { + const dataSources = { + labels: `${TEST_HOST}/autocomplete_sources/labels`, + }; + + const allLabels = labelsFixture; + const assignedLabels = allLabels.filter(label => label.set); + const unassignedLabels = allLabels.filter(label => !label.set); + + let autocomplete; + let $textarea; + + beforeEach(() => { + autocomplete = new GfmAutoComplete(dataSources); + $textarea = $('<textarea></textarea>'); + autocomplete.setup($textarea, { labels: true }); + }); + + afterEach(() => { + autocomplete.destroy(); + }); + + const triggerDropdown = text => { + $textarea + .trigger('focus') + .val(text) + .caret('pos', -1); + $textarea.trigger('keyup'); + + return new Promise(window.requestAnimationFrame); + }; + + const getDropdownItems = () => { + const dropdown = document.getElementById('at-view-labels'); + const items = dropdown.getElementsByTagName('li'); + return [].map.call(items, item => item.textContent.trim()); + }; + + const expectLabels = ({ input, output }) => + triggerDropdown(input).then(() => { + expect(getDropdownItems()).toEqual(output.map(label => label.title)); + }); + + describe('with no labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = [...unassignedLabels]; + }); + + it.each` + input | output + ${'~'} | ${unassignedLabels} + ${'/label ~'} | ${unassignedLabels} + ${'/relabel ~'} | ${unassignedLabels} + ${'/unlabel ~'} | ${[]} + `('$input shows $output.length labels', expectLabels); + }); + + describe('with some labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = allLabels; + }); + + it.each` + input | output + ${'~'} | ${allLabels} + ${'/label ~'} | ${unassignedLabels} + ${'/relabel ~'} | ${allLabels} + ${'/unlabel ~'} | ${assignedLabels} + `('$input shows $output.length labels', expectLabels); + }); + + describe('with all labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = [...assignedLabels]; + }); + + it.each` + input | output + ${'~'} | ${assignedLabels} + ${'/label ~'} | ${[]} + ${'/relabel ~'} | ${assignedLabels} + ${'/unlabel ~'} | ${assignedLabels} + `('$input shows $output.length labels', expectLabels); + }); + }); }); diff --git a/spec/frontend/helpers/class_spec_helper.js b/spec/frontend/helpers/class_spec_helper.js new file mode 100644 index 00000000000..7a60d33b471 --- /dev/null +++ b/spec/frontend/helpers/class_spec_helper.js @@ -0,0 +1,9 @@ +export default class ClassSpecHelper { + static itShouldBeAStaticMethod(base, method) { + return it('should be a static method', () => { + expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy(); + }); + } +} + +window.ClassSpecHelper = ClassSpecHelper; diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js new file mode 100644 index 00000000000..f0351aa31c6 --- /dev/null +++ b/spec/frontend/helpers/fixtures.js @@ -0,0 +1,36 @@ +import fs from 'fs'; +import path from 'path'; + +import { ErrorWithStack } from 'jest-util'; + +const fixturesBasePath = path.join(process.cwd(), 'spec', 'javascripts', 'fixtures'); + +export function getFixture(relativePath) { + const absolutePath = path.join(fixturesBasePath, relativePath); + if (!fs.existsSync(absolutePath)) { + throw new ErrorWithStack( + `Fixture file ${relativePath} does not exist. + +Did you run bin/rake karma:fixtures?`, + getFixture, + ); + } + + return fs.readFileSync(absolutePath, 'utf8'); +} + +export const getJSONFixture = relativePath => JSON.parse(getFixture(relativePath)); + +export const resetHTMLFixture = () => { + document.body.textContent = ''; +}; + +export const setHTMLFixture = (htmlContent, resetHook = afterEach) => { + document.body.outerHTML = htmlContent; + resetHook(resetHTMLFixture); +}; + +export const loadHTMLFixture = (relativePath, resetHook = afterEach) => { + const fileContent = getFixture(relativePath); + setHTMLFixture(fileContent, resetHook); +}; diff --git a/spec/frontend/helpers/locale_helper.js b/spec/frontend/helpers/locale_helper.js new file mode 100644 index 00000000000..80047b06003 --- /dev/null +++ b/spec/frontend/helpers/locale_helper.js @@ -0,0 +1,11 @@ +/* eslint-disable import/prefer-default-export */ + +export const setLanguage = languageCode => { + const htmlElement = document.querySelector('html'); + + if (languageCode) { + htmlElement.setAttribute('lang', languageCode); + } else { + htmlElement.removeAttribute('lang'); + } +}; diff --git a/spec/frontend/helpers/scroll_into_view_promise.js b/spec/frontend/helpers/scroll_into_view_promise.js new file mode 100644 index 00000000000..0edea2103da --- /dev/null +++ b/spec/frontend/helpers/scroll_into_view_promise.js @@ -0,0 +1,28 @@ +export default function scrollIntoViewPromise(intersectionTarget, timeout = 100, maxTries = 5) { + return new Promise((resolve, reject) => { + let intersectionObserver; + let retry = 0; + + const intervalId = setInterval(() => { + if (retry >= maxTries) { + intersectionObserver.disconnect(); + clearInterval(intervalId); + reject(new Error(`Could not scroll target into viewPort within ${timeout * maxTries} ms`)); + } + retry += 1; + intersectionTarget.scrollIntoView(); + }, timeout); + + intersectionObserver = new IntersectionObserver(entries => { + if (entries[0].isIntersecting) { + intersectionObserver.disconnect(); + clearInterval(intervalId); + resolve(); + } + }); + + intersectionObserver.observe(intersectionTarget); + + intersectionTarget.scrollIntoView(); + }); +} diff --git a/spec/frontend/helpers/set_timeout_promise_helper.js b/spec/frontend/helpers/set_timeout_promise_helper.js new file mode 100644 index 00000000000..47087619187 --- /dev/null +++ b/spec/frontend/helpers/set_timeout_promise_helper.js @@ -0,0 +1,4 @@ +export default (time = 0) => + new Promise(resolve => { + setTimeout(resolve, time); + }); diff --git a/spec/frontend/helpers/user_mock_data_helper.js b/spec/frontend/helpers/user_mock_data_helper.js new file mode 100644 index 00000000000..6999fa1f8a1 --- /dev/null +++ b/spec/frontend/helpers/user_mock_data_helper.js @@ -0,0 +1,14 @@ +export default { + createNumberRandomUsers(numberUsers) { + const users = []; + for (let i = 0; i < numberUsers; i += 1) { + users.push({ + avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + id: i + 1, + name: `GitLab User ${i}`, + username: `gitlab${i}`, + }); + } + return users; + }, +}; diff --git a/spec/frontend/helpers/vue_component_helper.js b/spec/frontend/helpers/vue_component_helper.js new file mode 100644 index 00000000000..e0fe18e5560 --- /dev/null +++ b/spec/frontend/helpers/vue_component_helper.js @@ -0,0 +1,18 @@ +/** + * Replaces line break with an empty space + * @param {*} data + */ +export const removeBreakLine = data => data.replace(/\r?\n|\r/g, ' '); + +/** + * Removes line breaks, spaces and trims the given text + * @param {String} str + * @returns {String} + */ +export const trimText = str => + str + .replace(/\r?\n|\r/g, '') + .replace(/\s\s+/g, ' ') + .trim(); + +export const removeWhitespace = str => str.replace(/\s\s+/g, ' '); diff --git a/spec/frontend/helpers/vue_mount_component_helper.js b/spec/frontend/helpers/vue_mount_component_helper.js new file mode 100644 index 00000000000..6848c95d95d --- /dev/null +++ b/spec/frontend/helpers/vue_mount_component_helper.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; + +const mountComponent = (Component, props = {}, el = null) => + new Component({ + propsData: props, + }).$mount(el); + +export const createComponentWithStore = (Component, store, propsData = {}) => + new Component({ + store, + propsData, + }); + +export const mountComponentWithStore = (Component, { el, props, store }) => + new Component({ + store, + propsData: props || {}, + }).$mount(el); + +export const mountComponentWithSlots = (Component, { props, slots }) => { + const component = new Component({ + propsData: props || {}, + }); + + component.$slots = slots; + + return component.$mount(); +}; + +/** + * Mount a component with the given render method. + * + * This helps with inserting slots that need to be compiled. + */ +export const mountComponentWithRender = (render, el = null) => + mountComponent(Vue.extend({ render }), {}, el); + +export default mountComponent; diff --git a/spec/frontend/helpers/vue_resource_helper.js b/spec/frontend/helpers/vue_resource_helper.js new file mode 100644 index 00000000000..0f58af09933 --- /dev/null +++ b/spec/frontend/helpers/vue_resource_helper.js @@ -0,0 +1,11 @@ +// eslint-disable-next-line import/prefer-default-export +export const headersInterceptor = (request, next) => { + next(response => { + const headers = {}; + response.headers.forEach((value, key) => { + headers[key] = value; + }); + // eslint-disable-next-line no-param-reassign + response.headers = headers; + }); +}; diff --git a/spec/frontend/helpers/vue_test_utils_helper.js b/spec/frontend/helpers/vue_test_utils_helper.js new file mode 100644 index 00000000000..19e27388eeb --- /dev/null +++ b/spec/frontend/helpers/vue_test_utils_helper.js @@ -0,0 +1,19 @@ +/* eslint-disable import/prefer-default-export */ + +const vNodeContainsText = (vnode, text) => + (vnode.text && vnode.text.includes(text)) || + (vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length); + +/** + * Determines whether a `shallowMount` Wrapper contains text + * within one of it's slots. This will also work on Wrappers + * acquired with `find()`, but only if it's parent Wrapper + * was shallowMounted. + * NOTE: Prefer checking the rendered output of a component + * wherever possible using something like `text()` instead. + * @param {Wrapper} shallowWrapper - Vue test utils wrapper (shallowMounted) + * @param {String} slotName + * @param {String} text + */ +export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) => + !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length; diff --git a/spec/frontend/helpers/vuex_action_helper.js b/spec/frontend/helpers/vuex_action_helper.js new file mode 100644 index 00000000000..88652202a8e --- /dev/null +++ b/spec/frontend/helpers/vuex_action_helper.js @@ -0,0 +1,104 @@ +const noop = () => {}; + +/** + * Helper for testing action with expected mutations inspired in + * https://vuex.vuejs.org/en/testing.html + * + * @param {Function} action to be tested + * @param {Object} payload will be provided to the action + * @param {Object} state will be provided to the action + * @param {Array} [expectedMutations=[]] mutations expected to be committed + * @param {Array} [expectedActions=[]] actions expected to be dispatched + * @param {Function} [done=noop] to be executed after the tests + * @return {Promise} + * + * @example + * testAction( + * actions.actionName, // action + * { }, // mocked payload + * state, //state + * // expected mutations + * [ + * { type: types.MUTATION} + * { type: types.MUTATION_1, payload: jasmine.any(Number)} + * ], + * // expected actions + * [ + * { type: 'actionName', payload: {param: 'foobar'}}, + * { type: 'actionName1'} + * ] + * done, + * ); + * + * @example + * testAction( + * actions.actionName, // action + * { }, // mocked payload + * state, //state + * [ { type: types.MUTATION} ], // expected mutations + * [], // expected actions + * ).then(done) + * .catch(done.fail); + */ +export default ( + action, + payload, + state, + expectedMutations = [], + expectedActions = [], + done = noop, +) => { + const mutations = []; + const actions = []; + + // mock commit + const commit = (type, mutationPayload) => { + const mutation = { type }; + + if (typeof mutationPayload !== 'undefined') { + mutation.payload = mutationPayload; + } + + mutations.push(mutation); + }; + + // mock dispatch + const dispatch = (type, actionPayload) => { + const dispatchedAction = { type }; + + if (typeof actionPayload !== 'undefined') { + dispatchedAction.payload = actionPayload; + } + + actions.push(dispatchedAction); + }; + + const validateResults = () => { + expect({ + mutations, + actions, + }).toEqual({ + mutations: expectedMutations, + actions: expectedActions, + }); + done(); + }; + + const result = action( + { commit, state, dispatch, rootState: state, rootGetters: state, getters: state }, + payload, + ); + + return new Promise(resolve => { + setImmediate(resolve); + }) + .then(() => result) + .catch(error => { + validateResults(); + throw error; + }) + .then(data => { + validateResults(); + return data; + }); +}; diff --git a/spec/frontend/helpers/wait_for_attribute_change.js b/spec/frontend/helpers/wait_for_attribute_change.js new file mode 100644 index 00000000000..8f22d569222 --- /dev/null +++ b/spec/frontend/helpers/wait_for_attribute_change.js @@ -0,0 +1,16 @@ +export default (domElement, attributes, timeout = 1500) => + new Promise((resolve, reject) => { + let observer; + const timeoutId = setTimeout(() => { + observer.disconnect(); + reject(new Error(`Could not see an attribute update within ${timeout} ms`)); + }, timeout); + + observer = new MutationObserver(() => { + clearTimeout(timeoutId); + observer.disconnect(); + resolve(); + }); + + observer.observe(domElement, { attributes: true, attributeFilter: attributes }); + }); diff --git a/spec/frontend/helpers/wait_for_promises.js b/spec/frontend/helpers/wait_for_promises.js new file mode 100644 index 00000000000..1d2b53fc770 --- /dev/null +++ b/spec/frontend/helpers/wait_for_promises.js @@ -0,0 +1 @@ +export default () => new Promise(resolve => requestAnimationFrame(resolve)); diff --git a/spec/javascripts/ide/lib/common/disposable_spec.js b/spec/frontend/ide/lib/common/disposable_spec.js index af12ca15369..af12ca15369 100644 --- a/spec/javascripts/ide/lib/common/disposable_spec.js +++ b/spec/frontend/ide/lib/common/disposable_spec.js diff --git a/spec/javascripts/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js index 57f3ac3d365..57f3ac3d365 100644 --- a/spec/javascripts/ide/lib/diff/diff_spec.js +++ b/spec/frontend/ide/lib/diff/diff_spec.js diff --git a/spec/javascripts/ide/lib/editor_options_spec.js b/spec/frontend/ide/lib/editor_options_spec.js index d149a883166..d149a883166 100644 --- a/spec/javascripts/ide/lib/editor_options_spec.js +++ b/spec/frontend/ide/lib/editor_options_spec.js diff --git a/spec/javascripts/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js index fe791aa2b74..fe791aa2b74 100644 --- a/spec/javascripts/ide/lib/files_spec.js +++ b/spec/frontend/ide/lib/files_spec.js diff --git a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js index 5de7a281d34..5de7a281d34 100644 --- a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js +++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js diff --git a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js index 17cb457881f..17cb457881f 100644 --- a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js +++ b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js diff --git a/spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js index 8e0e3ae99a1..8e0e3ae99a1 100644 --- a/spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js +++ b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js diff --git a/spec/javascripts/ide/stores/modules/pane/getters_spec.js b/spec/frontend/ide/stores/modules/pane/getters_spec.js index 8a213323de0..8a213323de0 100644 --- a/spec/javascripts/ide/stores/modules/pane/getters_spec.js +++ b/spec/frontend/ide/stores/modules/pane/getters_spec.js diff --git a/spec/javascripts/ide/stores/modules/pane/mutations_spec.js b/spec/frontend/ide/stores/modules/pane/mutations_spec.js index b5fcd35912e..b5fcd35912e 100644 --- a/spec/javascripts/ide/stores/modules/pane/mutations_spec.js +++ b/spec/frontend/ide/stores/modules/pane/mutations_spec.js diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/frontend/ide/stores/modules/pipelines/getters_spec.js index 4514896b5ea..4514896b5ea 100644 --- a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js +++ b/spec/frontend/ide/stores/modules/pipelines/getters_spec.js diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/frontend/ide/stores/mutations/branch_spec.js index 29eb859ddaf..29eb859ddaf 100644 --- a/spec/javascripts/ide/stores/mutations/branch_spec.js +++ b/spec/frontend/ide/stores/mutations/branch_spec.js diff --git a/spec/javascripts/ide/stores/mutations/merge_request_spec.js b/spec/frontend/ide/stores/mutations/merge_request_spec.js index e30ca22022f..e30ca22022f 100644 --- a/spec/javascripts/ide/stores/mutations/merge_request_spec.js +++ b/spec/frontend/ide/stores/mutations/merge_request_spec.js diff --git a/spec/javascripts/image_diff/view_types_spec.js b/spec/frontend/image_diff/view_types_spec.js index e9639f46497..e9639f46497 100644 --- a/spec/javascripts/image_diff/view_types_spec.js +++ b/spec/frontend/image_diff/view_types_spec.js diff --git a/spec/javascripts/import_projects/store/getters_spec.js b/spec/frontend/import_projects/store/getters_spec.js index e5e4a95f473..e5e4a95f473 100644 --- a/spec/javascripts/import_projects/store/getters_spec.js +++ b/spec/frontend/import_projects/store/getters_spec.js diff --git a/spec/javascripts/import_projects/store/mutations_spec.js b/spec/frontend/import_projects/store/mutations_spec.js index 8db8e9819ba..8db8e9819ba 100644 --- a/spec/javascripts/import_projects/store/mutations_spec.js +++ b/spec/frontend/import_projects/store/mutations_spec.js diff --git a/spec/javascripts/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js index a2df79bdda0..a2df79bdda0 100644 --- a/spec/javascripts/jobs/components/empty_state_spec.js +++ b/spec/frontend/jobs/components/empty_state_spec.js diff --git a/spec/javascripts/jobs/components/erased_block_spec.js b/spec/frontend/jobs/components/erased_block_spec.js index 8e0433d3fb7..8e0433d3fb7 100644 --- a/spec/javascripts/jobs/components/erased_block_spec.js +++ b/spec/frontend/jobs/components/erased_block_spec.js diff --git a/spec/javascripts/jobs/components/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/sidebar_detail_row_spec.js index 42d11266dad..42d11266dad 100644 --- a/spec/javascripts/jobs/components/sidebar_detail_row_spec.js +++ b/spec/frontend/jobs/components/sidebar_detail_row_spec.js diff --git a/spec/javascripts/jobs/components/stuck_block_spec.js b/spec/frontend/jobs/components/stuck_block_spec.js index c320793b2be..c320793b2be 100644 --- a/spec/javascripts/jobs/components/stuck_block_spec.js +++ b/spec/frontend/jobs/components/stuck_block_spec.js diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/frontend/jobs/store/getters_spec.js index 379114c3737..379114c3737 100644 --- a/spec/javascripts/jobs/store/getters_spec.js +++ b/spec/frontend/jobs/store/getters_spec.js diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js index d7908efcf13..d7908efcf13 100644 --- a/spec/javascripts/jobs/store/mutations_spec.js +++ b/spec/frontend/jobs/store/mutations_spec.js diff --git a/spec/javascripts/labels_select_spec.js b/spec/frontend/labels_select_spec.js index acfdc885032..acfdc885032 100644 --- a/spec/javascripts/labels_select_spec.js +++ b/spec/frontend/labels_select_spec.js diff --git a/spec/frontend/lib/utils/autosave_spec.js b/spec/frontend/lib/utils/autosave_spec.js new file mode 100644 index 00000000000..12e97f6cdec --- /dev/null +++ b/spec/frontend/lib/utils/autosave_spec.js @@ -0,0 +1,64 @@ +import { clearDraft, getDraft, updateDraft } from '~/lib/utils/autosave'; + +describe('autosave utils', () => { + const autosaveKey = 'dummy-autosave-key'; + const text = 'some dummy text'; + + describe('clearDraft', () => { + beforeEach(() => { + localStorage.setItem(`autosave/${autosaveKey}`, text); + }); + + afterEach(() => { + localStorage.removeItem(`autosave/${autosaveKey}`); + }); + + it('removes the draft from localStorage', () => { + clearDraft(autosaveKey); + + expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(null); + }); + }); + + describe('getDraft', () => { + beforeEach(() => { + localStorage.setItem(`autosave/${autosaveKey}`, text); + }); + + afterEach(() => { + localStorage.removeItem(`autosave/${autosaveKey}`); + }); + + it('returns the draft from localStorage', () => { + const result = getDraft(autosaveKey); + + expect(result).toBe(text); + }); + + it('returns null if no entry exists in localStorage', () => { + localStorage.removeItem(`autosave/${autosaveKey}`); + + const result = getDraft(autosaveKey); + + expect(result).toBe(null); + }); + }); + + describe('updateDraft', () => { + beforeEach(() => { + localStorage.setItem(`autosave/${autosaveKey}`, text); + }); + + afterEach(() => { + localStorage.removeItem(`autosave/${autosaveKey}`); + }); + + it('removes the draft from localStorage', () => { + const newText = 'new text'; + + updateDraft(autosaveKey, newText); + + expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(newText); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/cache_spec.js b/spec/frontend/lib/utils/cache_spec.js index 2fe02a7592c..2fe02a7592c 100644 --- a/spec/javascripts/lib/utils/cache_spec.js +++ b/spec/frontend/lib/utils/cache_spec.js diff --git a/spec/javascripts/lib/utils/grammar_spec.js b/spec/frontend/lib/utils/grammar_spec.js index 377b2ffb48c..377b2ffb48c 100644 --- a/spec/javascripts/lib/utils/grammar_spec.js +++ b/spec/frontend/lib/utils/grammar_spec.js diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/frontend/lib/utils/image_utility_spec.js index a7eff419fba..a7eff419fba 100644 --- a/spec/javascripts/lib/utils/image_utility_spec.js +++ b/spec/frontend/lib/utils/image_utility_spec.js diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js index 818404bad81..818404bad81 100644 --- a/spec/javascripts/lib/utils/number_utility_spec.js +++ b/spec/frontend/lib/utils/number_utility_spec.js diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js index 0a266b19ea5..0a266b19ea5 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/frontend/lib/utils/text_utility_spec.js diff --git a/spec/javascripts/locale/ensure_single_line_spec.js b/spec/frontend/locale/ensure_single_line_spec.js index 20b04cab9c8..20b04cab9c8 100644 --- a/spec/javascripts/locale/ensure_single_line_spec.js +++ b/spec/frontend/locale/ensure_single_line_spec.js diff --git a/spec/javascripts/locale/sprintf_spec.js b/spec/frontend/locale/sprintf_spec.js index 52e903b819f..52e903b819f 100644 --- a/spec/javascripts/locale/sprintf_spec.js +++ b/spec/frontend/locale/sprintf_spec.js diff --git a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap new file mode 100644 index 00000000000..5f9f13d591d --- /dev/null +++ b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap @@ -0,0 +1,93 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`MR Popover loaded state matches the snapshot 1`] = ` +<glpopover-stub + boundary="viewport" + placement="top" + show="" + target="" +> + <div + class="mr-popover" + > + <div + class="d-flex align-items-center justify-content-between" + > + <div + class="d-inline-flex align-items-center" + > + <div + class="issuable-status-box status-box status-box-open" + > + + Open + + </div> + + <span + class="text-secondary" + > + Opened + <time> + just now + </time> + </span> + </div> + + <ciicon-stub + cssclasses="" + size="16" + status="[object Object]" + /> + </div> + + <h5 + class="my-2" + > + MR Title + </h5> + + <div + class="text-secondary" + > + + foo/bar!1 + + </div> + </div> +</glpopover-stub> +`; + +exports[`MR Popover shows skeleton-loader while apollo is loading 1`] = ` +<glpopover-stub + boundary="viewport" + placement="top" + show="" + target="" +> + <div + class="mr-popover" + > + <div> + <glskeletonloading-stub + class="animation-container-small mt-1" + lines="1" + /> + </div> + + <h5 + class="my-2" + > + MR Title + </h5> + + <div + class="text-secondary" + > + + foo/bar!1 + + </div> + </div> +</glpopover-stub> +`; diff --git a/spec/frontend/mr_popover/mr_popover_spec.js b/spec/frontend/mr_popover/mr_popover_spec.js new file mode 100644 index 00000000000..79ed4163010 --- /dev/null +++ b/spec/frontend/mr_popover/mr_popover_spec.js @@ -0,0 +1,61 @@ +import MRPopover from '~/mr_popover/components/mr_popover'; +import { shallowMount } from '@vue/test-utils'; + +describe('MR Popover', () => { + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(MRPopover, { + propsData: { + target: document.createElement('a'), + projectPath: 'foo/bar', + mergeRequestIID: '1', + mergeRequestTitle: 'MR Title', + }, + mocks: { + $apollo: { + loading: false, + }, + }, + }); + }); + + it('shows skeleton-loader while apollo is loading', () => { + wrapper.vm.$apollo.loading = true; + + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('loaded state', () => { + it('matches the snapshot', () => { + wrapper.setData({ + mergeRequest: { + state: 'opened', + createdAt: new Date(), + headPipeline: { + detailedStatus: { + group: 'success', + status: 'status_success', + }, + }, + }, + }); + + expect(wrapper.element).toMatchSnapshot(); + }); + + it('does not show CI Icon if there is no pipeline data', () => { + wrapper.setData({ + mergeRequest: { + state: 'opened', + headPipeline: null, + stateHumanName: 'Open', + title: 'Merge Request Title', + createdAt: new Date(), + }, + }); + + expect(wrapper.contains('ciicon-stub')).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/notebook/lib/highlight_spec.js b/spec/frontend/notebook/lib/highlight_spec.js index d71c5718858..d71c5718858 100644 --- a/spec/javascripts/notebook/lib/highlight_spec.js +++ b/spec/frontend/notebook/lib/highlight_spec.js diff --git a/spec/javascripts/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js index 07a366cf339..07a366cf339 100644 --- a/spec/javascripts/notes/components/discussion_reply_placeholder_spec.js +++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js diff --git a/spec/javascripts/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js index 5024f40ec5d..5024f40ec5d 100644 --- a/spec/javascripts/notes/components/discussion_resolve_button_spec.js +++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js diff --git a/spec/javascripts/notes/components/note_attachment_spec.js b/spec/frontend/notes/components/note_attachment_spec.js index b14a518b622..b14a518b622 100644 --- a/spec/javascripts/notes/components/note_attachment_spec.js +++ b/spec/frontend/notes/components/note_attachment_spec.js diff --git a/spec/javascripts/notes/components/note_edited_text_spec.js b/spec/frontend/notes/components/note_edited_text_spec.js index e4c8d954d50..e4c8d954d50 100644 --- a/spec/javascripts/notes/components/note_edited_text_spec.js +++ b/spec/frontend/notes/components/note_edited_text_spec.js diff --git a/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js index 23d07056925..1e0bc708c31 100644 --- a/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js +++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js @@ -3,7 +3,7 @@ import '~/lib/utils/text_utility'; import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports'; describe('Abuse Reports', () => { - const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; + const FIXTURE = 'abuse_reports/abuse_reports_list.html'; const MAX_MESSAGE_LENGTH = 500; let $messages; @@ -16,9 +16,9 @@ describe('Abuse Reports', () => { preloadFixtures(FIXTURE); - beforeEach(function() { + beforeEach(() => { loadFixtures(FIXTURE); - this.abuseReports = new AbuseReports(); + new AbuseReports(); // eslint-disable-line no-new $messages = $('.abuse-reports .message'); }); diff --git a/spec/javascripts/performance_bar/services/performance_bar_service_spec.js b/spec/frontend/performance_bar/services/performance_bar_service_spec.js index cfec4b779e4..cfec4b779e4 100644 --- a/spec/javascripts/performance_bar/services/performance_bar_service_spec.js +++ b/spec/frontend/performance_bar/services/performance_bar_service_spec.js diff --git a/spec/javascripts/pipelines/blank_state_spec.js b/spec/frontend/pipelines/blank_state_spec.js index 033bd5ccb73..033bd5ccb73 100644 --- a/spec/javascripts/pipelines/blank_state_spec.js +++ b/spec/frontend/pipelines/blank_state_spec.js diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js index f12950b8fce..f12950b8fce 100644 --- a/spec/javascripts/pipelines/empty_state_spec.js +++ b/spec/frontend/pipelines/empty_state_spec.js diff --git a/spec/javascripts/pipelines/pipeline_store_spec.js b/spec/frontend/pipelines/pipeline_store_spec.js index 1d5754d1f05..1d5754d1f05 100644 --- a/spec/javascripts/pipelines/pipeline_store_spec.js +++ b/spec/frontend/pipelines/pipeline_store_spec.js diff --git a/spec/javascripts/pipelines/pipelines_store_spec.js b/spec/frontend/pipelines/pipelines_store_spec.js index ce21f788ed5..ce21f788ed5 100644 --- a/spec/javascripts/pipelines/pipelines_store_spec.js +++ b/spec/frontend/pipelines/pipelines_store_spec.js diff --git a/spec/javascripts/registry/getters_spec.js b/spec/frontend/registry/getters_spec.js index 839aa718997..839aa718997 100644 --- a/spec/javascripts/registry/getters_spec.js +++ b/spec/frontend/registry/getters_spec.js diff --git a/spec/javascripts/reports/components/report_link_spec.js b/spec/frontend/reports/components/report_link_spec.js index f879899e9c5..f879899e9c5 100644 --- a/spec/javascripts/reports/components/report_link_spec.js +++ b/spec/frontend/reports/components/report_link_spec.js diff --git a/spec/javascripts/reports/store/utils_spec.js b/spec/frontend/reports/store/utils_spec.js index 1679d120db2..1679d120db2 100644 --- a/spec/javascripts/reports/store/utils_spec.js +++ b/spec/frontend/reports/store/utils_spec.js diff --git a/spec/javascripts/sidebar/confidential_edit_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_buttons_spec.js index 32da9f83112..32da9f83112 100644 --- a/spec/javascripts/sidebar/confidential_edit_buttons_spec.js +++ b/spec/frontend/sidebar/confidential_edit_buttons_spec.js diff --git a/spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js index 369088cb258..369088cb258 100644 --- a/spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js +++ b/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js diff --git a/spec/javascripts/sidebar/lock/edit_form_spec.js b/spec/frontend/sidebar/lock/edit_form_spec.js index ec10a999a40..ec10a999a40 100644 --- a/spec/javascripts/sidebar/lock/edit_form_spec.js +++ b/spec/frontend/sidebar/lock/edit_form_spec.js diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 8c36d8ff49f..1e3c28e25eb 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -2,6 +2,12 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; import axios from '~/lib/utils/axios_utils'; import { initializeTestTimeout } from './helpers/timeout'; +import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './helpers/fixtures'; + +// wait for pending setTimeout()s +afterEach(() => { + jest.runAllTimers(); +}); initializeTestTimeout(300); @@ -17,4 +23,24 @@ beforeEach(done => { done(); }); +Vue.config.devtools = false; +Vue.config.productionTip = false; + Vue.use(Translate); + +// workaround for JSDOM not supporting innerText +// see https://github.com/jsdom/jsdom/issues/1245 +Object.defineProperty(global.Element.prototype, 'innerText', { + get() { + return this.textContent; + }, + configurable: true, // make it so that it doesn't blow chunks on re-running tests with things like --watch +}); + +// convenience wrapper for migration from Karma +Object.assign(global, { + loadFixtures: loadHTMLFixture, + loadJSONFixtures: getJSONFixture, + preloadFixtures() {}, + setFixtures: setHTMLFixture, +}); diff --git a/spec/javascripts/u2f/util_spec.js b/spec/frontend/u2f/util_spec.js index 32cd6891384..32cd6891384 100644 --- a/spec/javascripts/u2f/util_spec.js +++ b/spec/frontend/u2f/util_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js index 16c8c939a6f..16c8c939a6f 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js index f7c2376eebf..f7c2376eebf 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js index 994d6255324..994d6255324 100644 --- a/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js index daf1cc2d98b..daf1cc2d98b 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js index 9ee2f88c78d..9ee2f88c78d 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js index b356ea85cad..b356ea85cad 100644 --- a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js +++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js diff --git a/spec/javascripts/vue_shared/components/callout_spec.js b/spec/frontend/vue_shared/components/callout_spec.js index 91208dfb31a..91208dfb31a 100644 --- a/spec/javascripts/vue_shared/components/callout_spec.js +++ b/spec/frontend/vue_shared/components/callout_spec.js diff --git a/spec/javascripts/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js index 6b91a20ff76..6b91a20ff76 100644 --- a/spec/javascripts/vue_shared/components/code_block_spec.js +++ b/spec/frontend/vue_shared/components/code_block_spec.js diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js index c4358f0d9cb..c4358f0d9cb 100644 --- a/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js diff --git a/spec/javascripts/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js index 0b3dbb61c96..0b3dbb61c96 100644 --- a/spec/javascripts/vue_shared/components/identicon_spec.js +++ b/spec/frontend/vue_shared/components/identicon_spec.js diff --git a/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js b/spec/frontend/vue_shared/components/lib/utils/dom_utils_spec.js index 2388660b0c2..2388660b0c2 100644 --- a/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js +++ b/spec/frontend/vue_shared/components/lib/utils/dom_utils_spec.js diff --git a/spec/javascripts/vue_shared/components/pagination_links_spec.js b/spec/frontend/vue_shared/components/pagination_links_spec.js index d0cb3731050..d0cb3731050 100644 --- a/spec/javascripts/vue_shared/components/pagination_links_spec.js +++ b/spec/frontend/vue_shared/components/pagination_links_spec.js diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js index 536bb57b946..536bb57b946 100644 --- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js +++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js diff --git a/spec/javascripts/vuex_shared/modules/modal/mutations_spec.js b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js index d07f8ba1e65..d07f8ba1e65 100644 --- a/spec/javascripts/vuex_shared/modules/modal/mutations_spec.js +++ b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb new file mode 100644 index 00000000000..a21162adb42 --- /dev/null +++ b/spec/graphql/types/ci/detailed_status_type_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Types::Ci::DetailedStatusType do + it { expect(described_class.graphql_name).to eq('DetailedStatus') } + + it "has all fields" do + expect(described_class).to have_graphql_fields(:group, :icon, :favicon, + :details_path, :has_details, + :label, :text, :tooltip) + end +end diff --git a/spec/helpers/appearances_helper_spec.rb b/spec/helpers/appearances_helper_spec.rb index 8d717b968dd..a3511e078ce 100644 --- a/spec/helpers/appearances_helper_spec.rb +++ b/spec/helpers/appearances_helper_spec.rb @@ -65,12 +65,10 @@ describe AppearancesHelper do end describe '#brand_title' do - it 'returns the default CE title when no appearance is present' do - allow(helper) - .to receive(:current_appearance) - .and_return(nil) + it 'returns the default title when no appearance is present' do + allow(helper).to receive(:current_appearance).and_return(nil) - expect(helper.brand_title).to eq('GitLab Community Edition') + expect(helper.brand_title).to eq(helper.default_brand_title) end end end diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index f0c2e4768ec..aae515def0c 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "spec_helper" describe AuthHelper do @@ -97,17 +99,37 @@ describe AuthHelper do end end - describe 'unlink_allowed?' do - [:saml, :cas3].each do |provider| - it "returns true if the provider is #{provider}" do - expect(helper.unlink_allowed?(provider)).to be false - end + describe '#link_provider_allowed?' do + let(:policy) { instance_double('IdentityProviderPolicy') } + let(:current_user) { instance_double('User') } + let(:provider) { double } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + allow(IdentityProviderPolicy).to receive(:new).with(current_user, provider).and_return(policy) end - [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq].each do |provider| - it "returns false if the provider is #{provider}" do - expect(helper.unlink_allowed?(provider)).to be true - end + it 'delegates to identity provider policy' do + allow(policy).to receive(:can?).with(:link).and_return('policy_link_result') + + expect(helper.link_provider_allowed?(provider)).to eq 'policy_link_result' + end + end + + describe '#unlink_provider_allowed?' do + let(:policy) { instance_double('IdentityProviderPolicy') } + let(:current_user) { instance_double('User') } + let(:provider) { double } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + allow(IdentityProviderPolicy).to receive(:new).with(current_user, provider).and_return(policy) + end + + it 'delegates to identity provider policy' do + allow(policy).to receive(:can?).with(:unlink).and_return('policy_unlink_result') + + expect(helper.unlink_provider_allowed?(provider)).to eq 'policy_unlink_result' end end end diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb new file mode 100644 index 00000000000..4ea0f76fc28 --- /dev/null +++ b/spec/helpers/clusters_helper_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ClustersHelper do + describe '#has_rbac_enabled?' do + context 'when kubernetes platform has been created' do + let(:platform_kubernetes) { build_stubbed(:cluster_platform_kubernetes) } + let(:cluster) { build_stubbed(:cluster, :provided_by_gcp, platform_kubernetes: platform_kubernetes) } + + it 'returns kubernetes platform value' do + expect(helper.has_rbac_enabled?(cluster)).to be_truthy + end + end + + context 'when kubernetes platform has not been created yet' do + let(:cluster) { build_stubbed(:cluster, :providing_by_gcp) } + + it 'delegates to cluster provider' do + expect(helper.has_rbac_enabled?(cluster)).to be_truthy + end + + context 'when ABAC cluster is created' do + let(:provider) { build_stubbed(:cluster_provider_gcp, :abac_enabled) } + let(:cluster) { build_stubbed(:cluster, :providing_by_gcp, provider_gcp: provider) } + + it 'delegates to cluster provider' do + expect(helper.has_rbac_enabled?(cluster)).to be_falsy + end + end + end + end +end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 540a8674ec2..91541a16c13 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe GroupsHelper do diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 8b82dea2524..1d1446eaa30 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe IssuablesHelper do @@ -176,7 +178,7 @@ describe IssuablesHelper do stub_commonmark_sourcepos_disabled end - it 'returns the correct json for an issue' do + it 'returns the correct data for an issue' do issue = create(:issue, author: user, description: 'issue text') @project = issue.project @@ -198,7 +200,7 @@ describe IssuablesHelper do initialDescriptionText: 'issue text', initialTaskStatus: '0 of 0 tasks completed' } - expect(helper.issuable_initial_data(issue)).to eq(expected_data) + expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data)) end end end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 885204062fe..193390d2f2c 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe MergeRequestsHelper do diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 291eafece94..37c63807c82 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe ProjectsHelper do diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js index 068b8eb65bc..23b6de7e4e0 100644 --- a/spec/javascripts/activities_spec.js +++ b/spec/javascripts/activities_spec.js @@ -7,7 +7,7 @@ import Pager from '~/pager'; describe('Activities', () => { window.gon || (window.gon = {}); - const fixtureTemplate = 'static/event_filter.html.raw'; + const fixtureTemplate = 'static/event_filter.html'; const filters = [ { id: 'all', diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js index 9389fc94f17..89195a4397f 100644 --- a/spec/javascripts/ajax_loading_spinner_spec.js +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import AjaxLoadingSpinner from '~/ajax_loading_spinner'; describe('Ajax Loading Spinner', () => { - const fixtureTemplate = 'static/ajax_loading_spinner.html.raw'; + const fixtureTemplate = 'static/ajax_loading_spinner.html'; preloadFixtures(fixtureTemplate); beforeEach(() => { diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index e5b5707dcef..e10df1b45e7 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -24,13 +24,13 @@ const lazyAssert = function(done, assertFn) { describe('AwardsHandler', function() { const emojiData = getJSONFixture('emojis/emojis.json'); - preloadFixtures('snippets/show.html.raw'); + preloadFixtures('snippets/show.html'); beforeEach(function(done) { mock = new MockAdapter(axios); mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData); - loadFixtures('snippets/show.html.raw'); + loadFixtures('snippets/show.html'); loadAwardsHandler(true) .then(obj => { awardsHandler = obj; diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js index 536671db377..2f72c9ed89d 100644 --- a/spec/javascripts/badges/components/badge_list_spec.js +++ b/spec/javascripts/badges/components/badge_list_spec.js @@ -60,7 +60,7 @@ describe('BadgeList component', () => { Vue.nextTick() .then(() => { - const loadingIcon = vm.$el.querySelector('.fa-spinner'); + const loadingIcon = vm.$el.querySelector('.spinner'); expect(loadingIcon).toBeVisible(); }) diff --git a/spec/javascripts/badges/components/badge_spec.js b/spec/javascripts/badges/components/badge_spec.js index 29805408bcf..4e4d1ae2e99 100644 --- a/spec/javascripts/badges/components/badge_spec.js +++ b/spec/javascripts/badges/components/badge_spec.js @@ -15,7 +15,7 @@ describe('Badge component', () => { const buttons = vm.$el.querySelectorAll('button'); return { badgeImage: vm.$el.querySelector('img.project-badge'), - loadingIcon: vm.$el.querySelector('.fa-spinner'), + loadingIcon: vm.$el.querySelector('.spinner'), reloadButton: buttons[buttons.length - 1], }; }; diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 681463aab66..7af8c984841 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -4,10 +4,10 @@ import '~/behaviors/quick_submit'; describe('Quick Submit behavior', function() { const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options); - preloadFixtures('snippets/show.html.raw'); + preloadFixtures('snippets/show.html'); beforeEach(() => { - loadFixtures('snippets/show.html.raw'); + loadFixtures('snippets/show.html'); $('form').submit(e => { // Prevent a form submit from moving us off the testing page e.preventDefault(); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 1bde2bb3024..617fe49b059 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -3,10 +3,10 @@ import '~/behaviors/requires_input'; describe('requiresInput', () => { let submitButton; - preloadFixtures('branches/new_branch.html.raw'); + preloadFixtures('branches/new_branch.html'); beforeEach(() => { - loadFixtures('branches/new_branch.html.raw'); + loadFixtures('branches/new_branch.html'); submitButton = $('button[type="submit"]'); }); diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js index 4843a0386b5..5e457a4e823 100644 --- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js +++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -9,7 +9,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form'; describe('ShortcutsIssuable', function() { - const fixtureName = 'snippets/show.html.raw'; + const fixtureName = 'snippets/show.html'; preloadFixtures(fixtureName); beforeAll(done => { diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js index 5f027f59fcf..68b4f261617 100644 --- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js +++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js @@ -6,10 +6,10 @@ describe('Balsamiq integration spec', () => { let endpoint; let balsamiqViewer; - preloadFixtures('static/balsamiq_viewer.html.raw'); + preloadFixtures('static/balsamiq_viewer.html'); beforeEach(() => { - loadFixtures('static/balsamiq_viewer.html.raw'); + loadFixtures('static/balsamiq_viewer.html'); container = document.getElementById('js-balsamiq-viewer'); balsamiqViewer = new BalsamiqViewer(container); diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js index 432d8a65b0a..cab06a0a9be 100644 --- a/spec/javascripts/blob/blob_file_dropzone_spec.js +++ b/spec/javascripts/blob/blob_file_dropzone_spec.js @@ -2,10 +2,10 @@ import $ from 'jquery'; import BlobFileDropzone from '~/blob/blob_file_dropzone'; describe('BlobFileDropzone', function() { - preloadFixtures('blob/show.html.raw'); + preloadFixtures('blob/show.html'); beforeEach(() => { - loadFixtures('blob/show.html.raw'); + loadFixtures('blob/show.html'); const form = $('.js-upload-blob-form'); this.blobFileDropzone = new BlobFileDropzone(form, 'POST'); this.dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone; diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js index 28d3b2f5ea3..6bb5bac007f 100644 --- a/spec/javascripts/blob/notebook/index_spec.js +++ b/spec/javascripts/blob/notebook/index_spec.js @@ -3,10 +3,10 @@ import axios from '~/lib/utils/axios_utils'; import renderNotebook from '~/blob/notebook'; describe('iPython notebook renderer', () => { - preloadFixtures('static/notebook_viewer.html.raw'); + preloadFixtures('static/notebook_viewer.html'); beforeEach(() => { - loadFixtures('static/notebook_viewer.html.raw'); + loadFixtures('static/notebook_viewer.html'); }); it('shows loading icon', () => { diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js index be917a0613f..acf87580777 100644 --- a/spec/javascripts/blob/pdf/index_spec.js +++ b/spec/javascripts/blob/pdf/index_spec.js @@ -15,10 +15,10 @@ describe('PDF renderer', () => { } }; - preloadFixtures('static/pdf_viewer.html.raw'); + preloadFixtures('static/pdf_viewer.html'); beforeEach(() => { - loadFixtures('static/pdf_viewer.html.raw'); + loadFixtures('static/pdf_viewer.html'); viewer = document.getElementById('js-pdf-viewer'); viewer.dataset.endpoint = testPDF; }); diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js index 2b1e81e9cbc..3d3129e10da 100644 --- a/spec/javascripts/blob/sketch/index_spec.js +++ b/spec/javascripts/blob/sketch/index_spec.js @@ -13,10 +13,10 @@ describe('Sketch viewer', () => { }); }; - preloadFixtures('static/sketch_viewer.html.raw'); + preloadFixtures('static/sketch_viewer.html'); beforeEach(() => { - loadFixtures('static/sketch_viewer.html.raw'); + loadFixtures('static/sketch_viewer.html'); }); describe('with error message', () => { diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js index 93a942fe8d4..4ac15ca5aa2 100644 --- a/spec/javascripts/blob/viewer/index_spec.js +++ b/spec/javascripts/blob/viewer/index_spec.js @@ -9,12 +9,12 @@ describe('Blob viewer', () => { let blob; let mock; - preloadFixtures('snippets/show.html.raw'); + preloadFixtures('snippets/show.html'); beforeEach(() => { mock = new MockAdapter(axios); - loadFixtures('snippets/show.html.raw'); + loadFixtures('snippets/show.html'); $('#modal-upload-blob').remove(); blob = new BlobViewer(); diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 2642c8b1bdb..396fc823ef5 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -195,7 +195,7 @@ describe('Board list component', () => { component.list.loadingMore = true; Vue.nextTick(() => { - expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count .spinner')).not.toBeNull(); done(); }); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js index dee7841c088..6e6b3e6950b 100644 --- a/spec/javascripts/boards/components/board_spec.js +++ b/spec/javascripts/boards/components/board_spec.js @@ -9,7 +9,7 @@ describe('Board component', () => { let el; beforeEach(done => { - loadFixtures('boards/show.html.raw'); + loadFixtures('boards/show.html'); el = document.createElement('div'); document.body.appendChild(el); diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/javascripts/bootstrap_linked_tabs_spec.js index c3e3d78ff63..1d21637ceae 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js @@ -1,10 +1,10 @@ import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; describe('Linked Tabs', () => { - preloadFixtures('static/linked_tabs.html.raw'); + preloadFixtures('static/linked_tabs.html'); beforeEach(() => { - loadFixtures('static/linked_tabs.html.raw'); + loadFixtures('static/linked_tabs.html'); }); describe('when is initialized', () => { diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js index 1fc0e206d5e..481b1a4d4b0 100644 --- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -7,8 +7,8 @@ const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-proje const HIDE_CLASS = 'hide'; describe('AjaxFormVariableList', () => { - preloadFixtures('projects/ci_cd_settings.html.raw'); - preloadFixtures('projects/ci_cd_settings_with_variables.html.raw'); + preloadFixtures('projects/ci_cd_settings.html'); + preloadFixtures('projects/ci_cd_settings_with_variables.html'); let container; let saveButton; @@ -18,7 +18,7 @@ describe('AjaxFormVariableList', () => { let ajaxVariableList; beforeEach(() => { - loadFixtures('projects/ci_cd_settings.html.raw'); + loadFixtures('projects/ci_cd_settings.html'); container = document.querySelector('.js-ci-variable-list-section'); mock = new MockAdapter(axios); @@ -168,7 +168,7 @@ describe('AjaxFormVariableList', () => { describe('updateRowsWithPersistedVariables', () => { beforeEach(() => { - loadFixtures('projects/ci_cd_settings_with_variables.html.raw'); + loadFixtures('projects/ci_cd_settings_with_variables.html'); container = document.querySelector('.js-ci-variable-list-section'); const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section'); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index bef59b86d0c..70f49469300 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -5,9 +5,9 @@ import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; const HIDE_CLASS = 'hide'; describe('VariableList', () => { - preloadFixtures('pipeline_schedules/edit.html.raw'); - preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); - preloadFixtures('projects/ci_cd_settings.html.raw'); + preloadFixtures('pipeline_schedules/edit.html'); + preloadFixtures('pipeline_schedules/edit_with_variables.html'); + preloadFixtures('projects/ci_cd_settings.html'); let $wrapper; let variableList; @@ -15,7 +15,7 @@ describe('VariableList', () => { describe('with only key/value inputs', () => { describe('with no variables', () => { beforeEach(() => { - loadFixtures('pipeline_schedules/edit.html.raw'); + loadFixtures('pipeline_schedules/edit.html'); $wrapper = $('.js-ci-variable-list-section'); variableList = new VariableList({ @@ -82,7 +82,7 @@ describe('VariableList', () => { describe('with persisted variables', () => { beforeEach(() => { - loadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + loadFixtures('pipeline_schedules/edit_with_variables.html'); $wrapper = $('.js-ci-variable-list-section'); variableList = new VariableList({ @@ -115,7 +115,7 @@ describe('VariableList', () => { describe('with all inputs(key, value, protected)', () => { beforeEach(() => { - loadFixtures('projects/ci_cd_settings.html.raw'); + loadFixtures('projects/ci_cd_settings.html'); $wrapper = $('.js-ci-variable-list-section'); $wrapper.find('.js-ci-variable-input-protected').attr('data-default', 'false'); @@ -149,7 +149,7 @@ describe('VariableList', () => { describe('toggleEnableRow method', () => { beforeEach(() => { - loadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + loadFixtures('pipeline_schedules/edit_with_variables.html'); $wrapper = $('.js-ci-variable-list-section'); variableList = new VariableList({ @@ -198,7 +198,7 @@ describe('VariableList', () => { describe('hideValues', () => { beforeEach(() => { - loadFixtures('projects/ci_cd_settings.html.raw'); + loadFixtures('projects/ci_cd_settings.html'); $wrapper = $('.js-ci-variable-list-section'); variableList = new VariableList({ diff --git a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js index 997d0d54d79..4982b68fa81 100644 --- a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js @@ -2,12 +2,12 @@ import $ from 'jquery'; import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list'; describe('NativeFormVariableList', () => { - preloadFixtures('pipeline_schedules/edit.html.raw'); + preloadFixtures('pipeline_schedules/edit.html'); let $wrapper; beforeEach(() => { - loadFixtures('pipeline_schedules/edit.html.raw'); + loadFixtures('pipeline_schedules/edit.html'); $wrapper = $('.js-ci-variable-list-section'); setupNativeFormVariableList({ diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js index 7928feeadfa..0d3dcc29f22 100644 --- a/spec/javascripts/clusters/clusters_bundle_spec.js +++ b/spec/javascripts/clusters/clusters_bundle_spec.js @@ -1,13 +1,18 @@ import Clusters from '~/clusters/clusters_bundle'; -import { REQUEST_SUBMITTED, REQUEST_FAILURE, APPLICATION_STATUS } from '~/clusters/constants'; +import { + REQUEST_SUBMITTED, + REQUEST_FAILURE, + APPLICATION_STATUS, + INGRESS_DOMAIN_SUFFIX, +} from '~/clusters/constants'; import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; describe('Clusters', () => { let cluster; - preloadFixtures('clusters/show_cluster.html.raw'); + preloadFixtures('clusters/show_cluster.html'); beforeEach(() => { - loadFixtures('clusters/show_cluster.html.raw'); + loadFixtures('clusters/show_cluster.html'); cluster = new Clusters(); }); @@ -265,4 +270,77 @@ describe('Clusters', () => { .catch(done.fail); }); }); + + describe('handleSuccess', () => { + beforeEach(() => { + spyOn(cluster.store, 'updateStateFromServer'); + spyOn(cluster, 'toggleIngressDomainHelpText'); + spyOn(cluster, 'checkForNewInstalls'); + spyOn(cluster, 'updateContainer'); + + cluster.handleSuccess({ data: {} }); + }); + + it('updates clusters store', () => { + expect(cluster.store.updateStateFromServer).toHaveBeenCalled(); + }); + + it('checks for new installable apps', () => { + expect(cluster.checkForNewInstalls).toHaveBeenCalled(); + }); + + it('toggles ingress domain help text', () => { + expect(cluster.toggleIngressDomainHelpText).toHaveBeenCalled(); + }); + + it('updates message containers', () => { + expect(cluster.updateContainer).toHaveBeenCalled(); + }); + }); + + describe('toggleIngressDomainHelpText', () => { + const { INSTALLED, INSTALLABLE, NOT_INSTALLABLE } = APPLICATION_STATUS; + + const ingressPreviousState = { status: INSTALLABLE }; + const ingressNewState = { status: INSTALLED, externalIp: '127.0.0.1' }; + + describe(`when ingress application new status is ${INSTALLED}`, () => { + beforeEach(() => { + ingressNewState.status = INSTALLED; + cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); + }); + + it('displays custom domain help text', () => { + expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(false); + }); + + it('updates ingress external ip address', () => { + expect(cluster.ingressDomainSnippet.textContent).toEqual( + `${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`, + ); + }); + }); + + describe(`when ingress application new status is different from ${INSTALLED}`, () => { + it('hides custom domain help text', () => { + ingressNewState.status = NOT_INSTALLABLE; + cluster.ingressDomainHelpText.classList.remove('hide'); + + cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); + + expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); + }); + }); + + describe('when ingress application new status and old status are the same', () => { + it('does not modify custom domain help text', () => { + ingressPreviousState.status = INSTALLED; + ingressNewState.status = ingressPreviousState.status; + + cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState); + + expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true); + }); + }); + }); }); diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js index 790e4b9602c..0f8153ad493 100644 --- a/spec/javascripts/clusters/components/applications_spec.js +++ b/spec/javascripts/clusters/components/applications_spec.js @@ -79,7 +79,7 @@ describe('Applications', () => { }); it('renders a row for GitLab Runner', () => { - expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeNull(); + expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull(); }); it('renders a row for Jupyter', () => { diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js index dc5737558c0..bb90e53e525 100644 --- a/spec/javascripts/collapsed_sidebar_todo_spec.js +++ b/spec/javascripts/collapsed_sidebar_todo_spec.js @@ -6,7 +6,7 @@ import Sidebar from '~/right_sidebar'; import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Issuable right sidebar collapsed todo toggle', () => { - const fixtureName = 'issues/open-issue.html.raw'; + const fixtureName = 'issues/open-issue.html'; const jsonFixtureName = 'todos/todos.json'; let mock; diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/javascripts/create_item_dropdown_spec.js index 9cf72d7c55b..a814952faab 100644 --- a/spec/javascripts/create_item_dropdown_spec.js +++ b/spec/javascripts/create_item_dropdown_spec.js @@ -20,7 +20,7 @@ const DROPDOWN_ITEM_DATA = [ ]; describe('CreateItemDropdown', () => { - preloadFixtures('static/create_item_dropdown.html.raw'); + preloadFixtures('static/create_item_dropdown.html'); let $wrapperEl; let createItemDropdown; @@ -44,7 +44,7 @@ describe('CreateItemDropdown', () => { } beforeEach(() => { - loadFixtures('static/create_item_dropdown.html.raw'); + loadFixtures('static/create_item_dropdown.html'); $wrapperEl = $('.js-create-item-dropdown-fixture-root'); }); diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index d81c433cca6..8d7c52a2876 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -397,4 +397,61 @@ describe('diffs/components/app', () => { expect(wrapper.find(TreeList).exists()).toBe(true); }); }); + + describe('hideTreeListIfJustOneFile', () => { + let toggleShowTreeList; + + beforeEach(() => { + toggleShowTreeList = jasmine.createSpy('toggleShowTreeList'); + }); + + afterEach(() => { + localStorage.removeItem('mr_tree_show'); + }); + + it('calls toggleShowTreeList when only 1 file', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).toHaveBeenCalledWith(false); + }); + + it('does not call toggleShowTreeList when more than 1 file', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + state.diffs.diffFiles.push({ sha: '124' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).not.toHaveBeenCalled(); + }); + + it('does not call toggleShowTreeList when localStorage is set', () => { + localStorage.setItem('mr_tree_show', 'true'); + + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).not.toHaveBeenCalled(); + }); + }); }); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 66c5b17b825..e10193c25b7 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -23,9 +23,6 @@ describe('diff_file_header', () => { }); beforeEach(() => { - gon.features = { - expandDiffFullFile: true, - }; const diffFile = diffDiscussionMock.diff_file; diffFile.added_lines = 2; diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index fd5dd611383..711ab543411 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -496,7 +496,7 @@ export default { { text: 'line', rich_text: - '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n', + '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n', can_receive_suggestion: true, line_code: '6f209374f7e565f771b95720abf46024c41d1885_1_1', type: 'new', diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 6c637097893..bca99caa920 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -734,6 +734,14 @@ describe('DiffsStoreActions', () => { expect(localStorage.setItem).toHaveBeenCalledWith('mr_tree_show', true); }); + + it('does not update localStorage', () => { + spyOn(localStorage, 'setItem'); + + toggleShowTreeList({ commit() {}, state: { showTreeList: true } }, false); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); }); describe('renderFileForDiscussionId', () => { diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js index 8b877994515..388d7063d13 100644 --- a/spec/javascripts/environments/environment_item_spec.js +++ b/spec/javascripts/environments/environment_item_spec.js @@ -60,7 +60,7 @@ describe('Environment item', () => { sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', ref: { name: 'master', - ref_path: 'root/ci-folders/tree/master', + ref_url: 'root/ci-folders/tree/master', }, tag: true, 'last?': true, diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index e8fcc8592eb..f764800fff0 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -72,7 +72,7 @@ describe('Dropdown User', () => { }); describe('hideCurrentUser', () => { - const fixtureTemplate = 'issues/issue_list.html.raw'; + const fixtureTemplate = 'issues/issue_list.html'; preloadFixtures(fixtureTemplate); let dropdown; diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index cfd0b96ec43..62d1bd69635 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -4,7 +4,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Dropdown Utils', () => { - const issueListFixture = 'issues/issue_list.html.raw'; + const issueListFixture = 'issues/issue_list.html'; preloadFixtures(issueListFixture); describe('getEscapedText', () => { diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js index f52dc26a7bb..14217d460cc 100644 --- a/spec/javascripts/filtered_search/visual_token_value_spec.js +++ b/spec/javascripts/filtered_search/visual_token_value_spec.js @@ -317,7 +317,18 @@ describe('Filtered Search Visual Tokens', () => { it('does not update user token appearance for `none` filter', () => { const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); - subject.tokenType = 'none'; + subject.tokenValue = 'none'; + + const { updateUserTokenAppearanceSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + + it('does not update user token appearance for `None` filter', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + + subject.tokenValue = 'None'; const { updateUserTokenAppearanceSpy } = setupSpies(subject); subject.render(tokenValueContainer, tokenValueElement); @@ -328,7 +339,7 @@ describe('Filtered Search Visual Tokens', () => { it('does not update user token appearance for `any` filter', () => { const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); - subject.tokenType = 'any'; + subject.tokenValue = 'any'; const { updateUserTokenAppearanceSpy } = setupSpies(subject); subject.render(tokenValueContainer, tokenValueElement); @@ -336,10 +347,21 @@ describe('Filtered Search Visual Tokens', () => { expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); }); + it('does not update label token color for `None` filter', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); + + subject.tokenValue = 'None'; + + const { updateLabelTokenColorSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + }); + it('does not update label token color for `none` filter', () => { const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); - subject.tokenType = 'none'; + subject.tokenValue = 'none'; const { updateLabelTokenColorSpy } = setupSpies(subject); subject.render(tokenValueContainer, tokenValueElement); @@ -350,7 +372,7 @@ describe('Filtered Search Visual Tokens', () => { it('does not update label token color for `any` filter', () => { const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); - subject.tokenType = 'any'; + subject.tokenValue = 'any'; const { updateLabelTokenColorSpy } = setupSpies(subject); subject.render(tokenValueContainer, tokenValueElement); diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore index 0c35cdd778e..2507c8e7263 100644 --- a/spec/javascripts/fixtures/.gitignore +++ b/spec/javascripts/fixtures/.gitignore @@ -1,2 +1,3 @@ *.html.raw +*.html *.json diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb index 387858cba77..54b6419bcdb 100644 --- a/spec/javascripts/fixtures/abuse_reports.rb +++ b/spec/javascripts/fixtures/abuse_reports.rb @@ -18,7 +18,7 @@ describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controll sign_in(admin) end - it 'abuse_reports/abuse_reports_list.html.raw' do |example| + it 'abuse_reports/abuse_reports_list.html' do |example| get :index expect(response).to be_success diff --git a/spec/javascripts/fixtures/admin_users.rb b/spec/javascripts/fixtures/admin_users.rb index 9989ac4fff2..76dbdf603da 100644 --- a/spec/javascripts/fixtures/admin_users.rb +++ b/spec/javascripts/fixtures/admin_users.rb @@ -17,7 +17,7 @@ describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do clean_frontend_fixtures('admin/users') end - it 'admin/users/new_with_internal_user_regex.html.raw' do |example| + it 'admin/users/new_with_internal_user_regex.html' do |example| stub_application_setting(user_default_external: true) stub_application_setting(user_default_internal_regex: '^(?:(?!\.ext@).)*$\r?') diff --git a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml deleted file mode 100644 index 09d8c9df3b2..00000000000 --- a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%a.js-ajax-loading-spinner{href: "http://goesnowhere.nothing/whereami", data: {remote: true}} - %i.fa.fa-trash-o diff --git a/spec/javascripts/fixtures/application_settings.rb b/spec/javascripts/fixtures/application_settings.rb index a9d3043f73d..c535e598e12 100644 --- a/spec/javascripts/fixtures/application_settings.rb +++ b/spec/javascripts/fixtures/application_settings.rb @@ -23,7 +23,7 @@ describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :c remove_repository(project) end - it 'application_settings/accounts_and_limit.html.raw' do |example| + it 'application_settings/accounts_and_limit.html' do |example| stub_application_setting(user_default_external: false) get :show diff --git a/spec/javascripts/fixtures/autocomplete_sources.rb b/spec/javascripts/fixtures/autocomplete_sources.rb new file mode 100644 index 00000000000..c117fb7cd24 --- /dev/null +++ b/spec/javascripts/fixtures/autocomplete_sources.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + set(:admin) { create(:admin) } + set(:group) { create(:group, name: 'frontend-fixtures') } + set(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') } + set(:issue) { create(:issue, project: project) } + + before(:all) do + clean_frontend_fixtures('autocomplete_sources/') + end + + before do + sign_in(admin) + end + + it 'autocomplete_sources/labels.json' do |example| + issue.labels << create(:label, project: project, title: 'bug') + issue.labels << create(:label, project: project, title: 'critical') + + create(:label, project: project, title: 'feature') + create(:label, project: project, title: 'documentation') + + get :labels, + format: :json, + params: { + namespace_id: group.path, + project_id: project.path, + type: issue.class.name, + type_id: issue.id + } + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/balsamiq_viewer.html.haml b/spec/javascripts/fixtures/balsamiq_viewer.html.haml deleted file mode 100644 index 18166ba4901..00000000000 --- a/spec/javascripts/fixtures/balsamiq_viewer.html.haml +++ /dev/null @@ -1 +0,0 @@ -.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb index cd66d98f92a..db7749bc000 100644 --- a/spec/javascripts/fixtures/blob.rb +++ b/spec/javascripts/fixtures/blob.rb @@ -22,7 +22,7 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'blob/show.html.raw' do |example| + it 'blob/show.html' do |example| get(:show, params: { namespace_id: project.namespace, project_id: project, diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb index 1d675e008ba..c4390e89578 100644 --- a/spec/javascripts/fixtures/boards.rb +++ b/spec/javascripts/fixtures/boards.rb @@ -17,7 +17,7 @@ describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller sign_in(admin) end - it 'boards/show.html.raw' do |example| + it 'boards/show.html' do |example| get(:index, params: { namespace_id: project.namespace, project_id: project diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb index 3cc713ef90f..5d2d6c7ec0e 100644 --- a/spec/javascripts/fixtures/branches.rb +++ b/spec/javascripts/fixtures/branches.rb @@ -21,7 +21,7 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle remove_repository(project) end - it 'branches/new_branch.html.raw' do |example| + it 'branches/new_branch.html' do |example| get :new, params: { namespace_id: project.namespace.to_param, project_id: project diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb index 69dbe54ffc2..8ebd8a41366 100644 --- a/spec/javascripts/fixtures/clusters.rb +++ b/spec/javascripts/fixtures/clusters.rb @@ -22,7 +22,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle remove_repository(project) end - it 'clusters/show_cluster.html.raw' do |example| + it 'clusters/show_cluster.html' do |example| get :show, params: { namespace_id: project.namespace.to_param, project_id: project, diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb index 295f13b34a4..ab10f559e4b 100644 --- a/spec/javascripts/fixtures/commit.rb +++ b/spec/javascripts/fixtures/commit.rb @@ -19,7 +19,7 @@ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon') end - it 'commit/show.html.raw' do |example| + it 'commit/show.html' do |example| params = { namespace_id: project.namespace, project_id: project, diff --git a/spec/javascripts/fixtures/create_item_dropdown.html.haml b/spec/javascripts/fixtures/create_item_dropdown.html.haml deleted file mode 100644 index d4d91b93caf..00000000000 --- a/spec/javascripts/fixtures/create_item_dropdown.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -.js-create-item-dropdown-fixture-root - %input{ name: 'variable[environment]', type: 'hidden' } - = dropdown_tag('some label', - options: { toggle_class: 'js-dropdown-menu-toggle', - content_class: 'js-dropdown-content', - filter: true, - dropdown_class: "dropdown-menu-selectable", - footer_content: true }) do - %ul.dropdown-footer-list - %li - %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item" } - Create wildcard - %code diff --git a/spec/javascripts/fixtures/emojis.rb b/spec/javascripts/fixtures/emojis.rb deleted file mode 100644 index 4dab697e5e2..00000000000 --- a/spec/javascripts/fixtures/emojis.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe 'Emojis (JavaScript fixtures)' do - include JavaScriptFixturesHelpers - - before(:all) do - clean_frontend_fixtures('emojis/') - end - - it 'emojis/emojis.json' do |example| - JavaScriptFixturesHelpers::FIXTURE_PATHS.each do |fixture_path| - next unless File.directory?(fixture_path) - - # Copying the emojis.json from the public folder - fixture_file_name = File.expand_path('emojis/emojis.json', fixture_path) - FileUtils.mkdir_p(File.dirname(fixture_file_name)) - FileUtils.cp(Rails.root.join('public/-/emojis/1/emojis.json'), fixture_file_name) - end - end -end diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml deleted file mode 100644 index aa7af61c7eb..00000000000 --- a/spec/javascripts/fixtures/event_filter.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs - %li.active - %a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"} - %span - All - %li - %a.event-filter-link{ id: "push_event_filter", title: "Filter by push events", href: "/dashboard/activity"} - %span - Push events - %li - %a.event-filter-link{ id: "merged_event_filter", title: "Filter by merge events", href: "/dashboard/activity"} - %span - Merge events - %li - %a.event-filter-link{ id: "issue_event_filter", title: "Filter by issue events", href: "/dashboard/activity"} - %span - Issue events - %li - %a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"} - %span - Comments - %li - %a.event-filter-link{ id: "team_event_filter", title: "Filter by team", href: "/dashboard/activity"} - %span - Team
\ No newline at end of file diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml deleted file mode 100644 index 43d57c2c4dc..00000000000 --- a/spec/javascripts/fixtures/gl_dropdown.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%div - .dropdown.inline - %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} - .dropdown-toggle-text - Projects - %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle - .dropdown-menu.dropdown-select.dropdown-menu-selectable - .dropdown-title - %span Go to project - %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}} - %i.fa.fa-times.dropdown-menu-close-icon - .dropdown-input - %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'} - %i.fa.fa-search.dropdown-input-search - .dropdown-content - .dropdown-loading - %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/gl_field_errors.html.haml b/spec/javascripts/fixtures/gl_field_errors.html.haml deleted file mode 100644 index 69445b61367..00000000000 --- a/spec/javascripts/fixtures/gl_field_errors.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -%form.gl-show-field-errors{action: 'submit', method: 'post'} - .form-group - %input.required-text{required: true, type: 'text'} Text - .form-group - %input.email{type: 'email', title: 'Please provide a valid email address.', required: true } Email - .form-group - %input.password{type: 'password', required: true} Password - .form-group - %input.alphanumeric{type: 'text', pattern: '[a-zA-Z0-9]', required: true} Alphanumeric - .form-group - %input.hidden{ type:'hidden' } - .form-group - %input.custom.gl-field-error-ignore{ type:'text' } Custom, do not validate - .form-group - %input.submit{type: 'submit'} Submit diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb index 03136f4e661..16e31028b05 100644 --- a/spec/javascripts/fixtures/groups.rb +++ b/spec/javascripts/fixtures/groups.rb @@ -18,7 +18,7 @@ describe 'Groups (JavaScript fixtures)', type: :controller do end describe GroupsController, '(JavaScript fixtures)', type: :controller do - it 'groups/edit.html.raw' do |example| + it 'groups/edit.html' do |example| get :edit, params: { id: group } expect(response).to be_success @@ -27,7 +27,7 @@ describe 'Groups (JavaScript fixtures)', type: :controller do end describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do - it 'groups/ci_cd_settings.html.raw' do |example| + it 'groups/ci_cd_settings.html' do |example| get :show, params: { group_id: group } expect(response).to be_success diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml deleted file mode 100644 index 84fa5395cb8..00000000000 --- a/spec/javascripts/fixtures/issuable_filter.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'} - %input{id: 'utf8', name: 'utf8', value: '✓'} - %input{id: 'check-all-issues', name: 'check-all-issues'} - %input{id: 'search', name: 'search'} - %input{id: 'author_id', name: 'author_id'} - %input{id: 'assignee_id', name: 'assignee_id'} - %input{id: 'milestone_title', name: 'milestone_title'} - %input{id: 'label_name', name: 'label_name'} diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml deleted file mode 100644 index 06ce248dc9c..00000000000 --- a/spec/javascripts/fixtures/issue_sidebar_label.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.block.labels - .sidebar-collapsed-icon.js-sidebar-labels-tooltip - .title.hide-collapsed - %a.edit-link.float-right{ href: "#" } - Edit - .selectbox.hide-collapsed{ style: "display: none;" } - .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } } - %span.dropdown-toggle-text - Label - %i.fa.fa-chevron-down - .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - .dropdown-page-one - .dropdown-content - .dropdown-loading - %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb index 9b8e90c2a43..645b3aa788a 100644 --- a/spec/javascripts/fixtures/issues.rb +++ b/spec/javascripts/fixtures/issues.rb @@ -21,26 +21,26 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller remove_repository(project) end - it 'issues/open-issue.html.raw' do |example| + it 'issues/open-issue.html' do |example| render_issue(example.description, create(:issue, project: project)) end - it 'issues/closed-issue.html.raw' do |example| + it 'issues/closed-issue.html' do |example| render_issue(example.description, create(:closed_issue, project: project)) end - it 'issues/issue-with-task-list.html.raw' do |example| + it 'issues/issue-with-task-list.html' do |example| issue = create(:issue, project: project, description: '- [ ] Task List Item') render_issue(example.description, issue) end - it 'issues/issue_with_comment.html.raw' do |example| + it 'issues/issue_with_comment.html' do |example| issue = create(:issue, project: project) create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save render_issue(example.description, issue) end - it 'issues/issue_list.html.raw' do |example| + it 'issues/issue_list.html' do |example| create(:issue, project: project) get :index, params: { diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb index 433bb690a1c..941235190b5 100644 --- a/spec/javascripts/fixtures/jobs.rb +++ b/spec/javascripts/fixtures/jobs.rb @@ -32,7 +32,7 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'builds/build-with-artifacts.html.raw' do |example| + it 'builds/build-with-artifacts.html' do |example| get :show, params: { namespace_id: project.namespace.to_param, project_id: project, diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml deleted file mode 100644 index 2782c50e298..00000000000 --- a/spec/javascripts/fixtures/line_highlighter.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -.file-holder - .file-content - .line-numbers - - 1.upto(25) do |i| - %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} - %i.fa.fa-link - = i - %pre.code.highlight - %code - - 1.upto(25) do |i| - %span.line{id: "LC#{i}"}= "Line #{i}" diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml deleted file mode 100644 index 632606e0536..00000000000 --- a/spec/javascripts/fixtures/linked_tabs.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%ul.nav.nav-tabs.new-session-tabs.linked-tabs - %li.nav-item - %a.nav-link{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } } - Tab 1 - %li.nav-item - %a.nav-link{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } } - Tab 2 - -.tab-content - #tab1.tab-pane - Tab 1 Content - #tab2.tab-pane - Tab 2 Content diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb index eb37be87e1d..7df1e5cb512 100644 --- a/spec/javascripts/fixtures/merge_requests.rb +++ b/spec/javascripts/fixtures/merge_requests.rb @@ -42,19 +42,19 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont remove_repository(project) end - it 'merge_requests/merge_request_of_current_user.html.raw' do |example| + it 'merge_requests/merge_request_of_current_user.html' do |example| merge_request.update(author: admin) render_merge_request(example.description, merge_request) end - it 'merge_requests/merge_request_with_task_list.html.raw' do |example| + it 'merge_requests/merge_request_with_task_list.html' do |example| create(:ci_build, :pending, pipeline: pipeline) render_merge_request(example.description, merge_request) end - it 'merge_requests/merged_merge_request.html.raw' do |example| + it 'merge_requests/merged_merge_request.html' do |example| expect_next_instance_of(MergeRequest) do |merge_request| allow(merge_request).to receive(:source_branch_exists?).and_return(true) allow(merge_request).to receive(:can_remove_source_branch?).and_return(true) @@ -62,13 +62,13 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont render_merge_request(example.description, merged_merge_request) end - it 'merge_requests/diff_comment.html.raw' do |example| + it 'merge_requests/diff_comment.html' do |example| create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request) create(:note_on_merge_request, author: admin, project: project, noteable: merge_request) render_merge_request(example.description, merge_request) end - it 'merge_requests/merge_request_with_comment.html.raw' do |example| + it 'merge_requests/merge_request_with_comment.html' do |example| create(:note_on_merge_request, author: admin, project: project, noteable: merge_request, note: '- [ ] Task List Item') render_merge_request(example.description, merge_request) end diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml deleted file mode 100644 index 8447dfdda32..00000000000 --- a/spec/javascripts/fixtures/merge_requests_show.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%a.btn-close - -.detail-page-description - .description.js-task-list-container - .wiki - %ul.task-list - %li.task-list-item - %input.task-list-item-checkbox{type: 'checkbox'} - Task List Item - %textarea.js-task-list-field - \- [ ] Task List Item - -%form.js-issuable-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml deleted file mode 100644 index 74584993739..00000000000 --- a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%div.js-builds-dropdown-tests.dropdown.dropdown.js-mini-pipeline-graph - %button.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar', data: { toggle: 'dropdown'} } - Dropdown - - %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container - %li.js-builds-dropdown-list.scrollable-menu - %ul - - %li.js-builds-dropdown-loading.hidden - %span.fa.fa-spinner diff --git a/spec/javascripts/fixtures/notebook_viewer.html.haml b/spec/javascripts/fixtures/notebook_viewer.html.haml deleted file mode 100644 index 17a7a9d8f31..00000000000 --- a/spec/javascripts/fixtures/notebook_viewer.html.haml +++ /dev/null @@ -1 +0,0 @@ -.file-content#js-notebook-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/fixtures/oauth_remember_me.html.haml b/spec/javascripts/fixtures/oauth_remember_me.html.haml deleted file mode 100644 index a5d7c4e816a..00000000000 --- a/spec/javascripts/fixtures/oauth_remember_me.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -#oauth-container - %input#remember_me{ type: "checkbox" } - - %a.oauth-login.twitter{ href: "http://example.com/" } - %a.oauth-login.github{ href: "http://example.com/" } - %a.oauth-login.facebook{ href: "http://example.com/?redirect_fragment=L1" } diff --git a/spec/javascripts/fixtures/pdf_viewer.html.haml b/spec/javascripts/fixtures/pdf_viewer.html.haml deleted file mode 100644 index 2e57beae54b..00000000000 --- a/spec/javascripts/fixtures/pdf_viewer.html.haml +++ /dev/null @@ -1 +0,0 @@ -.file-content#js-pdf-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml deleted file mode 100644 index c0b5ab4411e..00000000000 --- a/spec/javascripts/fixtures/pipeline_graph.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%div.pipeline-visualization.js-pipeline-graph - %ul.stage-column-list - %li.stage-column - .stage-name - %a{:href => "/"} - Test - .builds-container - %ul - %li.build - .curve - %a - %svg - .ci-status-text - stop_review diff --git a/spec/javascripts/fixtures/pipeline_schedules.rb b/spec/javascripts/fixtures/pipeline_schedules.rb index 05d79ec8de9..e5176a58273 100644 --- a/spec/javascripts/fixtures/pipeline_schedules.rb +++ b/spec/javascripts/fixtures/pipeline_schedules.rb @@ -21,7 +21,7 @@ describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: : sign_in(admin) end - it 'pipeline_schedules/edit.html.raw' do |example| + it 'pipeline_schedules/edit.html' do |example| get :edit, params: { namespace_id: project.namespace.to_param, project_id: project, @@ -32,7 +32,7 @@ describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: : store_frontend_fixture(response, example.description) end - it 'pipeline_schedules/edit_with_variables.html.raw' do |example| + it 'pipeline_schedules/edit_with_variables.html' do |example| get :edit, params: { namespace_id: project.namespace.to_param, project_id: project, diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml deleted file mode 100644 index 0161c0550d1..00000000000 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%div - #pipelines-list-vue{ data: { endpoint: 'foo', - "help-page-path" => 'foo', - "help-auto-devops-path" => 'foo', - "empty-state-svg-path" => 'foo', - "error-state-svg-path" => 'foo', - "new-pipeline-path" => 'foo', - "can-create-pipeline" => 'true', - "has-ci" => 'foo', - "ci-lint-path" => 'foo', - "reset-cache-path" => 'foo' } } - diff --git a/spec/javascripts/fixtures/project_select_combo_button.html.haml b/spec/javascripts/fixtures/project_select_combo_button.html.haml deleted file mode 100644 index 432cd5fcc74..00000000000 --- a/spec/javascripts/fixtures/project_select_combo_button.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.project-item-select-holder - %input.project-item-select{ data: { group_id: '12345' , relative_path: 'issues/new' } } - %a.new-project-item-link{ data: { label: 'New issue', type: 'issues' }, href: ''} - %i.fa.fa-spinner.spin - %a.new-project-item-select-button - %i.fa.fa-caret-down diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 85f02923804..446da83a7f9 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -28,7 +28,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do end describe ProjectsController, '(JavaScript fixtures)', type: :controller do - it 'projects/dashboard.html.raw' do |example| + it 'projects/dashboard.html' do |example| get :show, params: { namespace_id: project.namespace.to_param, id: project @@ -38,7 +38,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do store_frontend_fixture(response, example.description) end - it 'projects/overview.html.raw' do |example| + it 'projects/overview.html' do |example| get :show, params: { namespace_id: project_with_repo.namespace.to_param, id: project_with_repo @@ -48,7 +48,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do store_frontend_fixture(response, example.description) end - it 'projects/edit.html.raw' do |example| + it 'projects/edit.html' do |example| get :edit, params: { namespace_id: project.namespace.to_param, id: project @@ -60,7 +60,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do end describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do - it 'projects/ci_cd_settings.html.raw' do |example| + it 'projects/ci_cd_settings.html' do |example| get :show, params: { namespace_id: project.namespace.to_param, project_id: project @@ -70,7 +70,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do store_frontend_fixture(response, example.description) end - it 'projects/ci_cd_settings_with_variables.html.raw' do |example| + it 'projects/ci_cd_settings_with_variables.html' do |example| create(:ci_variable, project: project_variable_populated) create(:ci_variable, project: project_variable_populated) diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb index 746fbfd66dd..29dc95305b7 100644 --- a/spec/javascripts/fixtures/prometheus_service.rb +++ b/spec/javascripts/fixtures/prometheus_service.rb @@ -22,7 +22,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle remove_repository(project) end - it 'services/prometheus/prometheus_service.html.raw' do |example| + it 'services/prometheus/prometheus_service.html' do |example| get :edit, params: { namespace_id: namespace, project_id: project, diff --git a/spec/javascripts/fixtures/search.rb b/spec/javascripts/fixtures/search.rb index 703cd3d49fa..5f5b4d4e60d 100644 --- a/spec/javascripts/fixtures/search.rb +++ b/spec/javascripts/fixtures/search.rb @@ -9,7 +9,7 @@ describe SearchController, '(JavaScript fixtures)', type: :controller do clean_frontend_fixtures('search/') end - it 'search/show.html.raw' do |example| + it 'search/show.html' do |example| get :show expect(response).to be_success diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml deleted file mode 100644 index 4aa54da9411..00000000000 --- a/spec/javascripts/fixtures/search_autocomplete.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.search.search-form - %form.form-inline - .search-input-container - .search-input-wrap - .dropdown - %input#search.search-input.dropdown-menu-toggle - .dropdown-menu.dropdown-select - .dropdown-content - %input{ type: "hidden", class: "js-search-project-options" } diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb index 6ccd74a07ff..dc7ee484c22 100644 --- a/spec/javascripts/fixtures/services.rb +++ b/spec/javascripts/fixtures/services.rb @@ -22,7 +22,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle remove_repository(project) end - it 'services/edit_service.html.raw' do |example| + it 'services/edit_service.html' do |example| get :edit, params: { namespace_id: namespace, project_id: project, diff --git a/spec/javascripts/fixtures/sessions.rb b/spec/javascripts/fixtures/sessions.rb index e90a58e8c54..8656dea696a 100644 --- a/spec/javascripts/fixtures/sessions.rb +++ b/spec/javascripts/fixtures/sessions.rb @@ -16,7 +16,7 @@ describe 'Sessions (JavaScript fixtures)' do set_devise_mapping(context: @request) end - it 'sessions/new.html.raw' do |example| + it 'sessions/new.html' do |example| get :new expect(response).to be_success diff --git a/spec/javascripts/fixtures/signin_tabs.html.haml b/spec/javascripts/fixtures/signin_tabs.html.haml deleted file mode 100644 index 2e00fe7865e..00000000000 --- a/spec/javascripts/fixtures/signin_tabs.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%ul.nav-links.new-session-tabs - %li.active - %a{ href: '#ldap' } LDAP - %li - %a{ href: '#login-pane'} Standard diff --git a/spec/javascripts/fixtures/sketch_viewer.html.haml b/spec/javascripts/fixtures/sketch_viewer.html.haml deleted file mode 100644 index f01bd00925a..00000000000 --- a/spec/javascripts/fixtures/sketch_viewer.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -.file-content#js-sketch-viewer{ data: { endpoint: '/test_sketch_file.sketch' } } - .js-loading-icon diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb index bcd6546f3df..ebc5b793166 100644 --- a/spec/javascripts/fixtures/snippet.rb +++ b/spec/javascripts/fixtures/snippet.rb @@ -23,7 +23,7 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do remove_repository(project) end - it 'snippets/show.html.raw' do |example| + it 'snippets/show.html' do |example| create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item') get(:show, params: { id: snippet.to_param }) diff --git a/spec/javascripts/fixtures/static/README.md b/spec/javascripts/fixtures/static/README.md new file mode 100644 index 00000000000..b5c2f8233bf --- /dev/null +++ b/spec/javascripts/fixtures/static/README.md @@ -0,0 +1,3 @@ +# Please do not add new files here! + +Instead use a Ruby file in the fixtures root directory (`spec/javascripts/fixtures/`). diff --git a/spec/javascripts/fixtures/static/ajax_loading_spinner.html b/spec/javascripts/fixtures/static/ajax_loading_spinner.html new file mode 100644 index 00000000000..0e1ebb32b1c --- /dev/null +++ b/spec/javascripts/fixtures/static/ajax_loading_spinner.html @@ -0,0 +1,3 @@ +<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami"> +<i class="fa fa-trash-o"></i> +</a> diff --git a/spec/javascripts/fixtures/static/balsamiq_viewer.html b/spec/javascripts/fixtures/static/balsamiq_viewer.html new file mode 100644 index 00000000000..cdd723d1a84 --- /dev/null +++ b/spec/javascripts/fixtures/static/balsamiq_viewer.html @@ -0,0 +1 @@ +<div class="file-content balsamiq-viewer" data-endpoint="/test" id="js-balsamiq-viewer"></div> diff --git a/spec/javascripts/fixtures/static/create_item_dropdown.html b/spec/javascripts/fixtures/static/create_item_dropdown.html new file mode 100644 index 00000000000..d2d38370092 --- /dev/null +++ b/spec/javascripts/fixtures/static/create_item_dropdown.html @@ -0,0 +1,11 @@ +<div class="js-create-item-dropdown-fixture-root"> +<input name="variable[environment]" type="hidden"> +<div class="dropdown "><button class="dropdown-menu-toggle js-dropdown-menu-toggle" type="button" data-toggle="dropdown"><span class="dropdown-toggle-text ">some label</span><i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i></button><div class="dropdown-menu dropdown-select dropdown-menu-selectable"><div class="dropdown-input"><input type="search" id="" class="dropdown-input-field" autocomplete="off" /><i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i><i aria-hidden="true" data-hidden="true" role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i></div><div class="dropdown-content js-dropdown-content"></div><div class="dropdown-footer"><ul class="dropdown-footer-list"> +<li> +<button class="dropdown-create-new-item-button js-dropdown-create-new-item"> +Create wildcard +<code></code> +</button> +</li> +</ul> +</div><div class="dropdown-loading"><i aria-hidden="true" data-hidden="true" class="fa fa-spinner fa-spin"></i></div></div></div></div> diff --git a/spec/javascripts/fixtures/static/event_filter.html b/spec/javascripts/fixtures/static/event_filter.html new file mode 100644 index 00000000000..8e9b6fb1b5c --- /dev/null +++ b/spec/javascripts/fixtures/static/event_filter.html @@ -0,0 +1,44 @@ +<ul class="nav-links event-filter scrolling-tabs nav nav-tabs"> +<li class="active"> +<a class="event-filter-link" href="/dashboard/activity" id="all_event_filter" title="Filter by all"> +<span> +All +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="push_event_filter" title="Filter by push events"> +<span> +Push events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="merged_event_filter" title="Filter by merge events"> +<span> +Merge events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="issue_event_filter" title="Filter by issue events"> +<span> +Issue events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="comments_event_filter" title="Filter by comments"> +<span> +Comments +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="team_event_filter" title="Filter by team"> +<span> +Team +</span> +</a> +</li> +</ul> diff --git a/spec/javascripts/fixtures/static/gl_dropdown.html b/spec/javascripts/fixtures/static/gl_dropdown.html new file mode 100644 index 00000000000..08f6738414e --- /dev/null +++ b/spec/javascripts/fixtures/static/gl_dropdown.html @@ -0,0 +1,26 @@ +<div> +<div class="dropdown inline"> +<button class="dropdown-menu-toggle" data-toggle="dropdown" id="js-project-dropdown" type="button"> +<div class="dropdown-toggle-text"> +Projects +</div> +<i class="fa fa-chevron-down dropdown-toggle-caret js-projects-dropdown-toggle"></i> +</button> +<div class="dropdown-menu dropdown-select dropdown-menu-selectable"> +<div class="dropdown-title"> +<span>Go to project</span> +<button aria="{:label=>"Close"}" class="dropdown-title-button dropdown-menu-close"> +<i class="fa fa-times dropdown-menu-close-icon"></i> +</button> +</div> +<div class="dropdown-input"> +<input class="dropdown-input-field" placeholder="Filter results" type="search"> +<i class="fa fa-search dropdown-input-search"></i> +</div> +<div class="dropdown-content"></div> +<div class="dropdown-loading"> +<i class="fa fa-spinner fa-spin"></i> +</div> +</div> +</div> +</div> diff --git a/spec/javascripts/fixtures/static/gl_field_errors.html b/spec/javascripts/fixtures/static/gl_field_errors.html new file mode 100644 index 00000000000..f8470e02b7c --- /dev/null +++ b/spec/javascripts/fixtures/static/gl_field_errors.html @@ -0,0 +1,22 @@ +<form action="submit" class="gl-show-field-errors" method="post"> +<div class="form-group"> +<input class="required-text" required type="text">Text</input> +</div> +<div class="form-group"> +<input class="email" required title="Please provide a valid email address." type="email">Email</input> +</div> +<div class="form-group"> +<input class="password" required type="password">Password</input> +</div> +<div class="form-group"> +<input class="alphanumeric" pattern="[a-zA-Z0-9]" required type="text">Alphanumeric</input> +</div> +<div class="form-group"> +<input class="hidden" type="hidden"> +</div> +<div class="form-group"> +<input class="custom gl-field-error-ignore" type="text">Custom, do not validate</input> +</div> +<div class="form-group"></div> +<input class="submit" type="submit">Submit</input> +</form> diff --git a/spec/javascripts/fixtures/static/issuable_filter.html b/spec/javascripts/fixtures/static/issuable_filter.html new file mode 100644 index 00000000000..06b70fb43f1 --- /dev/null +++ b/spec/javascripts/fixtures/static/issuable_filter.html @@ -0,0 +1,9 @@ +<form action="/user/project/issues?scope=all&state=closed" class="js-filter-form"> +<input id="utf8" name="utf8" value="✓"> +<input id="check-all-issues" name="check-all-issues"> +<input id="search" name="search"> +<input id="author_id" name="author_id"> +<input id="assignee_id" name="assignee_id"> +<input id="milestone_title" name="milestone_title"> +<input id="label_name" name="label_name"> +</form> diff --git a/spec/javascripts/fixtures/static/issue_sidebar_label.html b/spec/javascripts/fixtures/static/issue_sidebar_label.html new file mode 100644 index 00000000000..ec8fb30f219 --- /dev/null +++ b/spec/javascripts/fixtures/static/issue_sidebar_label.html @@ -0,0 +1,26 @@ +<div class="block labels"> +<div class="sidebar-collapsed-icon js-sidebar-labels-tooltip"></div> +<div class="title hide-collapsed"> +<a class="edit-link float-right" href="#"> +Edit +</a> +</div> +<div class="selectbox hide-collapsed" style="display: none;"> +<div class="dropdown"> +<button class="dropdown-menu-toggle js-label-select js-multiselect" data-ability-name="issue" data-field-name="issue[label_names][]" data-issue-update="/root/test/issues/2.json" data-labels="/root/test/labels.json" data-project-id="12" data-show-any="true" data-show-no="true" data-toggle="dropdown" type="button"> +<span class="dropdown-toggle-text"> +Label +</span> +<i class="fa fa-chevron-down"></i> +</button> +<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> +<div class="dropdown-page-one"> +<div class="dropdown-content"></div> +<div class="dropdown-loading"> +<i class="fa fa-spinner fa-spin"></i> +</div> +</div> +</div> +</div> +</div> +</div> diff --git a/spec/javascripts/fixtures/static/line_highlighter.html b/spec/javascripts/fixtures/static/line_highlighter.html new file mode 100644 index 00000000000..897a25d6760 --- /dev/null +++ b/spec/javascripts/fixtures/static/line_highlighter.html @@ -0,0 +1,107 @@ +<div class="file-holder"> +<div class="file-content"> +<div class="line-numbers"> +<a data-line-number="1" href="#L1" id="L1"> +<i class="fa fa-link"></i> +1 +</a> +<a data-line-number="2" href="#L2" id="L2"> +<i class="fa fa-link"></i> +2 +</a> +<a data-line-number="3" href="#L3" id="L3"> +<i class="fa fa-link"></i> +3 +</a> +<a data-line-number="4" href="#L4" id="L4"> +<i class="fa fa-link"></i> +4 +</a> +<a data-line-number="5" href="#L5" id="L5"> +<i class="fa fa-link"></i> +5 +</a> +<a data-line-number="6" href="#L6" id="L6"> +<i class="fa fa-link"></i> +6 +</a> +<a data-line-number="7" href="#L7" id="L7"> +<i class="fa fa-link"></i> +7 +</a> +<a data-line-number="8" href="#L8" id="L8"> +<i class="fa fa-link"></i> +8 +</a> +<a data-line-number="9" href="#L9" id="L9"> +<i class="fa fa-link"></i> +9 +</a> +<a data-line-number="10" href="#L10" id="L10"> +<i class="fa fa-link"></i> +10 +</a> +<a data-line-number="11" href="#L11" id="L11"> +<i class="fa fa-link"></i> +11 +</a> +<a data-line-number="12" href="#L12" id="L12"> +<i class="fa fa-link"></i> +12 +</a> +<a data-line-number="13" href="#L13" id="L13"> +<i class="fa fa-link"></i> +13 +</a> +<a data-line-number="14" href="#L14" id="L14"> +<i class="fa fa-link"></i> +14 +</a> +<a data-line-number="15" href="#L15" id="L15"> +<i class="fa fa-link"></i> +15 +</a> +<a data-line-number="16" href="#L16" id="L16"> +<i class="fa fa-link"></i> +16 +</a> +<a data-line-number="17" href="#L17" id="L17"> +<i class="fa fa-link"></i> +17 +</a> +<a data-line-number="18" href="#L18" id="L18"> +<i class="fa fa-link"></i> +18 +</a> +<a data-line-number="19" href="#L19" id="L19"> +<i class="fa fa-link"></i> +19 +</a> +<a data-line-number="20" href="#L20" id="L20"> +<i class="fa fa-link"></i> +20 +</a> +<a data-line-number="21" href="#L21" id="L21"> +<i class="fa fa-link"></i> +21 +</a> +<a data-line-number="22" href="#L22" id="L22"> +<i class="fa fa-link"></i> +22 +</a> +<a data-line-number="23" href="#L23" id="L23"> +<i class="fa fa-link"></i> +23 +</a> +<a data-line-number="24" href="#L24" id="L24"> +<i class="fa fa-link"></i> +24 +</a> +<a data-line-number="25" href="#L25" id="L25"> +<i class="fa fa-link"></i> +25 +</a> +</div> +<pre class="code highlight"><code><span class="line" id="LC1">Line 1</span><span class="line" id="LC2">Line 2</span><span class="line" id="LC3">Line 3</span><span class="line" id="LC4">Line 4</span><span class="line" id="LC5">Line 5</span><span class="line" id="LC6">Line 6</span><span class="line" id="LC7">Line 7</span><span class="line" id="LC8">Line 8</span><span class="line" id="LC9">Line 9</span><span class="line" id="LC10">Line 10</span><span class="line" id="LC11">Line 11</span><span class="line" id="LC12">Line 12</span><span class="line" id="LC13">Line 13</span><span class="line" id="LC14">Line 14</span><span class="line" id="LC15">Line 15</span><span class="line" id="LC16">Line 16</span><span class="line" id="LC17">Line 17</span><span class="line" id="LC18">Line 18</span><span class="line" id="LC19">Line 19</span><span class="line" id="LC20">Line 20</span><span class="line" id="LC21">Line 21</span><span class="line" id="LC22">Line 22</span><span class="line" id="LC23">Line 23</span><span class="line" id="LC24">Line 24</span><span class="line" id="LC25">Line 25</span></code></pre> +</div> +</div> diff --git a/spec/javascripts/fixtures/static/linked_tabs.html b/spec/javascripts/fixtures/static/linked_tabs.html new file mode 100644 index 00000000000..c25463bf1db --- /dev/null +++ b/spec/javascripts/fixtures/static/linked_tabs.html @@ -0,0 +1,20 @@ +<ul class="nav nav-tabs new-session-tabs linked-tabs"> +<li class="nav-item"> +<a class="nav-link" data-action="tab1" data-target="div#tab1" data-toggle="tab" href="foo/bar/1"> +Tab 1 +</a> +</li> +<li class="nav-item"> +<a class="nav-link" data-action="tab2" data-target="div#tab2" data-toggle="tab" href="foo/bar/1/context"> +Tab 2 +</a> +</li> +</ul> +<div class="tab-content"> +<div class="tab-pane" id="tab1"> +Tab 1 Content +</div> +<div class="tab-pane" id="tab2"> +Tab 2 Content +</div> +</div> diff --git a/spec/javascripts/fixtures/static/merge_requests_show.html b/spec/javascripts/fixtures/static/merge_requests_show.html new file mode 100644 index 00000000000..87e36c9f315 --- /dev/null +++ b/spec/javascripts/fixtures/static/merge_requests_show.html @@ -0,0 +1,15 @@ +<a class="btn-close"></a> +<div class="detail-page-description"> +<div class="description js-task-list-container"> +<div class="md"> +<ul class="task-list"> +<li class="task-list-item"> +<input class="task-list-item-checkbox" type="checkbox"> +Task List Item +</li> +</ul> +<textarea class="js-task-list-field">- [ ] Task List Item</textarea> +</div> +</div> +</div> +<form action="/foo" class="js-issuable-update"></form> diff --git a/spec/javascripts/fixtures/static/mini_dropdown_graph.html b/spec/javascripts/fixtures/static/mini_dropdown_graph.html new file mode 100644 index 00000000000..cd0b8dec3fc --- /dev/null +++ b/spec/javascripts/fixtures/static/mini_dropdown_graph.html @@ -0,0 +1,13 @@ +<div class="js-builds-dropdown-tests dropdown dropdown js-mini-pipeline-graph"> +<button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar"> +Dropdown +</button> +<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> +<li class="js-builds-dropdown-list scrollable-menu"> +<ul></ul> +</li> +<li class="js-builds-dropdown-loading hidden"> +<span class="fa fa-spinner"></span> +</li> +</ul> +</div> diff --git a/spec/javascripts/fixtures/static/notebook_viewer.html b/spec/javascripts/fixtures/static/notebook_viewer.html new file mode 100644 index 00000000000..4bbb7bf1094 --- /dev/null +++ b/spec/javascripts/fixtures/static/notebook_viewer.html @@ -0,0 +1 @@ +<div class="file-content" data-endpoint="/test" id="js-notebook-viewer"></div> diff --git a/spec/javascripts/fixtures/static/oauth_remember_me.html b/spec/javascripts/fixtures/static/oauth_remember_me.html new file mode 100644 index 00000000000..9ba1ffc72fe --- /dev/null +++ b/spec/javascripts/fixtures/static/oauth_remember_me.html @@ -0,0 +1,6 @@ +<div id="oauth-container"> +<input id="remember_me" type="checkbox"> +<a class="oauth-login twitter" href="http://example.com/"></a> +<a class="oauth-login github" href="http://example.com/"></a> +<a class="oauth-login facebook" href="http://example.com/?redirect_fragment=L1"></a> +</div> diff --git a/spec/javascripts/fixtures/static/pdf_viewer.html b/spec/javascripts/fixtures/static/pdf_viewer.html new file mode 100644 index 00000000000..350d35a262f --- /dev/null +++ b/spec/javascripts/fixtures/static/pdf_viewer.html @@ -0,0 +1 @@ +<div class="file-content" data-endpoint="/test" id="js-pdf-viewer"></div> diff --git a/spec/javascripts/fixtures/static/pipeline_graph.html b/spec/javascripts/fixtures/static/pipeline_graph.html new file mode 100644 index 00000000000..422372bb7d5 --- /dev/null +++ b/spec/javascripts/fixtures/static/pipeline_graph.html @@ -0,0 +1,24 @@ +<div class="pipeline-visualization js-pipeline-graph"> +<ul class="stage-column-list"> +<li class="stage-column"> +<div class="stage-name"> +<a href="/"> +Test +<div class="builds-container"> +<ul> +<li class="build"> +<div class="curve"></div> +<a> +<svg></svg> +<div class="ci-status-text"> +stop_review +</div> +</a> +</li> +</ul> +</div> +</a> +</div> +</li> +</ul> +</div> diff --git a/spec/javascripts/fixtures/static/pipelines.html b/spec/javascripts/fixtures/static/pipelines.html new file mode 100644 index 00000000000..42333f94f2f --- /dev/null +++ b/spec/javascripts/fixtures/static/pipelines.html @@ -0,0 +1,3 @@ +<div> +<div data-can-create-pipeline="true" data-ci-lint-path="foo" data-empty-state-svg-path="foo" data-endpoint="foo" data-error-state-svg-path="foo" data-has-ci="foo" data-help-auto-devops-path="foo" data-help-page-path="foo" data-new-pipeline-path="foo" data-reset-cache-path="foo" id="pipelines-list-vue"></div> +</div> diff --git a/spec/javascripts/fixtures/static/project_select_combo_button.html b/spec/javascripts/fixtures/static/project_select_combo_button.html new file mode 100644 index 00000000000..50c826051c0 --- /dev/null +++ b/spec/javascripts/fixtures/static/project_select_combo_button.html @@ -0,0 +1,9 @@ +<div class="project-item-select-holder"> +<input class="project-item-select" data-group-id="12345" data-relative-path="issues/new"> +<a class="new-project-item-link" data-label="New issue" data-type="issues" href=""> +<i class="fa fa-spinner spin"></i> +</a> +<a class="new-project-item-select-button"> +<i class="fa fa-caret-down"></i> +</a> +</div> diff --git a/spec/javascripts/fixtures/static/search_autocomplete.html b/spec/javascripts/fixtures/static/search_autocomplete.html new file mode 100644 index 00000000000..29db9020424 --- /dev/null +++ b/spec/javascripts/fixtures/static/search_autocomplete.html @@ -0,0 +1,15 @@ +<div class="search search-form"> +<form class="form-inline"> +<div class="search-input-container"> +<div class="search-input-wrap"> +<div class="dropdown"> +<input class="search-input dropdown-menu-toggle" id="search"> +<div class="dropdown-menu dropdown-select"> +<div class="dropdown-content"></div> +</div> +</div> +</div> +</div> +<input class="js-search-project-options" type="hidden"> +</form> +</div> diff --git a/spec/javascripts/fixtures/static/signin_tabs.html b/spec/javascripts/fixtures/static/signin_tabs.html new file mode 100644 index 00000000000..7e66ab9394b --- /dev/null +++ b/spec/javascripts/fixtures/static/signin_tabs.html @@ -0,0 +1,8 @@ +<ul class="nav-links new-session-tabs"> +<li class="active"> +<a href="#ldap">LDAP</a> +</li> +<li> +<a href="#login-pane">Standard</a> +</li> +</ul> diff --git a/spec/javascripts/fixtures/static/sketch_viewer.html b/spec/javascripts/fixtures/static/sketch_viewer.html new file mode 100644 index 00000000000..e25e554e568 --- /dev/null +++ b/spec/javascripts/fixtures/static/sketch_viewer.html @@ -0,0 +1,3 @@ +<div class="file-content" data-endpoint="/test_sketch_file.sketch" id="js-sketch-viewer"> +<div class="js-loading-icon"></div> +</div> diff --git a/spec/javascripts/fixtures/static_fixtures.rb b/spec/javascripts/fixtures/static_fixtures.rb index 852a82587b9..cb4b90cdca5 100644 --- a/spec/javascripts/fixtures/static_fixtures.rb +++ b/spec/javascripts/fixtures/static_fixtures.rb @@ -3,29 +3,17 @@ require 'spec_helper' describe ApplicationController, '(Static JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - before(:all) do - clean_frontend_fixtures('static/') - end - - JavaScriptFixturesHelpers::FIXTURE_PATHS.each do |fixture_path| - fixtures_path = File.expand_path(fixture_path, Rails.root) - - Dir.glob(File.expand_path('**/*.haml', fixtures_path)).map do |file_path| - template_file_name = file_path.sub(/\A#{fixtures_path}#{File::SEPARATOR}/, '') - - it "static/#{template_file_name.sub(/\.haml\z/, '.raw')}" do |example| - fixture_file_name = example.description - rendered = render_template(fixture_path, template_file_name) - store_frontend_fixture(rendered, fixture_file_name) - end + Dir.glob('{,ee/}spec/javascripts/fixtures/**/*.haml').map do |file_path| + it "static/#{file_path.sub(%r{\A(ee/)?spec/javascripts/fixtures/}, '').sub(/\.haml\z/, '')}" do |example| + store_frontend_fixture(render_template(file_path), example.description) end end private - def render_template(fixture_path, template_file_name) + def render_template(template_file_name) controller = ApplicationController.new - controller.prepend_view_path(fixture_path) - controller.render_to_string(template: template_file_name, layout: false) + controller.prepend_view_path(File.dirname(template_file_name)) + controller.render_to_string(template: File.basename(template_file_name), layout: false) end end diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb index b5f6620873b..6e37a2e5a4c 100644 --- a/spec/javascripts/fixtures/todos.rb +++ b/spec/javascripts/fixtures/todos.rb @@ -26,7 +26,7 @@ describe 'Todos (JavaScript fixtures)' do sign_in(admin) end - it 'todos/todos.html.raw' do |example| + it 'todos/todos.html' do |example| get :index expect(response).to be_success diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb index 5cdbadef639..15866d65a4f 100644 --- a/spec/javascripts/fixtures/u2f.rb +++ b/spec/javascripts/fixtures/u2f.rb @@ -18,7 +18,7 @@ context 'U2F' do set_devise_mapping(context: @request) end - it 'u2f/authenticate.html.raw' do |example| + it 'u2f/authenticate.html' do |example| allow(controller).to receive(:find_user).and_return(user) post :create, params: { user: { login: user.username, password: user.password } } @@ -36,7 +36,7 @@ context 'U2F' do allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares') end - it 'u2f/register.html.raw' do |example| + it 'u2f/register.html' do |example| get :show expect(response).to be_success diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js index b1cc4d8dc8d..6814f656f5d 100644 --- a/spec/javascripts/frequent_items/components/app_spec.js +++ b/spec/javascripts/frequent_items/components/app_spec.js @@ -194,7 +194,7 @@ describe('Frequent Items App Component', () => { expect(loadingEl).toBeDefined(); expect(loadingEl.classList.contains('prepend-top-20')).toBe(true); - expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects'); + expect(loadingEl.querySelector('span').getAttribute('aria-label')).toBe('Loading projects'); done(); }); }); diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 85083653db8..57e31d933ca 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -5,7 +5,7 @@ import GLDropdown from '~/gl_dropdown'; import '~/lib/utils/common_utils'; describe('glDropdown', function describeDropdown() { - preloadFixtures('static/gl_dropdown.html.raw'); + preloadFixtures('static/gl_dropdown.html'); loadJSONFixtures('projects.json'); const NON_SELECTABLE_CLASSES = @@ -64,7 +64,7 @@ describe('glDropdown', function describeDropdown() { } beforeEach(() => { - loadFixtures('static/gl_dropdown.html.raw'); + loadFixtures('static/gl_dropdown.html'); this.dropdownContainerElement = $('.dropdown.inline'); this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); this.projectsData = getJSONFixture('projects.json'); diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js index b463c9afbee..294f219d6fe 100644 --- a/spec/javascripts/gl_field_errors_spec.js +++ b/spec/javascripts/gl_field_errors_spec.js @@ -4,10 +4,10 @@ import $ from 'jquery'; import GlFieldErrors from '~/gl_field_errors'; describe('GL Style Field Errors', function() { - preloadFixtures('static/gl_field_errors.html.raw'); + preloadFixtures('static/gl_field_errors.html'); beforeEach(function() { - loadFixtures('static/gl_field_errors.html.raw'); + loadFixtures('static/gl_field_errors.html'); const $form = $('form.gl-show-field-errors'); this.$form = $form; diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index d832441dc93..31873311e16 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -502,7 +502,7 @@ describe('AppComponent', () => { vm.isLoading = true; Vue.nextTick(() => { expect(vm.$el.querySelector('.loading-animation')).toBeDefined(); - expect(vm.$el.querySelector('i.fa').getAttribute('aria-label')).toBe('Loading groups'); + expect(vm.$el.querySelector('span').getAttribute('aria-label')).toBe('Loading groups'); done(); }); }); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index 2fe34e5a76f..0ddf589f368 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -3,7 +3,7 @@ import initTodoToggle from '~/header'; describe('Header', function() { const todosPendingCount = '.todos-count'; - const fixtureTemplate = 'issues/open-issue.html.raw'; + const fixtureTemplate = 'issues/open-issue.html'; function isTodosCountHidden() { return $(todosPendingCount).hasClass('hidden'); diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js index ffc2a4c9ddb..db1988be3e1 100644 --- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js @@ -76,6 +76,7 @@ describe('IDE commit sidebar radio group', () => { const Component = Vue.extend(radioGroup); store.state.commit.commitAction = '1'; + store.state.commit.newBranchName = 'test-123'; vm = createComponentWithStore(Component, store, { value: '1', @@ -113,6 +114,12 @@ describe('IDE commit sidebar radio group', () => { done(); }); }); + + it('renders newBranchName if present', () => { + const input = vm.$el.querySelector('.form-control'); + + expect(input.value).toBe('test-123'); + }); }); describe('tooltipTitle', () => { diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js index 430e8e2baa3..80d6c7fd564 100644 --- a/spec/javascripts/ide/components/error_message_spec.js +++ b/spec/javascripts/ide/components/error_message_spec.js @@ -84,7 +84,7 @@ describe('IDE error message component', () => { expect(vm.isLoading).toBe(true); - vm.$nextTick(() => { + setTimeout(() => { expect(vm.isLoading).toBe(false); done(); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index d1a0964ccdd..0556feae46a 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -109,6 +109,18 @@ describe('new file modal component', () => { expect(vm.entryName).toBe('index.js'); }); + + it('removes leading/trailing spaces when found in the new name', () => { + vm.entryName = ' index.js '; + + expect(vm.entryName).toBe('index.js'); + }); + + it('does not remove internal spaces in the file name', () => { + vm.entryName = ' In Praise of Idleness.txt '; + + expect(vm.entryName).toBe('In Praise of Idleness.txt'); + }); }); }); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 7ddc734ff56..1e5b55af4ba 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -121,68 +121,48 @@ describe('IDE store file actions', () => { store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line }); - it('calls scrollToTab', done => { - store - .dispatch('setFileActive', localFile.path) - .then(() => { - expect(scrollToTabSpy).toHaveBeenCalled(); - - done(); - }) - .catch(done.fail); - }); + it('calls scrollToTab', () => { + const dispatch = jasmine.createSpy(); - it('sets the file active', done => { - store - .dispatch('setFileActive', localFile.path) - .then(() => { - expect(localFile.active).toBeTruthy(); + actions.setFileActive( + { commit() {}, state: store.state, getters: store.getters, dispatch }, + localFile.path, + ); - done(); - }) - .catch(done.fail); + expect(dispatch).toHaveBeenCalledWith('scrollToTab'); }); - it('returns early if file is already active', done => { - localFile.active = true; + it('commits SET_FILE_ACTIVE', () => { + const commit = jasmine.createSpy(); - store - .dispatch('setFileActive', localFile.path) - .then(() => { - expect(scrollToTabSpy).not.toHaveBeenCalled(); + actions.setFileActive( + { commit, state: store.state, getters: store.getters, dispatch() {} }, + localFile.path, + ); - done(); - }) - .catch(done.fail); + expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { + path: localFile.path, + active: true, + }); }); - it('sets current active file to not active', done => { + it('sets current active file to not active', () => { const f = file('newActive'); store.state.entries[f.path] = f; localFile.active = true; store.state.openFiles.push(localFile); - store - .dispatch('setFileActive', f.path) - .then(() => { - expect(localFile.active).toBeFalsy(); + const commit = jasmine.createSpy(); - done(); - }) - .catch(done.fail); - }); - - it('resets location.hash for line highlighting', done => { - window.location.hash = 'test'; - - store - .dispatch('setFileActive', localFile.path) - .then(() => { - expect(window.location.hash).not.toBe('test'); + actions.setFileActive( + { commit, state: store.state, getters: store.getters, dispatch() {} }, + f.path, + ); - done(); - }) - .catch(done.fail); + expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { + path: localFile.path, + active: false, + }); }); }); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js index 06b8b452319..34d97347438 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js @@ -396,7 +396,7 @@ describe('IDE commit module actions', () => { .then(() => { expect(visitUrl).toHaveBeenCalledWith( `webUrl/merge_requests/new?merge_request[source_branch]=${ - store.getters['commit/newBranchName'] + store.getters['commit/placeholderBranchName'] }&merge_request[target_branch]=master`, ); diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js index 3f4bf407a1f..702e78ef773 100644 --- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js +++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js @@ -29,11 +29,11 @@ describe('IDE commit module getters', () => { }); }); - describe('newBranchName', () => { + describe('placeholderBranchName', () => { it('includes username, currentBranchId, patch & random number', () => { gon.current_username = 'username'; - const branch = getters.newBranchName(state, null, { + const branch = getters.placeholderBranchName(state, null, { currentBranchId: 'testing', }); @@ -46,7 +46,7 @@ describe('IDE commit module getters', () => { currentBranchId: 'master', }; const localGetters = { - newBranchName: 'newBranchName', + placeholderBranchName: 'newBranchName', }; beforeEach(() => { @@ -71,7 +71,7 @@ describe('IDE commit module getters', () => { expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName'); }); - it('uses getters newBranchName when state newBranchName is empty', () => { + it('uses placeholderBranchName when state newBranchName is empty', () => { Object.assign(state, { newBranchName: '', }); diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js index 4f4c9a7b463..069e2cb07b5 100644 --- a/spec/javascripts/integrations/integration_settings_form_spec.js +++ b/spec/javascripts/integrations/integration_settings_form_spec.js @@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils'; import IntegrationSettingsForm from '~/integrations/integration_settings_form'; describe('IntegrationSettingsForm', () => { - const FIXTURE = 'services/edit_service.html.raw'; + const FIXTURE = 'services/edit_service.html'; preloadFixtures(FIXTURE); beforeEach(() => { diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 0ccf771c7ef..dfc889773c1 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -75,7 +75,7 @@ describe('Issuable output', () => { .then(() => { expect(document.querySelector('title').innerText).toContain('this is a title (#1)'); expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>'); - expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>'); + expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>this is a description!</p>'); expect(vm.$el.querySelector('.js-task-list-field').value).toContain( 'this is a description', ); @@ -92,7 +92,7 @@ describe('Issuable output', () => { .then(() => { expect(document.querySelector('title').innerText).toContain('2 (#1)'); expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>'); - expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>'); + expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>42</p>'); expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42'); expect(vm.$el.querySelector('.edited-text')).toBeTruthy(); expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch( diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 2eeed6770be..7e00fbf2745 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -43,12 +43,12 @@ describe('Description component', () => { Vue.nextTick(() => { expect( - vm.$el.querySelector('.wiki').classList.contains('issue-realtime-pre-pulse'), + vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'), ).toBeTruthy(); setTimeout(() => { expect( - vm.$el.querySelector('.wiki').classList.contains('issue-realtime-trigger-pulse'), + vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'), ).toBeTruthy(); done(); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 7be495d1d35..11ab6c38a55 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -9,9 +9,9 @@ import '~/lib/utils/text_utility'; describe('Issue', function() { let $boxClosed, $boxOpen, $btn; - preloadFixtures('issues/closed-issue.html.raw'); - preloadFixtures('issues/issue-with-task-list.html.raw'); - preloadFixtures('issues/open-issue.html.raw'); + preloadFixtures('issues/closed-issue.html'); + preloadFixtures('issues/issue-with-task-list.html'); + preloadFixtures('issues/open-issue.html'); function expectErrorMessage() { const $flashMessage = $('div.flash-alert'); @@ -105,9 +105,9 @@ describe('Issue', function() { beforeEach(function() { if (isIssueInitiallyOpen) { - loadFixtures('issues/open-issue.html.raw'); + loadFixtures('issues/open-issue.html'); } else { - loadFixtures('issues/closed-issue.html.raw'); + loadFixtures('issues/closed-issue.html'); } mock = new MockAdapter(axios); diff --git a/spec/javascripts/jobs/components/commit_block_spec.js b/spec/javascripts/jobs/components/commit_block_spec.js index 98eba3ac976..c02f564d01a 100644 --- a/spec/javascripts/jobs/components/commit_block_spec.js +++ b/spec/javascripts/jobs/components/commit_block_spec.js @@ -9,6 +9,7 @@ describe('Commit block', () => { const props = { commit: { short_id: '1f0fb84f', + id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c', commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c', title: 'Update README.md', }, @@ -42,7 +43,7 @@ describe('Commit block', () => { it('renders clipboard button', () => { expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual( - props.commit.short_id, + props.commit.id, ); }); }); diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js index 9c731ae2f68..eccb4e13d67 100644 --- a/spec/javascripts/jobs/components/stages_dropdown_spec.js +++ b/spec/javascripts/jobs/components/stages_dropdown_spec.js @@ -1,59 +1,167 @@ import Vue from 'vue'; import component from '~/jobs/components/stages_dropdown.vue'; +import { trimText } from 'spec/helpers/vue_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Stages Dropdown', () => { const Component = Vue.extend(component); let vm; - beforeEach(() => { - vm = mountComponent(Component, { - pipeline: { - id: 28029444, - details: { - status: { - details_path: '/gitlab-org/gitlab-ce/pipelines/28029444', - group: 'success', - has_details: true, - icon: 'status_success', - label: 'passed', - text: 'passed', - tooltip: 'passed', - }, - }, - path: 'pipeline/28029444', + const mockPipelineData = { + id: 28029444, + details: { + status: { + details_path: '/gitlab-org/gitlab-ce/pipelines/28029444', + group: 'success', + has_details: true, + icon: 'status_success', + label: 'passed', + text: 'passed', + tooltip: 'passed', }, - stages: [ - { - name: 'build', - }, - { - name: 'test', - }, - ], - selectedStage: 'deploy', + }, + path: 'pipeline/28029444', + flags: { + merge_request_pipeline: true, + detached_merge_request_pipeline: false, + }, + merge_request: { + iid: 1234, + path: '/root/detached-merge-request-pipelines/merge_requests/1', + title: 'Update README.md', + source_branch: 'feature-1234', + source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1234', + target_branch: 'master', + target_branch_path: '/root/detached-merge-request-pipelines/branches/master', + }, + ref: { + name: 'test-branch', + }, + }; + + describe('without a merge request pipeline', () => { + let pipeline; + + beforeEach(() => { + pipeline = JSON.parse(JSON.stringify(mockPipelineData)); + delete pipeline.merge_request; + delete pipeline.flags.merge_request_pipeline; + delete pipeline.flags.detached_merge_request_pipeline; + + vm = mountComponent(Component, { + pipeline, + stages: [{ name: 'build' }, { name: 'test' }], + selectedStage: 'deploy', + }); }); - }); - afterEach(() => { - vm.$destroy(); - }); + afterEach(() => { + vm.$destroy(); + }); - it('renders pipeline status', () => { - expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull(); - }); + it('renders pipeline status', () => { + expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull(); + }); + + it('renders pipeline link', () => { + expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual( + 'pipeline/28029444', + ); + }); + + it('renders dropdown with stages', () => { + expect(vm.$el.querySelector('.dropdown .js-stage-item').textContent).toContain('build'); + }); + + it('rendes selected stage', () => { + expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy'); + }); - it('renders pipeline link', () => { - expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual( - 'pipeline/28029444', - ); + it(`renders the pipeline info text like "Pipeline #123 for source_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for ${pipeline.ref.name}`; + const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); + + expect(actual).toBe(expected); + }); }); - it('renders dropdown with stages', () => { - expect(vm.$el.querySelector('.dropdown .js-stage-item').textContent).toContain('build'); + describe('with an "attached" merge request pipeline', () => { + let pipeline; + + beforeEach(() => { + pipeline = JSON.parse(JSON.stringify(mockPipelineData)); + pipeline.flags.merge_request_pipeline = true; + pipeline.flags.detached_merge_request_pipeline = false; + + vm = mountComponent(Component, { + pipeline, + stages: [], + selectedStage: 'deploy', + }); + }); + + it(`renders the pipeline info text like "Pipeline #123 for !456 with source_branch into target_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${ + pipeline.merge_request.source_branch + } into ${pipeline.merge_request.target_branch}`; + const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); + + expect(actual).toBe(expected); + }); + + it(`renders the correct merge request link`, () => { + const actual = vm.$el.querySelector('.js-mr-link').href; + + expect(actual).toContain(pipeline.merge_request.path); + }); + + it(`renders the correct source branch link`, () => { + const actual = vm.$el.querySelector('.js-source-branch-link').href; + + expect(actual).toContain(pipeline.merge_request.source_branch_path); + }); + + it(`renders the correct target branch link`, () => { + const actual = vm.$el.querySelector('.js-target-branch-link').href; + + expect(actual).toContain(pipeline.merge_request.target_branch_path); + }); }); - it('rendes selected stage', () => { - expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy'); + describe('with a detached merge request pipeline', () => { + let pipeline; + + beforeEach(() => { + pipeline = JSON.parse(JSON.stringify(mockPipelineData)); + pipeline.flags.merge_request_pipeline = false; + pipeline.flags.detached_merge_request_pipeline = true; + + vm = mountComponent(Component, { + pipeline, + stages: [], + selectedStage: 'deploy', + }); + }); + + it(`renders the pipeline info like "Pipeline #123 for !456 with source_branch"`, () => { + const expected = `Pipeline #${pipeline.id} for !${pipeline.merge_request.iid} with ${ + pipeline.merge_request.source_branch + }`; + const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText); + + expect(actual).toBe(expected); + }); + + it(`renders the correct merge request link`, () => { + const actual = vm.$el.querySelector('.js-mr-link').href; + + expect(actual).toContain(pipeline.merge_request.path); + }); + + it(`renders the correct source branch link`, () => { + const actual = vm.$el.querySelector('.js-source-branch-link').href; + + expect(actual).toContain(pipeline.merge_request.source_branch_path); + }); }); }); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js index e5678ee5379..ccf439aac74 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js +++ b/spec/javascripts/labels_issue_sidebar_spec.js @@ -16,10 +16,10 @@ let saveLabelCount = 0; let mock; describe('Issue dropdown sidebar', () => { - preloadFixtures('static/issue_sidebar_label.html.raw'); + preloadFixtures('static/issue_sidebar_label.html'); beforeEach(() => { - loadFixtures('static/issue_sidebar_label.html.raw'); + loadFixtures('static/issue_sidebar_label.html'); mock = new MockAdapter(axios); diff --git a/spec/javascripts/lazy_loader_spec.js b/spec/javascripts/lazy_loader_spec.js index cbdc1644430..f3fb792c62d 100644 --- a/spec/javascripts/lazy_loader_spec.js +++ b/spec/javascripts/lazy_loader_spec.js @@ -11,11 +11,11 @@ const execImmediately = callback => { describe('LazyLoader', function() { let lazyLoader = null; - preloadFixtures('issues/issue_with_comment.html.raw'); + preloadFixtures('issues/issue_with_comment.html'); describe('without IntersectionObserver', () => { beforeEach(function() { - loadFixtures('issues/issue_with_comment.html.raw'); + loadFixtures('issues/issue_with_comment.html'); lazyLoader = new LazyLoader({ observerNode: 'foobar', @@ -131,7 +131,7 @@ describe('LazyLoader', function() { describe('with IntersectionObserver', () => { beforeEach(function() { - loadFixtures('issues/issue_with_comment.html.raw'); + loadFixtures('issues/issue_with_comment.html'); lazyLoader = new LazyLoader({ observerNode: 'foobar', diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 4eea364bd69..a75470b4db8 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -5,7 +5,7 @@ import LineHighlighter from '~/line_highlighter'; describe('LineHighlighter', function() { var clickLine; - preloadFixtures('static/line_highlighter.html.raw'); + preloadFixtures('static/line_highlighter.html'); clickLine = function(number, eventData = {}) { if ($.isEmptyObject(eventData)) { return $('#L' + number).click(); @@ -15,7 +15,7 @@ describe('LineHighlighter', function() { } }; beforeEach(function() { - loadFixtures('static/line_highlighter.html.raw'); + loadFixtures('static/line_highlighter.html'); this['class'] = new LineHighlighter(); this.css = this['class'].highlightLineClass; return (this.spies = { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index ab809930804..431798c6ec3 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -11,9 +11,9 @@ describe('MergeRequest', function() { describe('task lists', function() { let mock; - preloadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + preloadFixtures('merge_requests/merge_request_with_task_list.html'); beforeEach(function() { - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + loadFixtures('merge_requests/merge_request_with_task_list.html'); spyOn(axios, 'patch').and.callThrough(); mock = new MockAdapter(axios); @@ -125,7 +125,7 @@ describe('MergeRequest', function() { describe('hideCloseButton', () => { describe('merge request of another user', () => { beforeEach(() => { - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + loadFixtures('merge_requests/merge_request_with_task_list.html'); this.el = document.querySelector('.js-issuable-actions'); new MergeRequest(); // eslint-disable-line no-new MergeRequest.hideCloseButton(); @@ -145,7 +145,7 @@ describe('MergeRequest', function() { describe('merge request of current_user', () => { beforeEach(() => { - loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + loadFixtures('merge_requests/merge_request_of_current_user.html'); this.el = document.querySelector('.js-issuable-actions'); MergeRequest.hideCloseButton(); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index c8df05eccf5..1295d900de7 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -22,8 +22,8 @@ describe('MergeRequestTabs', function() { }; preloadFixtures( - 'merge_requests/merge_request_with_task_list.html.raw', - 'merge_requests/diff_comment.html.raw', + 'merge_requests/merge_request_with_task_list.html', + 'merge_requests/diff_comment.html', ); beforeEach(function() { @@ -48,7 +48,7 @@ describe('MergeRequestTabs', function() { var windowTarget = '_blank'; beforeEach(function() { - loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); + loadFixtures('merge_requests/merge_request_with_task_list.html'); tabUrl = $('.commits-tab a').attr('href'); }); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js index 092ca9e1dab..aa4a376caf7 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js @@ -5,10 +5,10 @@ import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import timeoutPromise from './helpers/set_timeout_promise_helper'; describe('Mini Pipeline Graph Dropdown', () => { - preloadFixtures('static/mini_dropdown_graph.html.raw'); + preloadFixtures('static/mini_dropdown_graph.html'); beforeEach(() => { - loadFixtures('static/mini_dropdown_graph.html.raw'); + loadFixtures('static/mini_dropdown_graph.html'); }); describe('When is initialized', () => { diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index fb49290be19..549a7935c0f 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -203,6 +203,10 @@ describe('Area component', () => { .length, ).toBe(data.length); }); + + it('formats line width correctly', () => { + expect(chartData[0].lineStyle.width).toBe(2); + }); }); describe('scatterSeries', () => { diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index b1778029a77..454777fa912 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -33,6 +33,11 @@ describe('Dashboard', () => { <div class="layout-page"></div> `); + window.gon = { + ...window.gon, + ee: false, + }; + mock = new MockAdapter(axios); DashboardComponent = Vue.extend(Dashboard); }); @@ -98,7 +103,7 @@ describe('Dashboard', () => { }); }); - it('renders the dropdown with a number of environments', done => { + it('renders the environments dropdown with a number of environments', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), propsData: { ...propsData, hasMetrics: true, showPanels: false }, @@ -107,14 +112,16 @@ describe('Dashboard', () => { component.store.storeEnvironmentsData(environmentData); setTimeout(() => { - const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul li a'); + const dropdownMenuEnvironments = component.$el.querySelectorAll( + '.js-environments-dropdown .dropdown-item', + ); expect(dropdownMenuEnvironments.length).toEqual(component.store.environmentsData.length); done(); }); }); - it('hides the dropdown list when there is no environments', done => { + it('hides the environments dropdown list when there is no environments', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), propsData: { ...propsData, hasMetrics: true, showPanels: false }, @@ -123,14 +130,16 @@ describe('Dashboard', () => { component.store.storeEnvironmentsData([]); setTimeout(() => { - const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul'); + const dropdownMenuEnvironments = component.$el.querySelectorAll( + '.js-environments-dropdown .dropdown-item', + ); expect(dropdownMenuEnvironments.length).toEqual(0); done(); }); }); - it('renders the dropdown with a single is-active element', done => { + it('renders the environments dropdown with a single is-active element', done => { const component = new DashboardComponent({ el: document.querySelector('.prometheus-graphs'), propsData: { ...propsData, hasMetrics: true, showPanels: false }, @@ -139,14 +148,31 @@ describe('Dashboard', () => { component.store.storeEnvironmentsData(environmentData); setTimeout(() => { - const dropdownIsActiveElement = component.$el.querySelectorAll( - '.dropdown-menu ul li a.is-active', + const dropdownItems = component.$el.querySelectorAll( + '.js-environments-dropdown .dropdown-item[active="true"]', ); - expect(dropdownIsActiveElement.length).toEqual(1); - expect(dropdownIsActiveElement[0].textContent.trim()).toEqual( - component.currentEnvironmentName, - ); + expect(dropdownItems.length).toEqual(1); + expect(dropdownItems[0].textContent.trim()).toEqual(component.currentEnvironmentName); + done(); + }); + }); + + it('hides the dropdown', done => { + const component = new DashboardComponent({ + el: document.querySelector('.prometheus-graphs'), + propsData: { + ...propsData, + hasMetrics: true, + showPanels: false, + environmentsEndpoint: '', + }, + }); + + Vue.nextTick(() => { + const dropdownIsActiveElement = component.$el.querySelectorAll('.environments'); + + expect(dropdownIsActiveElement.length).toEqual(0); done(); }); }); diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index ffc7148fde2..6d4ef960c1a 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -330,6 +330,11 @@ export const metricsGroupsAPIResponse = { weight: 1, queries: [ { + appearance: { + line: { + width: 2, + }, + }, query_range: 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', label: 'Core Usage', diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 1d7b885e64f..4e3140ce4f1 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -3,7 +3,7 @@ import NewBranchForm from '~/new_branch_form'; describe('Branch', function() { describe('create a new branch', function() { - preloadFixtures('branches/new_branch.html.raw'); + preloadFixtures('branches/new_branch.html'); function fillNameWith(value) { $('.js-branch-name') @@ -16,7 +16,7 @@ describe('Branch', function() { } beforeEach(function() { - loadFixtures('branches/new_branch.html.raw'); + loadFixtures('branches/new_branch.html'); $('form').on('submit', function(e) { return e.preventDefault(); }); diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js index d716ece3766..ef876dc2941 100644 --- a/spec/javascripts/notes/components/note_app_spec.js +++ b/spec/javascripts/notes/components/note_app_spec.js @@ -192,9 +192,9 @@ describe('note_app', () => { expect(service.updateNote).toHaveBeenCalled(); // Wait for the requests to finish before destroying - Vue.nextTick() - .then(done) - .catch(done.fail); + setTimeout(() => { + done(); + }); }); }); @@ -227,9 +227,9 @@ describe('note_app', () => { expect(service.updateNote).toHaveBeenCalled(); // Wait for the requests to finish before destroying - Vue.nextTick() - .then(done) - .catch(done.fail); + setTimeout(() => { + done(); + }); }); }); }); diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js index 7cc324cfe44..b632ee6736d 100644 --- a/spec/javascripts/notes/components/note_form_spec.js +++ b/spec/javascripts/notes/components/note_form_spec.js @@ -5,11 +5,33 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; describe('issue_note_form component', () => { + const dummyAutosaveKey = 'some-autosave-key'; + const dummyDraft = 'dummy draft content'; + let store; let wrapper; let props; + const createComponentWrapper = () => { + const localVue = createLocalVue(); + return shallowMount(NoteForm, { + store, + propsData: props, + // see https://gitlab.com/gitlab-org/gitlab-ce/issues/56317 for the following + localVue, + sync: false, + }); + }; + beforeEach(() => { + spyOnDependency(NoteForm, 'getDraft').and.callFake(key => { + if (key === dummyAutosaveKey) { + return dummyDraft; + } + + return null; + }); + store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); @@ -19,15 +41,6 @@ describe('issue_note_form component', () => { noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.', noteId: '545', }; - - const localVue = createLocalVue(); - wrapper = shallowMount(NoteForm, { - store, - propsData: props, - // see https://gitlab.com/gitlab-org/gitlab-ce/issues/56317 for the following - localVue, - sync: false, - }); }); afterEach(() => { @@ -35,6 +48,10 @@ describe('issue_note_form component', () => { }); describe('noteHash', () => { + beforeEach(() => { + wrapper = createComponentWrapper(); + }); + it('returns note hash string based on `noteId`', () => { expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`); }); @@ -56,6 +73,10 @@ describe('issue_note_form component', () => { }); describe('conflicts editing', () => { + beforeEach(() => { + wrapper = createComponentWrapper(); + }); + it('should show conflict message if note changes outside the component', done => { wrapper.setProps({ ...props, @@ -85,6 +106,10 @@ describe('issue_note_form component', () => { }); describe('form', () => { + beforeEach(() => { + wrapper = createComponentWrapper(); + }); + it('should render text area with placeholder', () => { const textarea = wrapper.find('textarea'); @@ -181,4 +206,63 @@ describe('issue_note_form component', () => { }); }); }); + + describe('with autosaveKey', () => { + describe('with draft', () => { + beforeEach(done => { + Object.assign(props, { + noteBody: '', + autosaveKey: dummyAutosaveKey, + }); + wrapper = createComponentWrapper(); + + wrapper.vm + .$nextTick() + .then(done) + .catch(done.fail); + }); + + it('displays the draft in textarea', () => { + const textarea = wrapper.find('textarea'); + + expect(textarea.element.value).toBe(dummyDraft); + }); + }); + + describe('without draft', () => { + beforeEach(done => { + Object.assign(props, { + noteBody: '', + autosaveKey: 'some key without draft', + }); + wrapper = createComponentWrapper(); + + wrapper.vm + .$nextTick() + .then(done) + .catch(done.fail); + }); + + it('leaves the textarea empty', () => { + const textarea = wrapper.find('textarea'); + + expect(textarea.element.value).toBe(''); + }); + }); + + it('updates the draft if textarea content changes', () => { + const updateDraftSpy = spyOnDependency(NoteForm, 'updateDraft').and.stub(); + Object.assign(props, { + noteBody: '', + autosaveKey: dummyAutosaveKey, + }); + wrapper = createComponentWrapper(); + const textarea = wrapper.find('textarea'); + const dummyContent = 'some new content'; + + textarea.setValue(dummyContent); + + expect(updateDraftSpy).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent); + }); + }); }); diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index 2b93fb9fb45..3304c79cdb7 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -3,6 +3,7 @@ import createStore from '~/notes/stores'; import noteableDiscussion from '~/notes/components/noteable_discussion.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue'; +import NoteForm from '~/notes/components/note_form.vue'; import '~/behaviors/markdown/render_gfm'; import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data'; import mockDiffFile from '../../diffs/mock_data/diff_file'; @@ -72,7 +73,18 @@ describe('noteable_discussion component', () => { .then(() => wrapper.vm.$nextTick()) .then(() => { expect(wrapper.vm.isReplying).toEqual(true); - expect(wrapper.vm.$refs.noteForm).not.toBeNull(); + + const noteForm = wrapper.find(NoteForm); + + expect(noteForm.exists()).toBe(true); + + const noteFormProps = noteForm.props(); + + expect(noteFormProps.discussion).toBe(discussionMock); + expect(noteFormProps.isEditing).toBe(false); + expect(noteFormProps.line).toBe(null); + expect(noteFormProps.saveButtonTitle).toBe('Comment'); + expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 7c869d4c326..3d2c617e479 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -34,7 +34,7 @@ const htmlEscape = comment => { describe('Notes', function() { const FLASH_TYPE_ALERT = 'alert'; const NOTES_POST_PATH = /(.*)\/notes\?html=true$/; - var fixture = 'snippets/show.html.raw'; + var fixture = 'snippets/show.html'; preloadFixtures(fixture); beforeEach(function() { diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/javascripts/oauth_remember_me_spec.js index 4125706a407..381be82697e 100644 --- a/spec/javascripts/oauth_remember_me_spec.js +++ b/spec/javascripts/oauth_remember_me_spec.js @@ -2,10 +2,10 @@ import $ from 'jquery'; import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me'; describe('OAuthRememberMe', () => { - preloadFixtures('static/oauth_remember_me.html.raw'); + preloadFixtures('static/oauth_remember_me.html'); beforeEach(() => { - loadFixtures('static/oauth_remember_me.html.raw'); + loadFixtures('static/oauth_remember_me.html'); new OAuthRememberMe({ container: $('#oauth-container') }).bindEvents(); }); diff --git a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js b/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js index 561bd2c96cb..6a239e307e9 100644 --- a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js +++ b/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js @@ -5,7 +5,7 @@ import initUserInternalRegexPlaceholder, { } from '~/pages/admin/application_settings/account_and_limits'; describe('AccountAndLimits', () => { - const FIXTURE = 'application_settings/accounts_and_limit.html.raw'; + const FIXTURE = 'application_settings/accounts_and_limit.html'; let $userDefaultExternal; let $userInternalRegex; preloadFixtures(FIXTURE); diff --git a/spec/javascripts/pages/admin/users/new/index_spec.js b/spec/javascripts/pages/admin/users/new/index_spec.js index 5a849f34bc3..3896323eef7 100644 --- a/spec/javascripts/pages/admin/users/new/index_spec.js +++ b/spec/javascripts/pages/admin/users/new/index_spec.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import UserInternalRegexHandler from '~/pages/admin/users/new/index'; describe('UserInternalRegexHandler', () => { - const FIXTURE = 'admin/users/new_with_internal_user_regex.html.raw'; + const FIXTURE = 'admin/users/new_with_internal_user_regex.html'; let $userExternal; let $userEmail; let $warningMessage; diff --git a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js b/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js index 7a8227479d4..1809e92e1d9 100644 --- a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js +++ b/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js @@ -2,10 +2,10 @@ import $ from 'jquery'; import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment'; describe('preserve_url_fragment', () => { - preloadFixtures('sessions/new.html.raw'); + preloadFixtures('sessions/new.html'); beforeEach(() => { - loadFixtures('sessions/new.html.raw'); + loadFixtures('sessions/new.html'); }); it('adds the url fragment to all login and sign up form actions', () => { diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js index 3d2232ff239..95717d659b8 100644 --- a/spec/javascripts/pipelines/graph/action_component_spec.js +++ b/spec/javascripts/pipelines/graph/action_component_spec.js @@ -55,13 +55,16 @@ describe('pipeline graph action component', () => { component.$el.click(); - component - .$nextTick() - .then(() => { - expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete'); - }) - .then(done) - .catch(done.fail); + setTimeout(() => { + component + .$nextTick() + .then(() => { + expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete'); + }) + .catch(done.fail); + + done(); + }, 0); }); }); }); diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index d0b8f877d6f..3240e8e4c1b 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -35,6 +35,7 @@ describe('stage column component', () => { component = mountComponent(StageColumnComponent, { title: 'foo', groups: mockGroups, + hasTriggeredBy: false, }); }); @@ -54,13 +55,14 @@ describe('stage column component', () => { id: 4259, name: '<img src=x onerror=alert(document.domain)>', status: { - icon: 'icon_status_success', + icon: 'status_success', label: 'success', tooltip: '<img src=x onerror=alert(document.domain)>', }, }, ], title: 'test', + hasTriggeredBy: false, }); expect(component.$el.querySelector('.builds-container li').getAttribute('id')).toEqual( diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index ea917b36526..faad49a78b0 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -100,7 +100,8 @@ describe('Pipeline Url Component', () => { latest: true, yaml_errors: true, stuck: true, - merge_request: true, + merge_request_pipeline: true, + detached_merge_request_pipeline: true, }, }, autoDevopsHelpPath: 'foo', @@ -108,15 +109,16 @@ describe('Pipeline Url Component', () => { }).$mount(); expect(component.$el.querySelector('.js-pipeline-url-latest').textContent).toContain('latest'); + expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain( 'yaml invalid', ); - expect(component.$el.querySelector('.js-pipeline-url-mergerequest').textContent).toContain( - 'merge request', - ); - expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck'); + + expect(component.$el.querySelector('.js-pipeline-url-detached').textContent).toContain( + 'detached', + ); }); it('should render a badge for autodevops', () => { diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js index 3c8b8032de8..19ae7860333 100644 --- a/spec/javascripts/pipelines/stage_spec.js +++ b/spec/javascripts/pipelines/stage_spec.js @@ -120,13 +120,15 @@ describe('Pipelines stage component', () => { setTimeout(() => { component.$el.querySelector('.js-ci-action').click(); - component - .$nextTick() - .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); - }) - .then(done) - .catch(done.fail); + setTimeout(() => { + component + .$nextTick() + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable'); + }) + .then(done) + .catch(done.fail); + }, 0); }, 0); }); }); diff --git a/spec/javascripts/pipelines_spec.js b/spec/javascripts/pipelines_spec.js index 6b86f9ea437..6d4d634c575 100644 --- a/spec/javascripts/pipelines_spec.js +++ b/spec/javascripts/pipelines_spec.js @@ -1,10 +1,10 @@ import Pipelines from '~/pipelines'; describe('Pipelines', () => { - preloadFixtures('static/pipeline_graph.html.raw'); + preloadFixtures('static/pipeline_graph.html'); beforeEach(() => { - loadFixtures('static/pipeline_graph.html.raw'); + loadFixtures('static/pipeline_graph.html'); }); it('should be defined', () => { diff --git a/spec/javascripts/project_select_combo_button_spec.js b/spec/javascripts/project_select_combo_button_spec.js index 109a5000f5d..dc85292c23e 100644 --- a/spec/javascripts/project_select_combo_button_spec.js +++ b/spec/javascripts/project_select_combo_button_spec.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import ProjectSelectComboButton from '~/project_select_combo_button'; -const fixturePath = 'static/project_select_combo_button.html.raw'; +const fixturePath = 'static/project_select_combo_button.html'; describe('Project Select Combo Button', function() { preloadFixtures(fixturePath); diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js index 030662b4d90..1eb7cb4bd5b 100644 --- a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js +++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js @@ -53,36 +53,32 @@ describe('GkeProjectIdDropdown', () => { }); it('returns default toggle text', done => - vm - .$nextTick() - .then(() => { - vm.setItem(emptyProjectMock); + setTimeout(() => { + vm.setItem(emptyProjectMock); - expect(vm.toggleText).toBe(LABELS.DEFAULT); - done(); - }) - .catch(done.fail)); + expect(vm.toggleText).toBe(LABELS.DEFAULT); + + done(); + })); it('returns project name if project selected', done => - vm - .$nextTick() - .then(() => { - expect(vm.toggleText).toBe(selectedProjectMock.name); - done(); - }) - .catch(done.fail)); + setTimeout(() => { + vm.isLoading = false; + + expect(vm.toggleText).toBe(selectedProjectMock.name); + + done(); + })); it('returns empty toggle text', done => - vm - .$nextTick() - .then(() => { - vm.$store.commit(SET_PROJECTS, null); - vm.setItem(emptyProjectMock); + setTimeout(() => { + vm.$store.commit(SET_PROJECTS, null); + vm.setItem(emptyProjectMock); - expect(vm.toggleText).toBe(LABELS.EMPTY); - done(); - }) - .catch(done.fail)); + expect(vm.toggleText).toBe(LABELS.EMPTY); + + done(); + })); }); describe('selectItem', () => { diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js index 94e2f959d46..dca3e1553b9 100644 --- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js +++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js @@ -5,7 +5,7 @@ import PANEL_STATE from '~/prometheus_metrics/constants'; import { metrics, missingVarMetrics } from './mock_data'; describe('PrometheusMetrics', () => { - const FIXTURE = 'services/prometheus/prometheus_service.html.raw'; + const FIXTURE = 'services/prometheus/prometheus_service.html'; preloadFixtures(FIXTURE); beforeEach(() => { diff --git a/spec/javascripts/read_more_spec.js b/spec/javascripts/read_more_spec.js index b1af0f80a50..d1d01272403 100644 --- a/spec/javascripts/read_more_spec.js +++ b/spec/javascripts/read_more_spec.js @@ -1,7 +1,7 @@ import initReadMore from '~/read_more'; describe('Read more click-to-expand functionality', () => { - const fixtureName = 'projects/overview.html.raw'; + const fixtureName = 'projects/overview.html'; preloadFixtures(fixtureName); diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 67118ac03a5..76a17e6fb31 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -99,7 +99,7 @@ describe('Registry List', () => { it('should render a loading spinner', done => { Vue.nextTick(() => { - expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null); + expect(vm.$el.querySelector('.spinner')).not.toBe(null); done(); }); }); diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js index 69767d9cf1c..a17494966a3 100644 --- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js +++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js @@ -61,7 +61,7 @@ describe('Grouped Test Reports App', () => { it('renders success summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.spinner')).not.toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary results are being parsed', ); @@ -81,7 +81,7 @@ describe('Grouped Test Reports App', () => { it('renders failed summary text + new badge', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 failed test results out of 11 total tests', ); @@ -109,7 +109,7 @@ describe('Grouped Test Reports App', () => { it('renders summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 failed test results and 2 fixed test results out of 11 total tests', ); @@ -137,7 +137,7 @@ describe('Grouped Test Reports App', () => { it('renders summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 fixed test results out of 11 total tests', ); @@ -190,7 +190,7 @@ describe('Grouped Test Reports App', () => { }); it('renders loading summary text with loading icon', done => { - expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.spinner')).not.toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary results are being parsed', ); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 992e17978c1..9565e3ce546 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -23,7 +23,7 @@ const assertSidebarState = function(state) { describe('RightSidebar', function() { describe('fixture tests', () => { - const fixtureName = 'issues/open-issue.html.raw'; + const fixtureName = 'issues/open-issue.html'; preloadFixtures(fixtureName); loadJSONFixtures('todos/todos.json'); let mock; diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 7a4ca587313..ce7fa7a52ae 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -126,9 +126,9 @@ describe('Search autocomplete dropdown', () => { expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created"); }; - preloadFixtures('static/search_autocomplete.html.raw'); + preloadFixtures('static/search_autocomplete.html'); beforeEach(function() { - loadFixtures('static/search_autocomplete.html.raw'); + loadFixtures('static/search_autocomplete.html'); window.gon = {}; window.gon.current_user_id = userId; diff --git a/spec/javascripts/search_spec.js b/spec/javascripts/search_spec.js index 40bdbac7451..32f60508fa3 100644 --- a/spec/javascripts/search_spec.js +++ b/spec/javascripts/search_spec.js @@ -3,7 +3,7 @@ import Api from '~/api'; import Search from '~/pages/search/show/search'; describe('Search', () => { - const fixturePath = 'search/show.html.raw'; + const fixturePath = 'search/show.html'; const searchTerm = 'some search'; const fillDropdownInput = dropdownSelector => { const dropdownElement = document.querySelector(dropdownSelector).parentNode; diff --git a/spec/javascripts/settings_panels_spec.js b/spec/javascripts/settings_panels_spec.js index 3b681a9ff28..2c5d91a45bc 100644 --- a/spec/javascripts/settings_panels_spec.js +++ b/spec/javascripts/settings_panels_spec.js @@ -2,10 +2,10 @@ import $ from 'jquery'; import initSettingsPanels from '~/settings_panels'; describe('Settings Panels', () => { - preloadFixtures('groups/edit.html.raw'); + preloadFixtures('groups/edit.html'); beforeEach(() => { - loadFixtures('groups/edit.html.raw'); + loadFixtures('groups/edit.html'); }); describe('initSettingsPane', () => { diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js index 3ca6ecaa938..df7012bb659 100644 --- a/spec/javascripts/shortcuts_spec.js +++ b/spec/javascripts/shortcuts_spec.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import Shortcuts from '~/behaviors/shortcuts/shortcuts'; describe('Shortcuts', () => { - const fixtureName = 'snippets/show.html.raw'; + const fixtureName = 'snippets/show.html'; const createEvent = (type, target) => $.Event(type, { target, diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js index eced4925489..57b16b12cb0 100644 --- a/spec/javascripts/sidebar/assignees_spec.js +++ b/spec/javascripts/sidebar/assignees_spec.js @@ -210,6 +210,19 @@ describe('Assignee component', () => { expect(component.$el.querySelector('.user-list-more')).toBe(null); }); + it('sets tooltip container to body', () => { + const users = UsersMockHelper.createNumberRandomUsers(2); + component = new AssigneeComponent({ + propsData: { + rootPath: 'http://localhost:3000', + users, + editable: true, + }, + }).$mount(); + + expect(component.$el.querySelector('.user-link').getAttribute('data-container')).toBe('body'); + }); + it('Shows the "show-less" assignees label', done => { const users = UsersMockHelper.createNumberRandomUsers(6); component = new AssigneeComponent({ diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js index 3f0f67d71ca..016f5e033a5 100644 --- a/spec/javascripts/sidebar/sidebar_assignees_spec.js +++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js @@ -11,12 +11,12 @@ describe('sidebar assignees', () => { let vm; let mediator; let sidebarAssigneesEl; - preloadFixtures('issues/open-issue.html.raw'); + preloadFixtures('issues/open-issue.html'); beforeEach(() => { Vue.http.interceptors.push(Mock.sidebarMockInterceptor); - loadFixtures('issues/open-issue.html.raw'); + loadFixtures('issues/open-issue.html'); mediator = new SidebarMediator(Mock.mediator); spyOn(mediator, 'saveAssignees').and.callThrough(); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js index 52da6a79939..ef5c774736b 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js +++ b/spec/javascripts/signin_tabs_memoizer_spec.js @@ -2,7 +2,7 @@ import AccessorUtilities from '~/lib/utils/accessor'; import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer'; describe('SigninTabsMemoizer', () => { - const fixtureTemplate = 'static/signin_tabs.html.raw'; + const fixtureTemplate = 'static/signin_tabs.html'; const tabSelector = 'ul.new-session-tabs'; const currentTabKey = 'current_signin_tab'; let memo; diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js index 69e43274250..802f54f6a7e 100644 --- a/spec/javascripts/todos_spec.js +++ b/spec/javascripts/todos_spec.js @@ -3,11 +3,11 @@ import Todos from '~/pages/dashboard/todos/index/todos'; import '~/lib/utils/common_utils'; describe('Todos', () => { - preloadFixtures('todos/todos.html.raw'); + preloadFixtures('todos/todos.html'); let todoItem; beforeEach(() => { - loadFixtures('todos/todos.html.raw'); + loadFixtures('todos/todos.html'); todoItem = document.querySelector('.todos-list .todo'); return new Todos(); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index ddb09811dda..8f9cb270729 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -4,10 +4,10 @@ import 'vendor/u2f'; import MockU2FDevice from './mock_u2f_device'; describe('U2FAuthenticate', function() { - preloadFixtures('u2f/authenticate.html.raw'); + preloadFixtures('u2f/authenticate.html'); beforeEach(() => { - loadFixtures('u2f/authenticate.html.raw'); + loadFixtures('u2f/authenticate.html'); this.u2fDevice = new MockU2FDevice(); this.container = $('#js-authenticate-u2f'); this.component = new U2FAuthenticate( diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 261db3d66d7..a75ceca9f4c 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -4,10 +4,10 @@ import 'vendor/u2f'; import MockU2FDevice from './mock_u2f_device'; describe('U2FRegister', function() { - preloadFixtures('u2f/register.html.raw'); + preloadFixtures('u2f/register.html'); beforeEach(done => { - loadFixtures('u2f/register.html.raw'); + loadFixtures('u2f/register.html'); this.u2fDevice = new MockU2FDevice(); this.container = $('#js-register-u2f'); this.component = new U2FRegister(this.container, $('#js-register-u2f-templates'), {}, 'token'); diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js index b174a51c1a0..c0d5ee9c446 100644 --- a/spec/javascripts/user_popovers_spec.js +++ b/spec/javascripts/user_popovers_spec.js @@ -2,7 +2,7 @@ import initUserPopovers from '~/user_popovers'; import UsersCache from '~/lib/utils/users_cache'; describe('User Popovers', () => { - const fixtureTemplate = 'merge_requests/diff_comment.html.raw'; + const fixtureTemplate = 'merge_requests/diff_comment.html'; preloadFixtures(fixtureTemplate); const selector = '.js-user-link'; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index d905bbe4040..de213210cfc 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue'; import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import { trimText } from 'spec/helpers/vue_component_helper'; import mockData from '../mock_data'; describe('MRWidgetPipeline', () => { @@ -123,7 +124,7 @@ describe('MRWidgetPipeline', () => { describe('without commit path', () => { beforeEach(() => { - const mockCopy = Object.assign({}, mockData); + const mockCopy = JSON.parse(JSON.stringify(mockData)); delete mockCopy.pipeline.commit; vm = mountComponent(Component, { @@ -164,7 +165,7 @@ describe('MRWidgetPipeline', () => { describe('without coverage', () => { it('should not render a coverage', () => { - const mockCopy = Object.assign({}, mockData); + const mockCopy = JSON.parse(JSON.stringify(mockData)); delete mockCopy.pipeline.coverage; vm = mountComponent(Component, { @@ -180,7 +181,7 @@ describe('MRWidgetPipeline', () => { describe('without a pipeline graph', () => { it('should not render a pipeline graph', () => { - const mockCopy = Object.assign({}, mockData); + const mockCopy = JSON.parse(JSON.stringify(mockData)); delete mockCopy.pipeline.details.stages; vm = mountComponent(Component, { @@ -193,5 +194,81 @@ describe('MRWidgetPipeline', () => { expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null); }); }); + + describe('without pipeline.merge_request', () => { + it('should render info that includes the commit and branch details', () => { + const mockCopy = JSON.parse(JSON.stringify(mockData)); + delete mockCopy.pipeline.merge_request; + const { pipeline } = mockCopy; + + vm = mountComponent(Component, { + pipeline, + hasCi: true, + ciStatus: 'success', + troubleshootingDocsPath: 'help', + sourceBranchLink: mockCopy.source_branch_link, + }); + + const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${ + pipeline.commit.short_id + } on ${mockCopy.source_branch_link}`; + + const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); + + expect(actual).toBe(expected); + }); + }); + + describe('with pipeline.merge_request and flags.merge_request_pipeline', () => { + it('should render info that includes the commit, MR, source branch, and target branch details', () => { + const mockCopy = JSON.parse(JSON.stringify(mockData)); + const { pipeline } = mockCopy; + pipeline.flags.merge_request_pipeline = true; + pipeline.flags.detached_merge_request_pipeline = false; + + vm = mountComponent(Component, { + pipeline, + hasCi: true, + ciStatus: 'success', + troubleshootingDocsPath: 'help', + sourceBranchLink: mockCopy.source_branch_link, + }); + + const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${ + pipeline.commit.short_id + } on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch} into ${ + pipeline.merge_request.target_branch + }`; + + const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); + + expect(actual).toBe(expected); + }); + }); + + describe('with pipeline.merge_request and flags.detached_merge_request_pipeline', () => { + it('should render info that includes the commit, MR, and source branch details', () => { + const mockCopy = JSON.parse(JSON.stringify(mockData)); + const { pipeline } = mockCopy; + pipeline.flags.merge_request_pipeline = false; + pipeline.flags.detached_merge_request_pipeline = true; + + vm = mountComponent(Component, { + pipeline, + hasCi: true, + ciStatus: 'success', + troubleshootingDocsPath: 'help', + sourceBranchLink: mockCopy.source_branch_link, + }); + + const expected = `Pipeline #${pipeline.id} ${pipeline.details.status.label} for ${ + pipeline.commit.short_id + } on !${pipeline.merge_request.iid} with ${pipeline.merge_request.source_branch}`; + + const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText); + + expect(actual).toBe(expected); + }); + }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js index a0a336ae604..f622f52a7b9 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -18,7 +18,7 @@ describe('MR widget status icon component', () => { it('renders loading icon', () => { vm = mountComponent(Component, { status: 'loading' }); - expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('spinner'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js index eb4fa0df727..d93badf8cd3 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -38,7 +38,7 @@ describe('MRWidgetAutoMergeFailed', () => { Vue.nextTick(() => { expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); - expect(vm.$el.querySelector('button i').classList).toContain('fa-spinner'); + expect(vm.$el.querySelector('button .loading-container span').classList).toContain('spinner'); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js index 7da27bb8890..96e512d222a 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -20,7 +20,7 @@ describe('MRWidgetChecking', () => { }); it('renders loading icon', () => { - expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('spinner'); }); it('renders information about merging', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js index 3229ddd5e27..780bed1bf69 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -120,7 +120,7 @@ describe('MRWidgetFailedToMerge', () => { it('renders given error', () => { expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual( - 'Merge error happened.', + 'Merge error happened', ); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 30659ad16f3..368c997d318 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -415,7 +415,7 @@ describe('ReadyToMerge', () => { }); beforeEach(() => { - loadFixtures('merge_requests/merge_request_of_current_user.html.raw'); + loadFixtures('merge_requests/merge_request_of_current_user.html'); }); it('should call start and stop polling when MR merged', done => { diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 6ef07f81705..7ab203a6011 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -134,6 +134,8 @@ export default { yaml_errors: false, retryable: true, cancelable: false, + merge_request_pipeline: false, + detached_merge_request_pipeline: true, }, ref: { name: 'daaaa', @@ -141,6 +143,15 @@ export default { tag: false, branch: true, }, + merge_request: { + iid: 1, + path: '/root/detached-merge-request-pipelines/merge_requests/1', + title: 'Update README.md', + source_branch: 'feature-1', + source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1', + target_branch: 'master', + target_branch_path: '/root/detached-merge-request-pipelines/branches/master', + }, commit: { id: '104096c51715e12e7ae41f9333e9fa35b73f385d', short_id: '104096c5', diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 18fcdf7ede1..f2e20f626b5 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -61,7 +61,7 @@ describe('Commit component', () => { }); it('should render a tag icon if it represents a tag', () => { - expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag'); + expect(component.$el.querySelector('.icon-container svg.ic-tag')).not.toBeNull(); }); it('should render a link to the ref url', () => { @@ -143,4 +143,92 @@ describe('Commit component', () => { ); }); }); + + describe('When commit ref is provided, but merge ref is not', () => { + it('should render the commit ref', () => { + props = { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', + title: null, + author: {}, + }; + + component = mountComponent(CommitComponent, props); + const refEl = component.$el.querySelector('.ref-name'); + + expect(refEl.textContent).toContain('master'); + + expect(refEl.href).toBe(props.commitRef.ref_url); + + expect(refEl.getAttribute('data-original-title')).toBe(props.commitRef.name); + + expect(component.$el.querySelector('.icon-container .ic-branch')).not.toBeNull(); + }); + }); + + describe('When both commit and merge ref are provided', () => { + it('should render the merge ref', () => { + props = { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + mergeRequestRef: { + iid: 1234, + path: 'https://example.com/path/to/mr', + title: 'Test MR', + }, + shortSha: 'b7836edd', + title: null, + author: {}, + }; + + component = mountComponent(CommitComponent, props); + const refEl = component.$el.querySelector('.ref-name'); + + expect(refEl.textContent).toContain('1234'); + + expect(refEl.href).toBe(props.mergeRequestRef.path); + + expect(refEl.getAttribute('data-original-title')).toBe(props.mergeRequestRef.title); + + expect(component.$el.querySelector('.icon-container .ic-git-merge')).not.toBeNull(); + }); + }); + + describe('When showRefInfo === false', () => { + it('should not render any ref info', () => { + props = { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: + 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + mergeRequestRef: { + iid: 1234, + path: '/path/to/mr', + title: 'Test MR', + }, + shortSha: 'b7836edd', + title: null, + author: {}, + showRefInfo: false, + }; + + component = mountComponent(CommitComponent, props); + + expect(component.$el.querySelector('.ref-name')).toBeNull(); + }); + }); }); diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js index 34c9b35e02a..5bea8c43da3 100644 --- a/spec/javascripts/vue_shared/components/file_icon_spec.js +++ b/spec/javascripts/vue_shared/components/file_icon_spec.js @@ -70,12 +70,9 @@ describe('File Icon component', () => { loading: true, }); - const { classList } = vm.$el.querySelector('i'); + const { classList } = vm.$el.querySelector('.loading-container span'); - expect(classList.contains('fa')).toEqual(true); - expect(classList.contains('fa-spin')).toEqual(true); - expect(classList.contains('fa-spinner')).toEqual(true); - expect(classList.contains('fa-1x')).toEqual(true); + expect(classList.contains('spinner')).toEqual(true); }); it('should add a special class and a size class', () => { diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index 7a741bdc067..a9c1a67b39b 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -88,7 +88,7 @@ describe('Header CI Component', () => { vm.actions[0].isLoading = true; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); + expect(vm.$el.querySelector('.btn .spinner').getAttribute('style')).toBeFalsy(); done(); }); }); diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js index 79e0e756a7a..02d73e1849a 100644 --- a/spec/javascripts/vue_shared/components/markdown/field_spec.js +++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js @@ -5,7 +5,7 @@ import fieldComponent from '~/vue_shared/components/markdown/field.vue'; function assertMarkdownTabs(isWrite, writeLink, previewLink, vm) { expect(writeLink.parentNode.classList.contains('active')).toEqual(isWrite); expect(previewLink.parentNode.classList.contains('active')).toEqual(!isWrite); - expect(vm.$el.querySelector('.md-preview').style.display).toEqual(isWrite ? 'none' : ''); + expect(vm.$el.querySelector('.md-preview-holder').style.display).toEqual(isWrite ? 'none' : ''); } describe('Markdown field component', () => { @@ -80,7 +80,9 @@ describe('Markdown field component', () => { previewLink.click(); Vue.nextTick(() => { - expect(vm.$el.querySelector('.md-preview').textContent.trim()).toContain('Loading…'); + expect(vm.$el.querySelector('.md-preview-holder').textContent.trim()).toContain( + 'Loading…', + ); done(); }); @@ -90,7 +92,7 @@ describe('Markdown field component', () => { previewLink.click(); setTimeout(() => { - expect(vm.$el.querySelector('.md-preview').innerHTML).toContain( + expect(vm.$el.querySelector('.md-preview-holder').innerHTML).toContain( '<p>markdown preview</p>', ); diff --git a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js index e8b41e8eeff..852558a83bc 100644 --- a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js @@ -17,7 +17,7 @@ const DEFAULT_PROPS = { const UserPopover = Vue.extend(userPopover); describe('User Popover Component', () => { - const fixtureTemplate = 'merge_requests/diff_comment.html.raw'; + const fixtureTemplate = 'merge_requests/diff_comment.html'; preloadFixtures(fixtureTemplate); let vm; diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index e5f1e6ae937..8f662c71c7a 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -6,7 +6,7 @@ import ZenMode from '~/zen_mode'; describe('ZenMode', () => { let zen; let dropzoneForElementSpy; - const fixtureName = 'snippets/show.html.raw'; + const fixtureName = 'snippets/show.html'; preloadFixtures(fixtureName); diff --git a/spec/lib/backup/uploads_spec.rb b/spec/lib/backup/uploads_spec.rb new file mode 100644 index 00000000000..544d3754c0f --- /dev/null +++ b/spec/lib/backup/uploads_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Backup::Uploads do + let(:progress) { StringIO.new } + subject(:backup) { described_class.new(progress) } + + describe '#initialize' do + it 'uses the correct upload dir' do + Dir.mktmpdir do |tmpdir| + FileUtils.mkdir_p("#{tmpdir}/uploads") + + allow(Gitlab.config.uploads).to receive(:storage_path) { tmpdir } + + expect(backup.app_files_dir).to eq("#{tmpdir}/uploads") + end + end + end +end diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index 55c41e55437..72dfd6ff9ea 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -30,6 +30,23 @@ describe Banzai::Filter::MergeRequestReferenceFilter do end end + describe 'all references' do + let(:doc) { reference_filter(merge.to_reference) } + let(:tag_el) { doc.css('a').first } + + it 'adds merge request iid' do + expect(tag_el["data-iid"]).to eq(merge.iid.to_s) + end + + it 'adds project data attribute with project id' do + expect(tag_el["data-project-path"]).to eq(project.full_path) + end + + it 'does not add `has-tooltip` class' do + expect(tag_el["class"]).not_to include('has-tooltip') + end + end + context 'internal reference' do let(:reference) { merge.to_reference } @@ -57,9 +74,9 @@ describe Banzai::Filter::MergeRequestReferenceFilter do expect(reference_filter(act).to_html).to eq exp end - it 'includes a title attribute' do + it 'has no title' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('title')).to eq merge.title + expect(doc.css('a').first.attr('title')).to eq "" end it 'escapes the title attribute' do @@ -69,9 +86,9 @@ describe Banzai::Filter::MergeRequestReferenceFilter do expect(doc.text).to eq "Merge #{reference}" end - it 'includes default classes' do + it 'includes default classes, without tooltip' do doc = reference_filter("Merge #{reference}") - expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request has-tooltip' + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request' end it 'includes a data-project attribute' do diff --git a/spec/lib/banzai/filter/output_safety_spec.rb b/spec/lib/banzai/filter/output_safety_spec.rb new file mode 100644 index 00000000000..5ffe591c9a4 --- /dev/null +++ b/spec/lib/banzai/filter/output_safety_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Banzai::Filter::OutputSafety do + subject do + Class.new do + include Banzai::Filter::OutputSafety + end.new + end + + let(:content) { '<pre><code>foo</code></pre>' } + + context 'when given HTML is safe' do + let(:html) { content.html_safe } + + it 'returns safe HTML' do + expect(subject.escape_once(html)).to eq(html) + end + end + + context 'when given HTML is not safe' do + let(:html) { content } + + it 'returns escaped HTML' do + expect(subject.escape_once(html)).to eq(ERB::Util.html_escape_once(html)) + end + end +end diff --git a/spec/lib/banzai/filter/suggestion_filter_spec.rb b/spec/lib/banzai/filter/suggestion_filter_spec.rb index b13c90b54bd..af6f002fa30 100644 --- a/spec/lib/banzai/filter/suggestion_filter_spec.rb +++ b/spec/lib/banzai/filter/suggestion_filter_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' describe Banzai::Filter::SuggestionFilter do include FilterSpecHelper - let(:input) { "<pre class='code highlight js-syntax-highlight suggestion'><code>foo\n</code></pre>" } + let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion"><code>foo\n</code></pre>) } let(:default_context) do { suggestions_filter_enabled: true } end @@ -23,4 +23,35 @@ describe Banzai::Filter::SuggestionFilter do expect(result[:class]).to be_nil end + + context 'multi-line suggestions' do + let(:data_attr) { Banzai::Filter::SyntaxHighlightFilter::LANG_PARAMS_ATTR } + let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion" #{data_attr}="-3+2"><code>foo\n</code></pre>) } + + context 'feature disabled' do + before do + stub_feature_flags(multi_line_suggestions: false) + end + + it 'removes data-lang-params if it matches a multi-line suggestion param' do + doc = filter(input, default_context) + pre = doc.css('pre').first + + expect(pre[data_attr]).to be_nil + end + end + + context 'feature enabled' do + before do + stub_feature_flags(multi_line_suggestions: true) + end + + it 'keeps data-lang-params' do + doc = filter(input, default_context) + pre = doc.css('pre').first + + expect(pre[data_attr]).to eq('-3+2') + end + end + end end diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb index ef52c572898..05057789cc1 100644 --- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb +++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb @@ -45,7 +45,10 @@ describe Banzai::Filter::SyntaxHighlightFilter do end context "languages that should be passed through" do - %w(math mermaid plantuml).each do |lang| + let(:delimiter) { described_class::PARAMS_DELIMITER } + let(:data_attr) { described_class::LANG_PARAMS_ATTR } + + %w(math mermaid plantuml suggestion).each do |lang| context "when #{lang} is specified" do it "highlights as plaintext but with the correct language attribute and class" do result = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>}) @@ -55,6 +58,33 @@ describe Banzai::Filter::SyntaxHighlightFilter do include_examples "XSS prevention", lang end + + context "when #{lang} has extra params" do + let(:lang_params) { 'foo-bar-kux' } + + it "includes data-lang-params tag with extra information" do + result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}">This is a test</code></pre>}) + + expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) + end + + include_examples "XSS prevention", lang + include_examples "XSS prevention", + "#{lang}#{described_class::PARAMS_DELIMITER}<script>alert(1)</script>" + include_examples "XSS prevention", + "#{lang}#{described_class::PARAMS_DELIMITER}<script>alert(1)</script>" + end + end + + context 'when multiple param delimiters are used' do + let(:lang) { 'suggestion' } + let(:lang_params) { '-1+10' } + + it "delimits on the first appearence" do + result = filter(%{<pre><code lang="#{lang}#{delimiter}#{lang_params}#{delimiter}more-things">This is a test</code></pre>}) + + expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{lang}" lang="#{lang}" #{data_attr}="#{lang_params}#{delimiter}more-things" v-pre="true"><code><span id="LC1" class="line" lang="#{lang}">This is a test</span></code></pre>}) + end end end diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index dcbd12fe190..b765c265e69 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -207,6 +207,7 @@ describe Gitlab::Auth::OAuth::User do before do allow(ldap_user).to receive(:uid) { uid } allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:name) { 'John Doe' } allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] } allow(ldap_user).to receive(:dn) { dn } end @@ -221,6 +222,7 @@ describe Gitlab::Auth::OAuth::User do it "creates a user with dual LDAP and omniauth identities" do expect(gl_user).to be_valid expect(gl_user.username).to eql uid + expect(gl_user.name).to eql 'John Doe' expect(gl_user.email).to eql 'johndoe@example.com' expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } @@ -232,11 +234,13 @@ describe Gitlab::Auth::OAuth::User do ) end - it "has email set as synced" do + it "has name and email set as synced" do + expect(gl_user.user_synced_attributes_metadata.name_synced).to be_truthy expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy end - it "has email set as read-only" do + it "has name and email set as read-only" do + expect(gl_user.read_only_attribute?(:name)).to be_truthy expect(gl_user.read_only_attribute?(:email)).to be_truthy end @@ -246,7 +250,7 @@ describe Gitlab::Auth::OAuth::User do end context "and LDAP user has an account already" do - let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } + let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } it "adds the omniauth identity to the LDAP account" do allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) @@ -254,6 +258,7 @@ describe Gitlab::Auth::OAuth::User do expect(gl_user).to be_valid expect(gl_user.username).to eql 'john' + expect(gl_user.name).to eql 'John Doe' expect(gl_user.email).to eql 'john@example.com' expect(gl_user.identities.length).to be 2 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } diff --git a/spec/lib/gitlab/authorized_keys_spec.rb b/spec/lib/gitlab/authorized_keys_spec.rb new file mode 100644 index 00000000000..42bc509eeef --- /dev/null +++ b/spec/lib/gitlab/authorized_keys_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::AuthorizedKeys do + let(:logger) { double('logger').as_null_object } + + subject { described_class.new(logger) } + + describe '#add_key' do + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it "adds a line at the end of the file and strips trailing garbage" do + auth_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E" + + expect(logger).to receive(:info).with('Adding key (key-741): ssh-rsa AAAAB3NzaDAxx2E') + expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E trailing garbage')) + .to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line}\n") + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it 'creates the file' do + expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E')).to be_truthy + expect(File.exist?(tmp_authorized_keys_path)).to be_truthy + end + end + end + + describe '#batch_add_keys' do + let(:keys) do + [ + double(shell_id: 'key-12', key: 'ssh-dsa ASDFASGADG trailing garbage'), + double(shell_id: 'key-123', key: 'ssh-rsa GFDGDFSGSDFG') + ] + end + + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it "adds lines at the end of the file" do + auth_line1 = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-12\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-dsa ASDFASGADG" + auth_line2 = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-123\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa GFDGDFSGSDFG" + + expect(logger).to receive(:info).with('Adding key (key-12): ssh-dsa ASDFASGADG') + expect(logger).to receive(:info).with('Adding key (key-123): ssh-rsa GFDGDFSGSDFG') + expect(subject.batch_add_keys(keys)).to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line1}\n#{auth_line2}\n") + end + + context "invalid key" do + let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } + + it "doesn't add keys" do + expect(subject.batch_add_keys(keys)).to be_falsey + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n") + end + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it 'creates the file' do + expect(subject.batch_add_keys(keys)).to be_truthy + expect(File.exist?(tmp_authorized_keys_path)).to be_truthy + end + end + end + + describe '#rm_key' do + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it "removes the right line" do + other_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" + delete_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" + erased_line = delete_line.gsub(/./, '#') + File.open(tmp_authorized_keys_path, 'a') do |auth_file| + auth_file.puts delete_line + auth_file.puts other_line + end + + expect(logger).to receive(:info).with('Removing key (key-741)') + expect(subject.rm_key('key-741')).to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{erased_line}\n#{other_line}\n") + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it 'returns false' do + expect(subject.rm_key('key-741')).to be_falsey + end + end + end + + describe '#clear' do + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture + end + + after do + delete_authorized_keys_file + end + + it "returns true" do + expect(subject.clear).to be_truthy + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it "still returns true" do + expect(subject.clear).to be_truthy + end + end + end + + describe '#list_key_ids' do + context 'authorized_keys file exists' do + before do + create_authorized_keys_fixture( + existing_content: + "key-1\tssh-dsa AAA\nkey-2\tssh-rsa BBB\nkey-3\tssh-rsa CCC\nkey-9000\tssh-rsa DDD\n" + ) + end + + after do + delete_authorized_keys_file + end + + it 'returns array of key IDs' do + expect(subject.list_key_ids).to eq([1, 2, 3, 9000]) + end + end + + context 'authorized_keys file does not exist' do + before do + delete_authorized_keys_file + end + + it 'returns an empty array' do + expect(subject.list_key_ids).to be_empty + end + end + end + + def create_authorized_keys_fixture(existing_content: 'existing content') + FileUtils.mkdir_p(File.dirname(tmp_authorized_keys_path)) + File.open(tmp_authorized_keys_path, 'w') { |file| file.puts(existing_content) } + end + + def delete_authorized_keys_file + File.delete(tmp_authorized_keys_path) if File.exist?(tmp_authorized_keys_path) + end + + def tmp_authorized_keys_path + Gitlab.config.gitlab_shell.authorized_keys_file + end +end diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb new file mode 100644 index 00000000000..4a81a37d341 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Gitlab::BackgroundMigration::PopulateMergeRequestAssigneesTable, :migration, schema: 20190315191339 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + + let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') } + let(:user_2) { users.create!(email: 'test2@example.com', projects_limit: 100, username: 'test') } + let(:user_3) { users.create!(email: 'test3@example.com', projects_limit: 100, username: 'test') } + + let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:merge_requests) { table(:merge_requests) } + let(:merge_request_assignees) { table(:merge_request_assignees) } + + def create_merge_request(id, params = {}) + params.merge!(id: id, + target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: "mr name#{id}") + + merge_requests.create(params) + end + + describe '#perform' do + it 'creates merge_request_assignees rows according to merge_requests' do + create_merge_request(2, assignee_id: user.id) + create_merge_request(3, assignee_id: user_2.id) + create_merge_request(4, assignee_id: user_3.id) + # Test filtering already migrated row + merge_request_assignees.create!(merge_request_id: 2, user_id: user_3.id) + + subject.perform(1, 4) + + rows = merge_request_assignees.order(:id).map { |row| row.attributes.slice('merge_request_id', 'user_id') } + existing_rows = [ + { 'merge_request_id' => 2, 'user_id' => user_3.id } + ] + created_rows = [ + { 'merge_request_id' => 3, 'user_id' => user_2.id }, + { 'merge_request_id' => 4, 'user_id' => user_3.id } + ] + expected_rows = existing_rows + created_rows + + expect(rows.size).to eq(expected_rows.size) + expected_rows.each do |expected_row| + expect(rows).to include(expected_row) + end + end + end +end diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/badge/pipeline/template_spec.rb index 20fa4f879c3..bcef0b7e120 100644 --- a/spec/lib/gitlab/badge/pipeline/template_spec.rb +++ b/spec/lib/gitlab/badge/pipeline/template_spec.rb @@ -59,6 +59,16 @@ describe Gitlab::Badge::Pipeline::Template do end end + context 'when status is preparing' do + before do + allow(badge).to receive(:status).and_return('preparing') + end + + it 'has expected color' do + expect(template.value_color).to eq '#dfb317' + end + end + context 'when status is unknown' do before do allow(badge).to receive(:status).and_return('unknown') diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb index 12beeecd470..8d5ab27a17c 100644 --- a/spec/lib/gitlab/checks/branch_check_spec.rb +++ b/spec/lib/gitlab/checks/branch_check_spec.rb @@ -108,64 +108,86 @@ describe Gitlab::Checks::BranchCheck do end context 'protected branch creation feature is enabled' do - context 'user is not allowed to create protected branches' do + context 'user can push to branch' do before do allow(user_access) - .to receive(:can_merge_to_branch?) + .to receive(:can_push_to_branch?) .with('feature') - .and_return(false) + .and_return(true) end - it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.') + it 'does not raise an error' do + expect { subject.validate! }.not_to raise_error end end - context 'user is allowed to create protected branches' do + context 'user cannot push to branch' do before do allow(user_access) - .to receive(:can_merge_to_branch?) + .to receive(:can_push_to_branch?) .with('feature') - .and_return(true) - - allow(project.repository) - .to receive(:branch_names_contains_sha) - .with(newrev) - .and_return(['branch']) + .and_return(false) end - context "newrev isn't in any protected branches" do + context 'user cannot merge to branch' do before do - allow(ProtectedBranch) - .to receive(:any_protected?) - .with(project, ['branch']) + allow(user_access) + .to receive(:can_merge_to_branch?) + .with('feature') .and_return(false) end it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.') + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.') end end - context 'newrev is included in a protected branch' do + context 'user can merge to branch' do before do - allow(ProtectedBranch) - .to receive(:any_protected?) - .with(project, ['branch']) + allow(user_access) + .to receive(:can_merge_to_branch?) + .with('feature') .and_return(true) + + allow(project.repository) + .to receive(:branch_names_contains_sha) + .with(newrev) + .and_return(['branch']) end - context 'via web interface' do - let(:protocol) { 'web' } + context "newrev isn't in any protected branches" do + before do + allow(ProtectedBranch) + .to receive(:any_protected?) + .with(project, ['branch']) + .and_return(false) + end - it 'allows branch creation' do - expect { subject.validate! }.not_to raise_error + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.') end end - context 'via SSH' do - it 'raises an error' do - expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.') + context 'newrev is included in a protected branch' do + before do + allow(ProtectedBranch) + .to receive(:any_protected?) + .with(project, ['branch']) + .and_return(true) + end + + context 'via web interface' do + let(:protocol) { 'web' } + + it 'allows branch creation' do + expect { subject.validate! }.not_to raise_error + end + end + + context 'via SSH' do + it 'raises an error' do + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.') + end end end end diff --git a/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb new file mode 100644 index 00000000000..5187f99a441 --- /dev/null +++ b/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Prerequisite::Factory do + let(:build) { create(:ci_build) } + + describe '.for_build' do + let(:kubernetes_namespace) do + instance_double( + Gitlab::Ci::Build::Prerequisite::KubernetesNamespace, + unmet?: unmet) + end + + subject { described_class.new(build).unmet } + + before do + expect(Gitlab::Ci::Build::Prerequisite::KubernetesNamespace) + .to receive(:new).with(build).and_return(kubernetes_namespace) + end + + context 'prerequisite is unmet' do + let(:unmet) { true } + + it { is_expected.to eq [kubernetes_namespace] } + end + + context 'prerequisite is met' do + let(:unmet) { false } + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb new file mode 100644 index 00000000000..62dcd80fad7 --- /dev/null +++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do + let(:build) { create(:ci_build) } + + describe '#unmet?' do + subject { described_class.new(build).unmet? } + + context 'build has no deployment' do + before do + expect(build.deployment).to be_nil + end + + it { is_expected.to be_falsey } + end + + context 'build has a deployment' do + let!(:deployment) { create(:deployment, deployable: build) } + + context 'and a cluster to deploy to' do + let(:cluster) { create(:cluster, projects: [build.project]) } + + before do + allow(build.deployment).to receive(:cluster).and_return(cluster) + end + + it { is_expected.to be_truthy } + + context 'and a namespace is already created for this project' do + let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: build.project) } + + it { is_expected.to be_falsey } + end + end + + context 'and no cluster to deploy to' do + before do + expect(deployment.cluster).to be_nil + end + + it { is_expected.to be_falsey } + end + end + end + + describe '#complete!' do + let!(:deployment) { create(:deployment, deployable: build) } + let(:service) { double(execute: true) } + + subject { described_class.new(build).complete! } + + context 'completion is required' do + let(:cluster) { create(:cluster, projects: [build.project]) } + + before do + allow(build.deployment).to receive(:cluster).and_return(cluster) + end + + it 'creates a kubernetes namespace' do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService) + .to receive(:new) + .with(cluster: cluster, kubernetes_namespace: instance_of(Clusters::KubernetesNamespace)) + .and_return(service) + + expect(service).to receive(:execute).once + + subject + end + end + + context 'completion is not required' do + before do + expect(deployment.cluster).to be_nil + end + + it 'does not create a namespace' do + expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new) + + subject + end + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index dab0fb51bcc..5181e9c1583 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -48,6 +48,24 @@ describe Gitlab::Ci::Pipeline::Chain::Command do end end + describe '#merge_request_ref_exists?' do + subject { command.merge_request_ref_exists? } + + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + + context 'for existing merge request ref' do + let(:origin_ref) { merge_request.ref_path } + + it { is_expected.to eq(true) } + end + + context 'for branch ref' do + let(:origin_ref) { merge_request.source_branch } + + it { is_expected.to eq(false) } + end + end + describe '#ref' do subject { command.ref } diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb index 8ba56d73838..7d750877d09 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb @@ -10,12 +10,33 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do let(:command) do Gitlab::Ci::Pipeline::Chain::Command.new( - project: project, current_user: user, origin_ref: ref) + project: project, current_user: user, origin_ref: origin_ref, merge_request: merge_request) end let(:step) { described_class.new(pipeline, command) } let(:ref) { 'master' } + let(:origin_ref) { ref } + let(:merge_request) { nil } + + shared_context 'detached merge request pipeline' do + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: ref, + target_project: project, + target_branch: 'feature') + end + + let(:pipeline) do + build(:ci_pipeline, + source: :merge_request_event, + merge_request: merge_request, + project: project) + end + + let(:origin_ref) { merge_request.ref_path } + end context 'when users has no ability to run a pipeline' do before do @@ -58,6 +79,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do it { is_expected.to be_truthy } + context 'when pipeline is a detached merge request pipeline' do + include_context 'detached merge request pipeline' + + it { is_expected.to be_truthy } + end + context 'when the branch is protected' do let!(:protected_branch) do create(:protected_branch, project: project, name: ref) @@ -65,6 +92,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do it { is_expected.to be_falsey } + context 'when pipeline is a detached merge request pipeline' do + include_context 'detached merge request pipeline' + + it { is_expected.to be_falsey } + end + context 'when developers are allowed to merge' do let!(:protected_branch) do create(:protected_branch, @@ -74,6 +107,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do end it { is_expected.to be_truthy } + + context 'when pipeline is a detached merge request pipeline' do + include_context 'detached merge request pipeline' + + it { is_expected.to be_truthy } + end end end @@ -112,6 +151,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do end it { is_expected.to be_truthy } + + context 'when pipeline is a detached merge request pipeline' do + include_context 'detached merge request pipeline' + + it { is_expected.to be_truthy } + end end context 'when the tag is protected' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb index a7cad423d09..2e8c9d70098 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb @@ -42,6 +42,25 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do end end + context 'when origin ref is a merge request ref' do + let(:command) do + Gitlab::Ci::Pipeline::Chain::Command.new( + project: project, current_user: user, origin_ref: origin_ref, checkout_sha: checkout_sha) + end + + let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } + let(:origin_ref) { merge_request.ref_path } + let(:checkout_sha) { project.repository.commit(merge_request.ref_path).id } + + it 'does not break the chain' do + expect(step.break?).to be false + end + + it 'does not append pipeline errors' do + expect(pipeline.errors).to be_empty + end + end + context 'when ref is ambiguous' do let(:project) do create(:project, :repository).tap do |proj| diff --git a/spec/lib/gitlab/ci/status/build/preparing_spec.rb b/spec/lib/gitlab/ci/status/build/preparing_spec.rb new file mode 100644 index 00000000000..4d8945845ba --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/preparing_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Preparing do + subject do + described_class.new(double('subject')) + end + + describe '#illustration' do + it { expect(subject.illustration).to include(:image, :size, :title, :content) } + end + + describe '.matches?' do + subject { described_class.matches?(build, nil) } + + context 'when build is preparing' do + let(:build) { create(:ci_build, :preparing) } + + it 'is a correct match' do + expect(subject).to be true + end + end + + context 'when build is not preparing' do + let(:build) { create(:ci_build, :success) } + + it 'does not match' do + expect(subject).to be false + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/preparing_spec.rb b/spec/lib/gitlab/ci/status/preparing_spec.rb new file mode 100644 index 00000000000..7211c0e506d --- /dev/null +++ b/spec/lib/gitlab/ci/status/preparing_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::Status::Preparing do + subject do + described_class.new(double('subject'), nil) + end + + describe '#text' do + it { expect(subject.text).to eq 'preparing' } + end + + describe '#label' do + it { expect(subject.label).to eq 'preparing' } + end + + describe '#icon' do + it { expect(subject.icon).to eq 'status_created' } + end + + describe '#favicon' do + it { expect(subject.favicon).to eq 'favicon_status_created' } + end + + describe '#group' do + it { expect(subject.group).to eq 'preparing' } + end +end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 17d5eae24f5..909dbffa38f 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -115,9 +115,8 @@ describe Gitlab::CurrentSettings do shared_examples 'a non-persisted ApplicationSetting object' do let(:current_settings) { described_class.current_application_settings } - it 'returns a non-persisted ApplicationSetting object' do - expect(current_settings).to be_a(ApplicationSetting) - expect(current_settings).not_to be_persisted + it 'returns a FakeApplicationSettings object' do + expect(current_settings).to be_a(Gitlab::FakeApplicationSettings) end it 'uses the default value from ApplicationSetting.defaults' do @@ -146,6 +145,16 @@ describe Gitlab::CurrentSettings do it 'uses the value from the DB attribute if present and not overridden by an accessor' do expect(current_settings.home_page_url).to eq(db_settings.home_page_url) end + + context 'when a new column is used before being migrated' do + before do + allow(ApplicationSetting).to receive(:defaults).and_return({ foo: 'bar' }) + end + + it 'uses the default value if present' do + expect(current_settings.foo).to eq('bar') + end + end end end diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index ae50abd0e7a..5f57cd6b825 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -17,6 +17,20 @@ describe Gitlab::Database do end end + describe '.human_adapter_name' do + it 'returns PostgreSQL when using PostgreSQL' do + allow(described_class).to receive(:postgresql?).and_return(true) + + expect(described_class.human_adapter_name).to eq('PostgreSQL') + end + + it 'returns MySQL when using MySQL' do + allow(described_class).to receive(:postgresql?).and_return(false) + + expect(described_class.human_adapter_name).to eq('MySQL') + end + end + # These are just simple smoke tests to check if the methods work (regardless # of what they may return). describe '.mysql?' do diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 611c3e946ed..cc36060f864 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -72,6 +72,13 @@ describe Gitlab::Diff::File do expect(diff_file.diff_lines_for_serializer.last.type).to eq('match') end + context 'when called multiple times' do + it 'only adds bottom match line once' do + expect(diff_file.diff_lines_for_serializer.size).to eq(31) + expect(diff_file.diff_lines_for_serializer.size).to eq(31) + end + end + context 'when deleted' do let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') } let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') } diff --git a/spec/lib/gitlab/diff/suggestion_diff_spec.rb b/spec/lib/gitlab/diff/suggestion_diff_spec.rb new file mode 100644 index 00000000000..5a32c2bea37 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestion_diff_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::SuggestionDiff do + describe '#diff_lines' do + let(:from_content) do + <<-BLOB.strip_heredoc + "tags": ["devel", "development", "nightly"], + "desktop-file-name-prefix": "(Development) ", + "finish-args": "foo", + BLOB + end + + let(:to_content) do + <<-BLOB.strip_heredoc + "buildsystem": "meson", + "builddir": true, + "name": "nautilus", + "bar": "bar", + BLOB + end + + let(:suggestion) do + instance_double(Suggestion, from_line: 12, + from_content: from_content, + to_content: to_content) + end + + subject { described_class.new(suggestion).diff_lines } + + let(:expected_diff_lines) do + [ + { old_pos: 12, new_pos: 12, type: "match", text: "@@ -12 +12" }, + { old_pos: 12, new_pos: 12, type: "old", text: "-\"tags\": [\"devel\", \"development\", \"nightly\"]," }, + { old_pos: 13, new_pos: 12, type: "old", text: "-\"desktop-file-name-prefix\": \"(Development) \"," }, + { old_pos: 14, new_pos: 12, type: "old", text: "-\"finish-args\": \"foo\"," }, + { old_pos: 15, new_pos: 12, type: "new", text: "+\"buildsystem\": \"meson\"," }, + { old_pos: 15, new_pos: 13, type: "new", text: "+\"builddir\": true," }, + { old_pos: 15, new_pos: 14, type: "new", text: "+\"name\": \"nautilus\"," }, + { old_pos: 15, new_pos: 15, type: "new", text: "+\"bar\": \"bar\"," } + ] + end + + it 'returns diff lines with correct line numbers' do + diff_lines = subject + + expect(diff_lines).to all(be_a(Gitlab::Diff::Line)) + + expected_diff_lines.each_with_index do |expected_line, index| + expect(diff_lines[index].to_hash).to include(expected_line) + end + end + end +end diff --git a/spec/lib/gitlab/diff/suggestion_spec.rb b/spec/lib/gitlab/diff/suggestion_spec.rb new file mode 100644 index 00000000000..71fd25df698 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestion_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::Suggestion do + shared_examples 'correct suggestion raw content' do + it 'returns correct raw data' do + expect(suggestion.to_hash).to include(from_content: expected_lines.join, + to_content: "#{text}\n", + lines_above: above, + lines_below: below) + end + end + + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:position) do + Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs) + end + let(:diff_file) do + position.diff_file(project.repository) + end + let(:text) { "# parsed suggestion content\n# with comments" } + + def blob_lines_data(from_line, to_line) + diff_file.new_blob_lines_between(from_line, to_line) + end + + def blob_data + blob = diff_file.new_blob + blob.load_all_data! + blob.data + end + + let(:suggestion) do + described_class.new(text, line: line, above: above, below: below, diff_file: diff_file) + end + + describe '#to_hash' do + context 'when changing content surpasses the top limit' do + let(:line) { 4 } + let(:above) { 5 } + let(:below) { 2 } + let(:expected_above) { line - 1 } + let(:expected_below) { below } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + + it_behaves_like 'correct suggestion raw content' + end + + context 'when changing content surpasses the amount of lines in the blob (bottom)' do + let(:line) { 5 } + let(:above) { 1 } + let(:below) { blob_data.lines.size + 10 } + let(:expected_below) { below } + let(:expected_above) { above } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + + it_behaves_like 'correct suggestion raw content' + end + + context 'when lines are within blob lines boundary' do + let(:line) { 5 } + let(:above) { 2 } + let(:below) { 3 } + let(:expected_below) { below } + let(:expected_above) { above } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + + it_behaves_like 'correct suggestion raw content' + end + + context 'when no extra lines (single-line suggestion)' do + let(:line) { 5 } + let(:above) { 0 } + let(:below) { 0 } + let(:expected_below) { below } + let(:expected_above) { above } + let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) } + + it_behaves_like 'correct suggestion raw content' + end + end +end diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb new file mode 100644 index 00000000000..1119ea04995 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::SuggestionsParser do + describe '.parse' do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:position) do + Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 9, + diff_refs: merge_request.diff_refs) + end + + let(:diff_file) do + position.diff_file(project.repository) + end + + subject do + described_class.parse(markdown, project: merge_request.project, + position: position) + end + + def blob_lines_data(from_line, to_line) + diff_file.new_blob_lines_between(from_line, to_line).join + end + + context 'single-line suggestions' do + let(:markdown) do + <<-MARKDOWN.strip_heredoc + ```suggestion + foo + bar + ``` + + ``` + nothing + ``` + + ```suggestion + xpto + baz + ``` + + ```thing + this is not a suggestion, it's a thing + ``` + MARKDOWN + end + + it 'returns a list of Gitlab::Diff::Suggestion' do + expect(subject).to all(be_a(Gitlab::Diff::Suggestion)) + expect(subject.size).to eq(2) + end + + it 'parsed suggestion has correct data' do + from_line, to_line = position.new_line, position.new_line + + expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line), + to_content: " foo\n bar\n", + lines_above: 0, + lines_below: 0) + + expect(subject.second.to_hash).to include(from_content: blob_lines_data(from_line, to_line), + to_content: " xpto\n baz\n", + lines_above: 0, + lines_below: 0) + end + end + end +end diff --git a/spec/lib/gitlab/fake_application_settings_spec.rb b/spec/lib/gitlab/fake_application_settings_spec.rb index af12e13d36d..c81cb83d9f4 100644 --- a/spec/lib/gitlab/fake_application_settings_spec.rb +++ b/spec/lib/gitlab/fake_application_settings_spec.rb @@ -1,32 +1,33 @@ require 'spec_helper' describe Gitlab::FakeApplicationSettings do - let(:defaults) { { password_authentication_enabled_for_web: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } } + let(:defaults) do + described_class.defaults.merge( + foobar: 'asdf', + 'test?' => 123 + ) + end - subject { described_class.new(defaults) } + let(:setting) { described_class.new(defaults) } it 'wraps OpenStruct variables properly' do - expect(subject.password_authentication_enabled_for_web).to be_falsey - expect(subject.signup_enabled).to be_truthy - expect(subject.foobar).to eq('asdf') + expect(setting.password_authentication_enabled_for_web).to be_truthy + expect(setting.signup_enabled).to be_truthy + expect(setting.foobar).to eq('asdf') end it 'defines predicate methods' do - expect(subject.password_authentication_enabled_for_web?).to be_falsey - expect(subject.signup_enabled?).to be_truthy - end - - it 'predicate method changes when value is updated' do - subject.password_authentication_enabled_for_web = true - - expect(subject.password_authentication_enabled_for_web?).to be_truthy + expect(setting.password_authentication_enabled_for_web?).to be_truthy + expect(setting.signup_enabled?).to be_truthy end it 'does not define a predicate method' do - expect(subject.foobar?).to be_nil + expect(setting.foobar?).to be_nil end it 'does not override an existing predicate method' do - expect(subject.test?).to eq(123) + expect(setting.test?).to eq(123) end + + it_behaves_like 'application settings examples' end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 3fb41a626b2..4a4ac833e39 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -537,6 +537,18 @@ describe Gitlab::Git::Commit, :seed_helper do end end + describe '#gitaly_commit?' do + context 'when the commit data comes from gitaly' do + it { expect(commit.gitaly_commit?).to eq(true) } + end + + context 'when the commit data comes from a Hash' do + let(:commit) { described_class.new(repository, sample_commit_hash) } + + it { expect(commit.gitaly_commit?).to eq(false) } + end + end + describe '#has_zero_stats?' do it { expect(commit.has_zero_stats?).to eq(false) } end diff --git a/spec/lib/gitlab/git/pre_receive_error_spec.rb b/spec/lib/gitlab/git/pre_receive_error_spec.rb index 1b8be62dec6..cb030e38032 100644 --- a/spec/lib/gitlab/git/pre_receive_error_spec.rb +++ b/spec/lib/gitlab/git/pre_receive_error_spec.rb @@ -1,9 +1,19 @@ require 'spec_helper' describe Gitlab::Git::PreReceiveError do - it 'makes its message HTML-friendly' do - ex = described_class.new("hello\nworld\n") + Gitlab::Git::PreReceiveError::SAFE_MESSAGE_PREFIXES.each do |prefix| + context "error messages prefixed with #{prefix}" do + it 'accepts only errors lines with the prefix' do + ex = described_class.new("#{prefix} Hello,\nworld!") - expect(ex.message).to eq('hello<br>world<br>') + expect(ex.message).to eq('Hello,') + end + + it 'makes its message HTML-friendly' do + ex = described_class.new("#{prefix} Hello,\n#{prefix} world!\n") + + expect(ex.message).to eq('Hello,<br>world!') + end + end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 7e6dfa30e37..8ba6862392c 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1688,6 +1688,11 @@ describe Gitlab::Git::Repository, :seed_helper do expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil + # Workaround for https://github.com/libgit2/rugged/issues/785: If + # Gitaly changes .gitconfig while Rugged has the file loaded + # Rugged::Repository#each_key will report stale values unless a + # lookup is done first. + expect(repository_rugged.config['test.foo1']).to be_nil config_keys = repository_rugged.config.each_key.to_a expect(config_keys).not_to include('test.foo1') expect(config_keys).not_to include('test.foo2') diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index d7bd757149d..6d6107ca3e7 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -221,6 +221,21 @@ describe Gitlab::GitalyClient::CommitService do expect(commit).to eq(commit_dbl) end end + + context 'when caching of the ref name is enabled' do + it 'returns a cached commit' do + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: commit_dbl)) + + commit = nil + 2.times do + ::Gitlab::GitalyClient.allow_ref_name_caching do + commit = described_class.new(repository).find_commit('master') + end + end + + expect(commit).to eq(commit_dbl) + end + end end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index b37fe2686b6..7579a6577b9 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -39,7 +39,7 @@ describe Gitlab::GitalyClient::OperationService do context "when pre_receive_error is present" do let(:response) do - Gitaly::UserCreateBranchResponse.new(pre_receive_error: "something failed") + Gitaly::UserCreateBranchResponse.new(pre_receive_error: "GitLab: something failed") end it "throws a PreReceive exception" do @@ -80,7 +80,7 @@ describe Gitlab::GitalyClient::OperationService do context "when pre_receive_error is present" do let(:response) do - Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "something failed") + Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "GitLab: something failed") end it "throws a PreReceive exception" do @@ -117,7 +117,7 @@ describe Gitlab::GitalyClient::OperationService do context "when pre_receive_error is present" do let(:response) do - Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed") + Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "GitLab: something failed") end it "throws a PreReceive exception" do @@ -175,7 +175,7 @@ describe Gitlab::GitalyClient::OperationService do shared_examples 'cherry pick and revert errors' do context 'when a pre_receive_error is present' do - let(:response) { response_class.new(pre_receive_error: "something failed") } + let(:response) { response_class.new(pre_receive_error: "GitLab: something failed") } it 'raises a PreReceiveError' do expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") @@ -313,7 +313,7 @@ describe Gitlab::GitalyClient::OperationService do end context 'when a pre_receive_error is present' do - let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "something failed") } + let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "GitLab: something failed") } it 'raises a PreReceiveError' do expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 400d426c949..0dab39575b9 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -89,6 +89,16 @@ describe Gitlab::GitalyClient::RefService do end end + describe '#list_new_blobs' do + it 'raises DeadlineExceeded when timeout is too small' do + newrev = '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' + + expect do + client.list_new_blobs(newrev, dynamic_timeout: 0.001) + end.to raise_error(GRPC::DeadlineExceeded) + end + end + describe '#local_branches' do it 'sends a find_local_branches message' do expect_any_instance_of(Gitaly::RefService::Stub) diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 15e59718dce..2e4a7c36fb8 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -11,6 +11,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi let(:source_commit) { project.repository.commit('feature') } let(:target_commit) { project.repository.commit('master') } let(:milestone) { create(:milestone, project: project) } + let(:state) { :closed } let(:pull_request) do alice = Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice') @@ -26,13 +27,13 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi source_repository_id: 400, target_repository_id: 200, source_repository_owner: 'alice', - state: :closed, + state: state, milestone_number: milestone.iid, author: alice, assignee: alice, created_at: created_at, updated_at: updated_at, - merged_at: merged_at + merged_at: state == :closed && merged_at ) end @@ -260,53 +261,63 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi end it 'does not create the source branch if merge request is merged' do - mr, exists = importer.create_merge_request - - importer.insert_git_data(mr, exists) + mr = insert_git_data expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy end - it 'creates the source branch if merge request is open' do - mr, exists = importer.create_merge_request - mr.state = 'opened' - mr.save + context 'when merge request is open' do + let(:state) { :opened } - importer.insert_git_data(mr, exists) + it 'creates the source branch' do + # Ensure the project creator is creating the branches because the + # merge request author may not have access to push to this + # repository. The project owner may also be a group. + allow(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original - expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy - expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy - end + mr = insert_git_data - it 'ignores Git errors when creating a branch' do - mr, exists = importer.create_merge_request - mr.state = 'opened' - mr.save + expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end - expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError) - expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + it 'is able to retry on pre-receive errors' do + expect(importer).to receive(:insert_or_replace_git_data).twice.and_call_original + expect(project.repository).to receive(:add_branch).and_raise('exception') - importer.insert_git_data(mr, exists) + expect { insert_git_data }.to raise_error('exception') - expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey - expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + expect(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original + + mr = insert_git_data + + expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + expect(mr.merge_request_diffs).to be_one + end + + it 'ignores Git command errors when creating a branch' do + expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError) + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + mr = insert_git_data + + expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end end it 'creates the merge request diffs' do - mr, exists = importer.create_merge_request - - importer.insert_git_data(mr, exists) + mr = insert_git_data expect(mr.merge_request_diffs.exists?).to eq(true) end it 'creates the merge request diff commits' do - mr, exists = importer.create_merge_request - - importer.insert_git_data(mr, exists) + mr = insert_git_data - diff = mr.merge_request_diffs.take + diff = mr.merge_request_diffs.reload.first expect(diff.merge_request_diff_commits.exists?).to eq(true) end @@ -322,5 +333,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi expect(mr.merge_request_diffs.exists?).to eq(true) end end + + def insert_git_data + mr, exists = importer.create_merge_request + importer.insert_git_data(mr, exists) + mr + end end end diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 47233ea6ee2..41810a8ec03 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -179,6 +179,17 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do describe '#import_repository' do it 'imports the repository' do + repo = double(:repo, default_branch: 'develop') + + expect(client) + .to receive(:repository) + .with('foo/bar') + .and_return(repo) + + expect(project) + .to receive(:change_head) + .with('develop') + expect(project) .to receive(:ensure_repository) diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb new file mode 100644 index 00000000000..f06a2448ff7 --- /dev/null +++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::GlRepository::RepoType do + set(:project) { create(:project) } + + shared_examples 'a repo type' do + describe "#identifier_for_subject" do + subject { described_class.identifier_for_subject(project) } + + it { is_expected.to eq(expected_identifier) } + end + + describe "#fetch_id" do + it "finds an id match in the identifier" do + expect(described_class.fetch_id(expected_identifier)).to eq(expected_id) + end + + it 'does not break on other identifiers' do + expect(described_class.fetch_id("wiki-noid")).to eq(nil) + end + end + + describe "#path_suffix" do + subject { described_class.path_suffix } + + it { is_expected.to eq(expected_suffix) } + end + + describe "#repository_for" do + it "finds the repository for the repo type" do + expect(described_class.repository_for(project)).to eq(expected_repository) + end + end + end + + describe Gitlab::GlRepository::PROJECT do + it_behaves_like 'a repo type' do + let(:expected_identifier) { "project-#{project.id}" } + let(:expected_id) { project.id.to_s } + let(:expected_suffix) { "" } + let(:expected_repository) { project.repository } + end + + it "knows its type" do + expect(described_class).not_to be_wiki + expect(described_class).to be_project + end + end + + describe Gitlab::GlRepository::WIKI do + it_behaves_like 'a repo type' do + let(:expected_identifier) { "wiki-#{project.id}" } + let(:expected_id) { project.id.to_s } + let(:expected_suffix) { ".wiki" } + let(:expected_repository) { project.wiki.repository } + end + + it "knows its type" do + expect(described_class).to be_wiki + expect(described_class).not_to be_project + end + end +end diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb index 4e09020471b..d4b6c629659 100644 --- a/spec/lib/gitlab/gl_repository_spec.rb +++ b/spec/lib/gitlab/gl_repository_spec.rb @@ -5,11 +5,11 @@ describe ::Gitlab::GlRepository do set(:project) { create(:project, :repository) } it 'parses a project gl_repository' do - expect(described_class.parse("project-#{project.id}")).to eq([project, false]) + expect(described_class.parse("project-#{project.id}")).to eq([project, Gitlab::GlRepository::PROJECT]) end it 'parses a wiki gl_repository' do - expect(described_class.parse("wiki-#{project.id}")).to eq([project, true]) + expect(described_class.parse("wiki-#{project.id}")).to eq([project, Gitlab::GlRepository::WIKI]) end it 'throws an argument error on an invalid gl_repository' do diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb new file mode 100644 index 00000000000..2734fcef0a0 --- /dev/null +++ b/spec/lib/gitlab/group_search_results_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Gitlab::GroupSearchResults do + let(:user) { create(:user) } + + describe 'user search' do + let(:group) { create(:group) } + + it 'returns the users belonging to the group matching the search query' do + user1 = create(:user, username: 'gob_bluth') + create(:group_member, :developer, user: user1, group: group) + + user2 = create(:user, username: 'michael_bluth') + create(:group_member, :developer, user: user2, group: group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the subgroup matching the search query', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + subgroup = create(:group, parent: group) + create(:group_member, :developer, user: user1, group: subgroup) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the parent group matching the search query', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + parent_group = create(:group, children: [group]) + create(:group_member, :developer, user: user1, group: parent_group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'does not return the user belonging to the private subgroup', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + subgroup = create(:group, :private, parent: group) + create(:group_member, :developer, user: user1, group: subgroup) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [] + end + + it 'does not return the user belonging to an unrelated group' do + user = create(:user, username: 'gob_bluth') + unrelated_group = create(:group) + create(:group_member, :developer, user: user, group: unrelated_group) + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [] + end + end +end diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb index d03a74ac9eb..8e253b51597 100644 --- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb +++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::HashedStorage::Migrator, :sidekiq do +describe Gitlab::HashedStorage::Migrator, :sidekiq, :redis do describe '#bulk_schedule_migration' do it 'schedules job to HashedStorage::MigratorWorker' do Sidekiq::Testing.fake! do @@ -189,7 +189,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq do set(:project) { create(:project, :empty_repo) } it 'returns true when there are MigratorWorker jobs scheduled' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::MigratorWorker.perform_async(1, 5) expect(subject.migration_pending?).to be_truthy @@ -197,7 +197,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq do end it 'returns true when there are ProjectMigrateWorker jobs scheduled' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::ProjectMigrateWorker.perform_async(1) expect(subject.migration_pending?).to be_truthy @@ -213,7 +213,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq do set(:project) { create(:project, :empty_repo) } it 'returns true when there are RollbackerWorker jobs scheduled' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::RollbackerWorker.perform_async(1, 5) expect(subject.rollback_pending?).to be_truthy @@ -221,7 +221,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq do end it 'returns true when there are jobs scheduled' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::ProjectRollbackWorker.perform_async(1) expect(subject.rollback_pending?).to be_truthy diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 01da3ea7081..e418516569a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -100,6 +100,8 @@ merge_requests: - head_pipeline - latest_merge_request_diff - merge_request_pipelines +- merge_request_assignees +- suggestions merge_request_diff: - merge_request - merge_request_diff_commits @@ -351,3 +353,5 @@ resource_label_events: - label error_tracking_setting: - project +suggestions: +- note diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index ee96e5c4d42..496567b0036 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -608,3 +608,14 @@ ErrorTracking::ProjectErrorTrackingSetting: - project_id - project_name - organization_name +Suggestion: +- id +- note_id +- relative_order +- applied +- commit_id +- from_content +- to_content +- outdated +- lines_above +- lines_below diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb index 2cae8ec031a..b82c09af306 100644 --- a/spec/lib/gitlab/json_cache_spec.rb +++ b/spec/lib/gitlab/json_cache_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::JsonCache do let(:namespace) { 'geo' } let(:key) { 'foo' } let(:expanded_key) { "#{namespace}:#{key}:#{Rails.version}" } - let(:broadcast_message) { create(:broadcast_message) } + set(:broadcast_message) { create(:broadcast_message) } subject(:cache) { described_class.new(namespace: namespace, backend: backend) } @@ -146,6 +146,18 @@ describe Gitlab::JsonCache do expect(cache.read(key, BroadcastMessage)).to be_nil end + + it 'gracefully handles excluded fields from attributes during serialization' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.except("message_html").to_json) + + result = cache.read(key, BroadcastMessage) + + BroadcastMessage.cached_markdown_fields.html_fields.each do |field| + expect(result.public_send(field)).to be_nil + end + end end context 'when the cached value is an array' do @@ -321,6 +333,46 @@ describe Gitlab::JsonCache do expect(result).to be_new_record end + + it 'gracefully handles bad cached entry' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return('{') + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to eq 'block result' + end + + it 'gracefully handles an empty hash' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return('{}') + + expect(cache.fetch(key, as: BroadcastMessage)).to be_a(BroadcastMessage) + end + + it 'gracefully handles unknown attributes' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.merge(unknown_attribute: 1).to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + expect(result).to eq 'block result' + end + + it 'gracefully handles excluded fields from attributes during serialization' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.except("message_html").to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + BroadcastMessage.cached_markdown_fields.html_fields.each do |field| + expect(result.public_send(field)).to be_nil + end + end end it "returns the result of the block when 'as' option is nil" do diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb index f326d57e9c6..57b570a9166 100644 --- a/spec/lib/gitlab/kubernetes_spec.rb +++ b/spec/lib/gitlab/kubernetes_spec.rb @@ -40,10 +40,40 @@ describe Gitlab::Kubernetes do describe '#filter_by_label' do it 'returns matching labels' do - matching_items = [kube_pod(app: 'foo')] + matching_items = [kube_pod(track: 'foo'), kube_deployment(track: 'foo')] + items = matching_items + [kube_pod, kube_deployment] + + expect(filter_by_label(items, 'track' => 'foo')).to eq(matching_items) + end + end + + describe '#filter_by_annotation' do + it 'returns matching labels' do + matching_items = [kube_pod(environment_slug: 'foo'), kube_deployment(environment_slug: 'foo')] + items = matching_items + [kube_pod, kube_deployment] + + expect(filter_by_annotation(items, 'app.gitlab.com/env' => 'foo')).to eq(matching_items) + end + end + + describe '#filter_by_project_environment' do + let(:matching_pod) { kube_pod(environment_slug: 'production', project_slug: 'my-cool-app') } + + it 'returns matching legacy env label' do + matching_pod['metadata']['annotations'].delete('app.gitlab.com/app') + matching_pod['metadata']['annotations'].delete('app.gitlab.com/env') + matching_pod['metadata']['labels']['app'] = 'production' + matching_items = [matching_pod] + items = matching_items + [kube_pod] + + expect(filter_by_project_environment(items, 'my-cool-app', 'production')).to eq(matching_items) + end + + it 'returns matching env label' do + matching_items = [matching_pod] items = matching_items + [kube_pod] - expect(filter_by_label(items, app: 'foo')).to eq(matching_items) + expect(filter_by_project_environment(items, 'my-cool-app', 'production')).to eq(matching_items) end end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 6831274d37c..4a41d5cf51e 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -412,4 +412,36 @@ describe Gitlab::ProjectSearchResults do end end end + + describe 'user search' do + it 'returns the user belonging to the project matching the search query' do + project = create(:project) + + user1 = create(:user, username: 'gob_bluth') + create(:project_member, :developer, user: user1, project: project) + + user2 = create(:user, username: 'michael_bluth') + create(:project_member, :developer, user: user2, project: project) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, project, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the group matching the search query' do + group = create(:group) + project = create(:project, namespace: group) + + user1 = create(:user, username: 'gob_bluth') + create(:group_member, :developer, user: user1, group: group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, project, 'gob').objects('users') + + expect(result).to eq [user1] + end + end end diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb index 136cfb5bcc5..b6e0adbc1c2 100644 --- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb +++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb @@ -69,6 +69,36 @@ describe Gitlab::QuickActions::CommandDefinition do expect(subject.available?(opts)).to be true end end + + context "when the command has types" do + before do + subject.types = [Issue, Commit] + end + + context "when the command target type is allowed" do + it "returns true" do + opts[:quick_action_target] = Issue.new + expect(subject.available?(opts)).to be true + end + end + + context "when the command target type is not allowed" do + it "returns true" do + opts[:quick_action_target] = MergeRequest.new + expect(subject.available?(opts)).to be false + end + end + end + + context "when the command has no types" do + it "any target type is allowed" do + opts[:quick_action_target] = Issue.new + expect(subject.available?(opts)).to be true + + opts[:quick_action_target] = MergeRequest.new + expect(subject.available?(opts)).to be true + end + end end describe "#execute" do diff --git a/spec/lib/gitlab/quick_actions/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb index fd4df8694ba..185adab1ff6 100644 --- a/spec/lib/gitlab/quick_actions/dsl_spec.rb +++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb @@ -48,13 +48,19 @@ describe Gitlab::QuickActions::Dsl do substitution :something do |text| "#{text} Some complicated thing you want in here" end + + desc 'A command with types' + types Issue, Commit + command :has_types do + "Has Issue and Commit types" + end end end describe '.command_definitions' do it 'returns an array with commands definitions' do no_args_def, explanation_with_aliases_def, dynamic_description_def, - cc_def, cond_action_def, with_params_parsing_def, substitution_def = + cc_def, cond_action_def, with_params_parsing_def, substitution_def, has_types = DummyClass.command_definitions expect(no_args_def.name).to eq(:no_args) @@ -63,6 +69,7 @@ describe Gitlab::QuickActions::Dsl do expect(no_args_def.explanation).to eq('') expect(no_args_def.params).to eq([]) expect(no_args_def.condition_block).to be_nil + expect(no_args_def.types).to eq([]) expect(no_args_def.action_block).to be_a_kind_of(Proc) expect(no_args_def.parse_params_block).to be_nil expect(no_args_def.warning).to eq('') @@ -73,6 +80,7 @@ describe Gitlab::QuickActions::Dsl do expect(explanation_with_aliases_def.explanation).to eq('Static explanation') expect(explanation_with_aliases_def.params).to eq(['The first argument']) expect(explanation_with_aliases_def.condition_block).to be_nil + expect(explanation_with_aliases_def.types).to eq([]) expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc) expect(explanation_with_aliases_def.parse_params_block).to be_nil expect(explanation_with_aliases_def.warning).to eq('Possible problem!') @@ -83,6 +91,7 @@ describe Gitlab::QuickActions::Dsl do expect(dynamic_description_def.explanation).to eq('') expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument']) expect(dynamic_description_def.condition_block).to be_nil + expect(dynamic_description_def.types).to eq([]) expect(dynamic_description_def.action_block).to be_a_kind_of(Proc) expect(dynamic_description_def.parse_params_block).to be_nil expect(dynamic_description_def.warning).to eq('') @@ -93,6 +102,7 @@ describe Gitlab::QuickActions::Dsl do expect(cc_def.explanation).to eq('') expect(cc_def.params).to eq([]) expect(cc_def.condition_block).to be_nil + expect(cc_def.types).to eq([]) expect(cc_def.action_block).to be_nil expect(cc_def.parse_params_block).to be_nil expect(cc_def.warning).to eq('') @@ -103,6 +113,7 @@ describe Gitlab::QuickActions::Dsl do expect(cond_action_def.explanation).to be_a_kind_of(Proc) expect(cond_action_def.params).to eq([]) expect(cond_action_def.condition_block).to be_a_kind_of(Proc) + expect(cond_action_def.types).to eq([]) expect(cond_action_def.action_block).to be_a_kind_of(Proc) expect(cond_action_def.parse_params_block).to be_nil expect(cond_action_def.warning).to eq('') @@ -113,6 +124,7 @@ describe Gitlab::QuickActions::Dsl do expect(with_params_parsing_def.explanation).to eq('') expect(with_params_parsing_def.params).to eq([]) expect(with_params_parsing_def.condition_block).to be_nil + expect(with_params_parsing_def.types).to eq([]) expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc) expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc) expect(with_params_parsing_def.warning).to eq('') @@ -123,9 +135,21 @@ describe Gitlab::QuickActions::Dsl do expect(substitution_def.explanation).to eq('') expect(substitution_def.params).to eq(['<Comment>']) expect(substitution_def.condition_block).to be_nil + expect(substitution_def.types).to eq([]) expect(substitution_def.action_block.call('text')).to eq('text Some complicated thing you want in here') expect(substitution_def.parse_params_block).to be_nil expect(substitution_def.warning).to eq('') + + expect(has_types.name).to eq(:has_types) + expect(has_types.aliases).to eq([]) + expect(has_types.description).to eq('A command with types') + expect(has_types.explanation).to eq('') + expect(has_types.params).to eq([]) + expect(has_types.condition_block).to be_nil + expect(has_types.types).to eq([Issue, Commit]) + expect(has_types.action_block).to be_a_kind_of(Proc) + expect(has_types.parse_params_block).to be_nil + expect(has_types.warning).to eq('') end end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 4139d1c650c..d982053d92e 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Gitlab::ReferenceExtractor do diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb index 13940713dfc..4c7ca4e2b57 100644 --- a/spec/lib/gitlab/repo_path_spec.rb +++ b/spec/lib/gitlab/repo_path_spec.rb @@ -6,43 +6,47 @@ describe ::Gitlab::RepoPath do context 'a repository storage path' do it 'parses a full repository path' do - expect(described_class.parse(project.repository.full_path)).to eq([project, false, nil]) + expect(described_class.parse(project.repository.full_path)).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end it 'parses a full wiki path' do - expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, true, nil]) + expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, Gitlab::GlRepository::WIKI, nil]) end end context 'a relative path' do it 'parses a relative repository path' do - expect(described_class.parse(project.full_path + '.git')).to eq([project, false, nil]) + expect(described_class.parse(project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end it 'parses a relative wiki path' do - expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true, nil]) + expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, nil]) end it 'parses a relative path starting with /' do - expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false, nil]) + expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil]) end context 'of a redirected project' do let(:redirect) { project.route.create_redirect('foo/bar') } it 'parses a relative repository path' do - expect(described_class.parse(redirect.path + '.git')).to eq([project, false, 'foo/bar']) + expect(described_class.parse(redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar']) end it 'parses a relative wiki path' do - expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, true, 'foo/bar.wiki']) + expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, 'foo/bar.wiki']) end it 'parses a relative path starting with /' do - expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, false, 'foo/bar']) + expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar']) end end end + + it "returns nil for non existent paths" do + expect(described_class.parse("path/non-existent.git")).to eq(nil) + end end describe '.find_project' do diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 87288baedb0..4b57eecff93 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -121,6 +121,22 @@ describe Gitlab::SearchResults do results.objects('issues') end end + + describe '#users' do + it 'does not call the UsersFinder when the current_user is not allowed to read users list' do + allow(Ability).to receive(:allowed?).and_return(false) + + expect(UsersFinder).not_to receive(:new).with(user, search: 'foo').and_call_original + + results.objects('users') + end + + it 'calls the UsersFinder' do + expect(UsersFinder).to receive(:new).with(user, search: 'foo').and_call_original + + results.objects('users') + end + end end it 'does not list issues on private projects' do diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index d6aadf0f7de..e2f09de2808 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -8,6 +8,7 @@ describe Gitlab::Shell do let(:gitlab_shell) { described_class.new } let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } } let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } + let(:gitlab_authorized_keys) { double } before do allow(Project).to receive(:find).and_return(project) @@ -49,13 +50,38 @@ describe Gitlab::Shell do describe '#add_key' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with add-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'add-key', + 'key-123', + 'ssh-rsa foobar' + ]) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') + end end end @@ -64,10 +90,24 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end end end @@ -76,24 +116,89 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with add-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'add-key', + 'key-123', + 'ssh-rsa foobar' + ]) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') + end end end end describe '#batch_add_keys' do + let(:keys) { [double(shell_id: 'key-123', key: 'ssh-rsa foobar')] } + context 'when authorized_keys_enabled is true' do - it 'instantiates KeyAdder' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + context 'authorized_keys_file not set' do + let(:io) { double } + + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + context 'valid keys' do + before do + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls gitlab-keys with batch-add-keys command' do + expect(IO) + .to receive(:popen) + .with("gitlab_shell_keys_path batch-add-keys", 'w') + .and_yield(io) + + expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") + expect(gitlab_shell.batch_add_keys(keys)).to be_truthy + end + end + + context 'invalid keys' do + let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } + + it 'catches failure and returns false' do + expect(gitlab_shell.batch_add_keys(keys)).to be_falsey + end + end + end - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) + + gitlab_shell.batch_add_keys(keys) end end end @@ -103,11 +208,23 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).not_to receive(:add_key) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(IO).not_to receive(:popen) + + gitlab_shell.batch_add_keys(keys) + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + gitlab_shell.batch_add_keys(keys) end end end @@ -117,11 +234,37 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'instantiates KeyAdder' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + context 'authorized_keys_file not set' do + let(:io) { double } - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls gitlab-keys with batch-add-keys command' do + expect(IO) + .to receive(:popen) + .with("gitlab_shell_keys_path batch-add-keys", 'w') + .and_yield(io) + + expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") + + gitlab_shell.batch_add_keys(keys) + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) + + gitlab_shell.batch_add_keys(keys) end end end @@ -129,13 +272,34 @@ describe Gitlab::Shell do describe '#remove_key' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + it 'calls #gitlab_shell_fast_execute with rm-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'rm-key', + 'key-123' + ]) + + gitlab_shell.remove_key('key-123') + end + end + + context 'authorized_keys_file not set' do + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') + + gitlab_shell.remove_key('key-123') + end end end @@ -144,10 +308,24 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + gitlab_shell.remove_key('key-123') + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) + + gitlab_shell.remove_key('key-123') + end end end @@ -156,232 +334,256 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with rm-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'rm-key', + 'key-123' + ]) - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + gitlab_shell.remove_key('key-123') + end end - end - context 'when key content is not given' do - it 'calls rm-key with only one argument' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123'] - ) + context 'authorized_keys_file not set' do + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - gitlab_shell.remove_key('key-123') + gitlab_shell.remove_key('key-123') + end end end end describe '#remove_all_keys' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with([:gitlab_shell_keys_path, 'clear']) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - gitlab_shell.remove_all_keys - end - end + it 'calls #gitlab_shell_fast_execute with clear command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([:gitlab_shell_keys_path, 'clear']) - context 'when authorized_keys_enabled is false' do - before do - stub_application_setting(authorized_keys_enabled: false) + gitlab_shell.remove_all_keys + end end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - gitlab_shell.remove_all_keys + gitlab_shell.remove_all_keys + end end end - context 'when authorized_keys_enabled is nil' do + context 'when authorized_keys_enabled is false' do before do - stub_application_setting(authorized_keys_enabled: nil) + stub_application_setting(authorized_keys_enabled: false) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'clear'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end - gitlab_shell.remove_all_keys - end - end - end + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) - describe '#remove_keys_not_found_in_db' do - context 'when keys are in the file that are not in the DB' do - before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') - @another_key = create(:key) # this one IS in the DB + gitlab_shell.remove_all_keys + end end - it 'removes the keys' do - expect(find_in_authorized_keys_file(1234)).to be_truthy - expect(find_in_authorized_keys_file(9876)).to be_truthy - expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey - expect(find_in_authorized_keys_file(9876)).to be_falsey - expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) + + gitlab_shell.remove_all_keys + end end end - context 'when keys there are duplicate keys in the file that are not in the DB' do + context 'when authorized_keys_enabled is nil' do before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + stub_application_setting(authorized_keys_enabled: nil) end - it 'removes the keys' do - expect(find_in_authorized_keys_file(1234)).to be_truthy - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey - end + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - it 'does not run remove more than once per key (in a batch)' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234').once - gitlab_shell.remove_keys_not_found_in_db - end - end + it 'calls #gitlab_shell_fast_execute with clear command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([:gitlab_shell_keys_path, 'clear']) - context 'when keys there are duplicate keys in the file that ARE in the DB' do - before do - gitlab_shell.remove_all_keys - @key = create(:key) - gitlab_shell.add_key(@key.shell_id, @key.key) + gitlab_shell.remove_all_keys + end end - it 'does not remove the key' do - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(@key.id)).to be_truthy - end + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - it 'does not need to run a SELECT query for that batch, on account of that key' do - expect_any_instance_of(ActiveRecord::Relation).not_to receive(:pluck) - gitlab_shell.remove_keys_not_found_in_db + gitlab_shell.remove_all_keys + end end end + end - unless ENV['CI'] # Skip in CI, it takes 1 minute - context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + describe '#remove_keys_not_found_in_db' do + context 'when keys are in the file that are not in the DB' do + context 'authorized_keys_file not set' do before do + stub_gitlab_shell_setting(authorized_keys_file: nil) gitlab_shell.remove_all_keys - 100.times { |i| create(:key) } # first batch is all in the DB gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB end - it 'removes the keys not in the DB' do - expect(find_in_authorized_keys_file(1234)).to be_truthy + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + expect(gitlab_shell).to receive(:remove_key).with('key-9876') + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") + gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey end end - end - end - describe '#batch_read_key_ids' do - context 'when there are keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys - (1..4).each do |i| - gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB end - end - it 'iterates over the key IDs in the file, in batches' do - loop_count = 0 - first_batch = [1, 2] - second_batch = [3, 4] + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + expect(gitlab_shell).to receive(:remove_key).with('key-9876') + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") - gitlab_shell.batch_read_key_ids(batch_size: 2) do |batch| - expected = (loop_count == 0 ? first_batch : second_batch) - expect(batch).to eq(expected) - loop_count += 1 + gitlab_shell.remove_keys_not_found_in_db end end end - end - describe '#list_key_ids' do - context 'when there are keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys - (1..4).each do |i| - gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + context 'when keys there are duplicate keys in the file that are not in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + + gitlab_shell.remove_keys_not_found_in_db end end - it 'outputs the key IDs in the file, separated by newlines' do - ids = [] - gitlab_shell.list_key_ids do |io| - io.each do |line| - ids << line - end + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') end - expect(ids).to eq(%W{1\n 2\n 3\n 4\n}) - end - end + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - context 'when there are no keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys + gitlab_shell.remove_keys_not_found_in_db + end end + end - it 'outputs nothing, not even an empty string' do - ids = [] - gitlab_shell.list_key_ids do |io| - io.each do |line| - ids << line - end + context 'when keys there are duplicate keys in the file that ARE in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) end - expect(ids).to eq([]) + it 'does not remove the key' do + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") + + gitlab_shell.remove_keys_not_found_in_db + end end - end - end - describe Gitlab::Shell::KeyAdder do - describe '#add_key' do - it 'removes trailing garbage' do - io = spy(:io) - adder = described_class.new(io) + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) + end - adder.add_key('key-42', "ssh-rsa foo bar\tbaz") + it 'does not remove the key' do + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") - expect(io).to have_received(:puts).with("key-42\tssh-rsa foo") + gitlab_shell.remove_keys_not_found_in_db + end end + end - it 'handles multiple spaces in the key' do - io = spy(:io) - adder = described_class.new(io) + unless ENV['CI'] # Skip in CI, it takes 1 minute + context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end - adder.add_key('key-42', "ssh-rsa foo") + it 'removes the keys not in the DB' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - expect(io).to have_received(:puts).with("key-42\tssh-rsa foo") - end + gitlab_shell.remove_keys_not_found_in_db + end + end - it 'raises an exception if the key contains a tab' do - expect do - described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar") - end.to raise_error(Gitlab::Shell::Error) - end + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys not in the DB' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - it 'raises an exception if the key contains a newline' do - expect do - described_class.new(StringIO.new).add_key('key-42', "ssh-rsa foobar\nssh-rsa pawned") - end.to raise_error(Gitlab::Shell::Error) + gitlab_shell.remove_keys_not_found_in_db + end + end end end end @@ -566,12 +768,4 @@ describe Gitlab::Shell do end end end - - def find_in_authorized_keys_file(key_id) - gitlab_shell.batch_read_key_ids do |ids| - return true if ids.include?(key_id) # rubocop:disable Cop/AvoidReturnFromBlocks - end - - false - end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index cd9e4d48cd1..549cc5ac057 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -13,6 +13,8 @@ describe Gitlab::UsageData do create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true) create(:service, project: projects[1], type: 'SlackService', active: true) create(:service, project: projects[2], type: 'SlackService', active: true) + create(:project_error_tracking_setting, project: projects[0]) + create(:project_error_tracking_setting, project: projects[1], enabled: false) gcp_cluster = create(:cluster, :provided_by_gcp) create(:cluster, :provided_by_user) @@ -117,6 +119,7 @@ describe Gitlab::UsageData do projects_slack_slash_active projects_prometheus_active projects_with_repositories_enabled + projects_with_error_tracking_enabled pages_domains protected_branches releases @@ -146,6 +149,7 @@ describe Gitlab::UsageData do expect(count_data[:projects_slack_notifications_active]).to eq(2) expect(count_data[:projects_slack_slash_active]).to eq(1) expect(count_data[:projects_with_repositories_enabled]).to eq(2) + expect(count_data[:projects_with_error_tracking_enabled]).to eq(1) expect(count_data[:clusters_enabled]).to eq(7) expect(count_data[:project_clusters_enabled]).to eq(6) diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb index 6e2bb81fbda..b86ec5445b8 100644 --- a/spec/lib/gitlab/user_extractor_spec.rb +++ b/spec/lib/gitlab/user_extractor_spec.rb @@ -38,6 +38,18 @@ describe Gitlab::UserExtractor do expect(extractor.users).to include(user) end + + context 'input as array of strings' do + it 'is treated as one string' do + extractor = described_class.new(text.lines) + + user_1 = create(:user, username: "USER-1") + user_4 = create(:user, username: "USER-4") + user_email = create(:user, email: 'user@gitlab.org') + + expect(extractor.users).to contain_exactly(user_1, user_4, user_email) + end + end end describe '#matches' do diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 8f5029b3565..4645339f439 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -213,4 +213,22 @@ describe Gitlab::Utils do expect(subject[:variables].first[:key]).to eq('VAR1') end end + + describe '.try_megabytes_to_bytes' do + context 'when the size can be converted to megabytes' do + it 'returns the size in megabytes' do + size = described_class.try_megabytes_to_bytes(1) + + expect(size).to eq(1.megabytes) + end + end + + context 'when the size can not be converted to megabytes' do + it 'returns the input size' do + size = described_class.try_megabytes_to_bytes('foo') + + expect(size).to eq('foo') + end + end + end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 7213eee5675..d88086b01b1 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -250,11 +250,11 @@ describe Gitlab::Workhorse do } end - subject { described_class.git_http_ok(repository, false, user, action) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) } it { expect(subject).to include(params) } - context 'when is_wiki' do + context 'when the repo_type is a wiki' do let(:params) do { GL_ID: "user-#{user.id}", @@ -264,7 +264,7 @@ describe Gitlab::Workhorse do } end - subject { described_class.git_http_ok(repository, true, user, action) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::WIKI, user, action) } it { expect(subject).to include(params) } end @@ -304,7 +304,7 @@ describe Gitlab::Workhorse do end context 'show_all_refs enabled' do - subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } it { is_expected.to include(ShowAllRefs: true) } end @@ -322,7 +322,7 @@ describe Gitlab::Workhorse do it { expect(subject).to include(gitaly_params) } context 'show_all_refs enabled' do - subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) } + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } it { is_expected.to include(ShowAllRefs: true) } end diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb index 8232715d00e..767b5779a79 100644 --- a/spec/lib/gitlab_spec.rb +++ b/spec/lib/gitlab_spec.rb @@ -10,7 +10,7 @@ describe Gitlab do end describe '.revision' do - let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] } + let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h --abbrev=11 -n 1] } around do |example| described_class.instance_variable_set(:@_revision, nil) diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb index 88e7e2e5ebb..3333f8307ae 100644 --- a/spec/lib/sentry/client_spec.rb +++ b/spec/lib/sentry/client_spec.rb @@ -65,7 +65,9 @@ describe Sentry::Client do let(:issue_status) { 'unresolved' } let(:limit) { 20 } - let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: issues_sample_response) } + let(:sentry_api_response) { issues_sample_response } + + let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: sentry_api_response) } subject { client.list_issues(issue_status: issue_status, limit: limit) } @@ -74,6 +76,14 @@ describe Sentry::Client do it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error it_behaves_like 'has correct length', 1 + shared_examples 'has correct external_url' do + context 'external_url' do + it 'is constructed correctly' do + expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11') + end + end + end + context 'error object created from sentry response' do using RSpec::Parameterized::TableSyntax @@ -96,14 +106,10 @@ describe Sentry::Client do end with_them do - it { expect(subject[0].public_send(error_object)).to eq(issues_sample_response[0].dig(*sentry_response)) } + it { expect(subject[0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) } end - context 'external_url' do - it 'is constructed correctly' do - expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11') - end - end + it_behaves_like 'has correct external_url' end context 'redirects' do @@ -135,12 +141,42 @@ describe Sentry::Client do expect(valid_req_stub).to have_been_requested end end + + context 'Older sentry versions where keys are not present' do + let(:sentry_api_response) do + issues_sample_response[0...1].map do |issue| + issue[:project].delete(:id) + issue + end + end + + it_behaves_like 'calls sentry api' + + it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error + it_behaves_like 'has correct length', 1 + + it_behaves_like 'has correct external_url' + end + + context 'essential keys missing in API response' do + let(:sentry_api_response) do + issues_sample_response[0...1].map do |issue| + issue.except(:id) + end + end + + it 'raises exception' do + expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"') + end + end end describe '#list_projects' do let(:sentry_list_projects_url) { 'https://sentrytest.gitlab.com/api/0/projects/' } - let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) } + let(:sentry_api_response) { projects_sample_response } + + let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: sentry_api_response) } subject { client.list_projects } @@ -149,14 +185,31 @@ describe Sentry::Client do it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project it_behaves_like 'has correct length', 2 - context 'keys missing in API response' do - it 'raises exception' do - projects_sample_response[0].delete(:slug) + context 'essential keys missing in API response' do + let(:sentry_api_response) do + projects_sample_response[0...1].map do |project| + project.except(:slug) + end + end - stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) + it 'raises exception' do + expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "slug"') + end + end - expect { subject }.to raise_error(Sentry::Client::SentryError, 'Sentry API response is missing keys. key not found: "slug"') + context 'optional keys missing in sentry response' do + let(:sentry_api_response) do + projects_sample_response[0...1].map do |project| + project[:organization].delete(:id) + project.delete(:id) + project.except(:status) + end end + + it_behaves_like 'calls sentry api' + + it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project + it_behaves_like 'has correct length', 1 end context 'error object created from sentry response' do @@ -173,7 +226,11 @@ describe Sentry::Client do end with_them do - it { expect(subject[0].public_send(sentry_project_object)).to eq(projects_sample_response[0].dig(*sentry_response)) } + it do + expect(subject[0].public_send(sentry_project_object)).to( + eq(sentry_api_response[0].dig(*sentry_response)) + ) + end end end diff --git a/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb b/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb index b2d8f476bb2..a1f243651b5 100644 --- a/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb +++ b/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb @@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20181219145520_migrate_cluster_co describe MigrateClusterConfigureWorkerSidekiqQueue, :sidekiq, :redis do include Gitlab::Database::MigrationHelpers + include StubWorker context 'when there are jobs in the queue' do it 'correctly migrates queue when migrating up' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'gcp_cluster:cluster_platform_configure').perform_async('Something', [1]) - stubbed_worker(queue: 'gcp_cluster:cluster_configure').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:cluster_platform_configure').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:cluster_configure').perform_async('Something', [1]) described_class.new.up @@ -19,12 +20,12 @@ describe MigrateClusterConfigureWorkerSidekiqQueue, :sidekiq, :redis do it 'does not affect other queues under the same namespace' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'gcp_cluster:cluster_install_app').perform_async('Something', [1]) - stubbed_worker(queue: 'gcp_cluster:cluster_provision').perform_async('Something', [1]) - stubbed_worker(queue: 'gcp_cluster:cluster_wait_for_app_installation').perform_async('Something', [1]) - stubbed_worker(queue: 'gcp_cluster:wait_for_cluster_creation').perform_async('Something', [1]) - stubbed_worker(queue: 'gcp_cluster:cluster_wait_for_ingress_ip_address').perform_async('Something', [1]) - stubbed_worker(queue: 'gcp_cluster:cluster_project_configure').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:cluster_install_app').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:cluster_provision').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:cluster_wait_for_app_installation').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:wait_for_cluster_creation').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:cluster_wait_for_ingress_ip_address').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:cluster_project_configure').perform_async('Something', [1]) described_class.new.up @@ -39,7 +40,7 @@ describe MigrateClusterConfigureWorkerSidekiqQueue, :sidekiq, :redis do it 'correctly migrates queue when migrating down' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'gcp_cluster:cluster_configure').perform_async('Something', [1]) + stub_worker(queue: 'gcp_cluster:cluster_configure').perform_async('Something', [1]) described_class.new.down @@ -58,11 +59,4 @@ describe MigrateClusterConfigureWorkerSidekiqQueue, :sidekiq, :redis do expect { described_class.new.down }.not_to raise_error end end - - def stubbed_worker(queue:) - Class.new do - include Sidekiq::Worker - sidekiq_options queue: queue - end - end end diff --git a/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb b/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb index c18ae3b76d3..66555118a43 100644 --- a/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb +++ b/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb @@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20180306074045_migrate_create_tra describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do include Gitlab::Database::MigrationHelpers + include StubWorker context 'when there are jobs in the queues' do it 'correctly migrates queue when migrating up' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'pipeline_default:create_trace_artifact').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:create_trace_artifact').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1]) described_class.new.up @@ -19,11 +20,11 @@ describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do it 'does not affect other queues under the same namespace' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1]) described_class.new.up @@ -37,7 +38,7 @@ describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do it 'correctly migrates queue when migrating down' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1]) described_class.new.down @@ -56,11 +57,4 @@ describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do expect { described_class.new.down }.not_to raise_error end end - - def stubbed_worker(queue:) - Class.new do - include Sidekiq::Worker - sidekiq_options queue: queue - end - end end diff --git a/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb index 1ee6c440cf4..6ce04805e5d 100644 --- a/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb +++ b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb @@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20180603190921_migrate_object_sto describe MigrateObjectStorageUploadSidekiqQueue, :sidekiq, :redis do include Gitlab::Database::MigrationHelpers + include StubWorker context 'when there are jobs in the queue' do it 'correctly migrates queue when migrating up' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'object_storage_upload').perform_async('Something', [1]) - stubbed_worker(queue: 'object_storage:object_storage_background_move').perform_async('Something', [1]) + stub_worker(queue: 'object_storage_upload').perform_async('Something', [1]) + stub_worker(queue: 'object_storage:object_storage_background_move').perform_async('Something', [1]) described_class.new.up @@ -23,11 +24,4 @@ describe MigrateObjectStorageUploadSidekiqQueue, :sidekiq, :redis do expect { described_class.new.up }.not_to raise_error end end - - def stubbed_worker(queue:) - Class.new do - include Sidekiq::Worker - sidekiq_options queue: queue - end - end end diff --git a/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb b/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb index e02bcd2f4da..e38044ccceb 100644 --- a/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb +++ b/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb @@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20170822101017_migrate_pipeline_s describe MigratePipelineSidekiqQueues, :sidekiq, :redis do include Gitlab::Database::MigrationHelpers + include StubWorker context 'when there are jobs in the queues' do it 'correctly migrates queue when migrating up' do Sidekiq::Testing.disable! do - stubbed_worker(queue: :pipeline).perform_async('Something', [1]) - stubbed_worker(queue: :build).perform_async('Something', [1]) + stub_worker(queue: :pipeline).perform_async('Something', [1]) + stub_worker(queue: :build).perform_async('Something', [1]) described_class.new.up @@ -20,10 +21,10 @@ describe MigratePipelineSidekiqQueues, :sidekiq, :redis do it 'correctly migrates queue when migrating down' do Sidekiq::Testing.disable! do - stubbed_worker(queue: :pipeline_default).perform_async('Class', [1]) - stubbed_worker(queue: :pipeline_processing).perform_async('Class', [2]) - stubbed_worker(queue: :pipeline_hooks).perform_async('Class', [3]) - stubbed_worker(queue: :pipeline_cache).perform_async('Class', [4]) + stub_worker(queue: :pipeline_default).perform_async('Class', [1]) + stub_worker(queue: :pipeline_processing).perform_async('Class', [2]) + stub_worker(queue: :pipeline_hooks).perform_async('Class', [3]) + stub_worker(queue: :pipeline_cache).perform_async('Class', [4]) described_class.new.down @@ -45,11 +46,4 @@ describe MigratePipelineSidekiqQueues, :sidekiq, :redis do expect { described_class.new.down }.not_to raise_error end end - - def stubbed_worker(queue:) - Class.new do - include Sidekiq::Worker - sidekiq_options queue: queue - end - end end diff --git a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb index f8cf76cb339..94de208e53e 100644 --- a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb +++ b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb @@ -3,11 +3,12 @@ require Rails.root.join('db', 'post_migrate', '20190124200344_migrate_storage_mi describe MigrateStorageMigratorSidekiqQueue, :sidekiq, :redis do include Gitlab::Database::MigrationHelpers + include StubWorker context 'when there are jobs in the queues' do it 'correctly migrates queue when migrating up' do Sidekiq::Testing.disable! do - stubbed_worker(queue: :storage_migrator).perform_async(1, 5) + stub_worker(queue: :storage_migrator).perform_async(1, 5) described_class.new.up @@ -18,7 +19,7 @@ describe MigrateStorageMigratorSidekiqQueue, :sidekiq, :redis do it 'correctly migrates queue when migrating down' do Sidekiq::Testing.disable! do - stubbed_worker(queue: :'hashed_storage:hashed_storage_migrator').perform_async(1, 5) + stub_worker(queue: :'hashed_storage:hashed_storage_migrator').perform_async(1, 5) described_class.new.down @@ -37,11 +38,4 @@ describe MigrateStorageMigratorSidekiqQueue, :sidekiq, :redis do expect { described_class.new.down }.not_to raise_error end end - - def stubbed_worker(queue:) - Class.new do - include Sidekiq::Worker - sidekiq_options queue: queue - end - end end diff --git a/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb b/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb index 5e3b20ab4a8..976f3ce07d7 100644 --- a/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb +++ b/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb @@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20180307012445_migrate_update_hea describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis do include Gitlab::Database::MigrationHelpers + include StubWorker context 'when there are jobs in the queues' do it 'correctly migrates queue when migrating up' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1]) described_class.new.up @@ -19,10 +20,10 @@ describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis it 'does not affect other queues under the same namespace' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1]) - stubbed_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1]) described_class.new.up @@ -35,7 +36,7 @@ describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis it 'correctly migrates queue when migrating down' do Sidekiq::Testing.disable! do - stubbed_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1]) + stub_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1]) described_class.new.down @@ -54,11 +55,4 @@ describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis expect { described_class.new.down }.not_to raise_error end end - - def stubbed_worker(queue:) - Class.new do - include Sidekiq::Worker - sidekiq_options queue: queue - end - end end diff --git a/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb b/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb new file mode 100644 index 00000000000..e397fbb7138 --- /dev/null +++ b/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20190322132835_schedule_populate_merge_request_assignees_table.rb') + +describe SchedulePopulateMergeRequestAssigneesTable, :migration, :sidekiq do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') } + let(:merge_requests) { table(:merge_requests) } + + def create_merge_request(id) + params = { + id: id, + target_project_id: project.id, + target_branch: 'master', + source_project_id: project.id, + source_branch: 'mr name', + title: "mr name#{id}" + } + + merge_requests.create!(params) + end + + it 'correctly schedules background migrations' do + create_merge_request(1) + create_merge_request(2) + create_merge_request(3) + + stub_const("#{described_class.name}::BATCH_SIZE", 2) + + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(8.minutes, 1, 2) + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(16.minutes, 3, 3) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 789e14e8a20..314f0728b8e 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -117,14 +117,6 @@ describe ApplicationSetting do it { expect(setting.repository_storages).to eq(['default']) } end - context '#commit_email_hostname' do - it 'returns configured gitlab hostname if commit_email_hostname is not defined' do - setting.update(commit_email_hostname: nil) - - expect(setting.commit_email_hostname).to eq("users.noreply.#{Gitlab.config.gitlab.host}") - end - end - context 'auto_devops_domain setting' do context 'when auto_devops_enabled? is true' do before do @@ -182,15 +174,6 @@ describe ApplicationSetting do it { is_expected.not_to allow_value("").for(:repository_storages) } it { is_expected.not_to allow_value(nil).for(:repository_storages) } end - - describe '.pick_repository_storage' do - it 'uses Array#sample to pick a random storage' do - array = double('array', sample: 'random') - expect(setting).to receive(:repository_storages).and_return(array) - - expect(setting.pick_repository_storage).to eq('random') - end - end end context 'housekeeping settings' do @@ -367,65 +350,6 @@ describe ApplicationSetting do end end - context 'restricted signup domains' do - it 'sets single domain' do - setting.domain_whitelist_raw = 'example.com' - expect(setting.domain_whitelist).to eq(['example.com']) - end - - it 'sets multiple domains with spaces' do - setting.domain_whitelist_raw = 'example.com *.example.com' - expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) - end - - it 'sets multiple domains with newlines and a space' do - setting.domain_whitelist_raw = "example.com\n *.example.com" - expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) - end - - it 'sets multiple domains with commas' do - setting.domain_whitelist_raw = "example.com, *.example.com" - expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) - end - end - - context 'blacklisted signup domains' do - it 'sets single domain' do - setting.domain_blacklist_raw = 'example.com' - expect(setting.domain_blacklist).to contain_exactly('example.com') - end - - it 'sets multiple domains with spaces' do - setting.domain_blacklist_raw = 'example.com *.example.com' - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') - end - - it 'sets multiple domains with newlines and a space' do - setting.domain_blacklist_raw = "example.com\n *.example.com" - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') - end - - it 'sets multiple domains with commas' do - setting.domain_blacklist_raw = "example.com, *.example.com" - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') - end - - it 'sets multiple domains with semicolon' do - setting.domain_blacklist_raw = "example.com; *.example.com" - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') - end - - it 'sets multiple domains with mixture of everything' do - setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com" - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com') - end - - it 'sets multiple domain with file' do - setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt')) - expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar') - end - end - describe 'performance bar settings' do describe 'performance_bar_allowed_group' do context 'with no performance_bar_allowed_group_id saved' do @@ -462,142 +386,6 @@ describe ApplicationSetting do end end - describe 'usage ping settings' do - context 'when the usage ping is disabled in gitlab.yml' do - before do - allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false) - end - - it 'does not allow the usage ping to be configured' do - expect(setting.usage_ping_can_be_configured?).to be_falsey - end - - context 'when the usage ping is disabled in the DB' do - before do - setting.usage_ping_enabled = false - end - - it 'returns false for usage_ping_enabled' do - expect(setting.usage_ping_enabled).to be_falsey - end - end - - context 'when the usage ping is enabled in the DB' do - before do - setting.usage_ping_enabled = true - end - - it 'returns false for usage_ping_enabled' do - expect(setting.usage_ping_enabled).to be_falsey - end - end - end - - context 'when the usage ping is enabled in gitlab.yml' do - before do - allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true) - end - - it 'allows the usage ping to be configured' do - expect(setting.usage_ping_can_be_configured?).to be_truthy - end - - context 'when the usage ping is disabled in the DB' do - before do - setting.usage_ping_enabled = false - end - - it 'returns false for usage_ping_enabled' do - expect(setting.usage_ping_enabled).to be_falsey - end - end - - context 'when the usage ping is enabled in the DB' do - before do - setting.usage_ping_enabled = true - end - - it 'returns true for usage_ping_enabled' do - expect(setting.usage_ping_enabled).to be_truthy - end - end - end - end - - describe '#allowed_key_types' do - it 'includes all key types by default' do - expect(setting.allowed_key_types).to contain_exactly(*described_class::SUPPORTED_KEY_TYPES) - end - - it 'excludes disabled key types' do - expect(setting.allowed_key_types).to include(:ed25519) - - setting.ed25519_key_restriction = described_class::FORBIDDEN_KEY_VALUE - - expect(setting.allowed_key_types).not_to include(:ed25519) - end - end - - describe '#key_restriction_for' do - it 'returns the restriction value for recognised types' do - setting.rsa_key_restriction = 1024 - - expect(setting.key_restriction_for(:rsa)).to eq(1024) - end - - it 'allows types to be passed as a string' do - setting.rsa_key_restriction = 1024 - - expect(setting.key_restriction_for('rsa')).to eq(1024) - end - - it 'returns forbidden for unrecognised type' do - expect(setting.key_restriction_for(:foo)).to eq(described_class::FORBIDDEN_KEY_VALUE) - end - end - - describe '#allow_signup?' do - it 'returns true' do - expect(setting.allow_signup?).to be_truthy - end - - it 'returns false if signup is disabled' do - allow(setting).to receive(:signup_enabled?).and_return(false) - - expect(setting.allow_signup?).to be_falsey - end - - it 'returns false if password authentication is disabled for the web interface' do - allow(setting).to receive(:password_authentication_enabled_for_web?).and_return(false) - - expect(setting.allow_signup?).to be_falsey - end - end - - describe '#user_default_internal_regex_enabled?' do - using RSpec::Parameterized::TableSyntax - - where(:user_default_external, :user_default_internal_regex, :result) do - false | nil | false - false | '' | false - false | '^(?:(?!\.ext@).)*$\r?\n?' | false - true | '' | false - true | nil | false - true | '^(?:(?!\.ext@).)*$\r?\n?' | true - end - - with_them do - before do - setting.update(user_default_external: user_default_external) - setting.update(user_default_internal_regex: user_default_internal_regex) - end - - subject { setting.user_default_internal_regex_enabled? } - - it { is_expected.to eq(result) } - end - end - context 'diff limit settings' do describe '#diff_max_patch_bytes' do context 'validations' do @@ -613,23 +401,5 @@ describe ApplicationSetting do end end - describe '#archive_builds_older_than' do - subject { setting.archive_builds_older_than } - - context 'when the archive_builds_in_seconds is set' do - before do - setting.archive_builds_in_seconds = 3600 - end - - it { is_expected.to be_within(1.minute).of(1.hour.ago) } - end - - context 'when the archive_builds_in_seconds is set' do - before do - setting.archive_builds_in_seconds = nil - end - - it { is_expected.to be_nil } - end - end + it_behaves_like 'application settings examples' end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 89839709131..30ca07d5d2c 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -95,6 +95,12 @@ describe BroadcastMessage do end end + describe '#attributes' do + it 'includes message_html field' do + expect(subject.attributes.keys).to include("cached_markdown_version", "message_html") + end + end + describe '#active?' do it 'is truthy when started and not ended' do message = build(:broadcast_message) diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 9ca4241d7d8..3ec07143e93 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -24,6 +24,8 @@ describe Ci::Build do it { is_expected.to respond_to(:has_trace?) } it { is_expected.to respond_to(:trace) } it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) } + it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) } + it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) } it { is_expected.to be_a(ArtifactMigratable) } @@ -186,6 +188,37 @@ describe Ci::Build do end end + describe '#enqueue' do + let(:build) { create(:ci_build, :created) } + + subject { build.enqueue } + + before do + allow(build).to receive(:any_unmet_prerequisites?).and_return(has_prerequisites) + allow(Ci::PrepareBuildService).to receive(:perform_async) + end + + context 'build has unmet prerequisites' do + let(:has_prerequisites) { true } + + it 'transitions to preparing' do + subject + + expect(build).to be_preparing + end + end + + context 'build has no prerequisites' do + let(:has_prerequisites) { false } + + it 'transitions to pending' do + subject + + expect(build).to be_pending + end + end + end + describe '#actionize' do context 'when build is a created' do before do @@ -344,6 +377,18 @@ describe Ci::Build do expect(build).to be_pending end + + context 'build has unmet prerequisites' do + before do + allow(build).to receive(:prerequisites).and_return([double]) + end + + it 'transits to preparing' do + subject + + expect(build).to be_preparing + end + end end end @@ -2876,6 +2921,36 @@ describe Ci::Build do end end + describe '#any_unmet_prerequisites?' do + let(:build) { create(:ci_build, :created) } + + subject { build.any_unmet_prerequisites? } + + context 'build has prerequisites' do + before do + allow(build).to receive(:prerequisites).and_return([double]) + end + + it { is_expected.to be_truthy } + + context 'and the ci_preparing_state feature is disabled' do + before do + stub_feature_flags(ci_preparing_state: false) + end + + it { is_expected.to be_falsey } + end + end + + context 'build does not have prerequisites' do + before do + allow(build).to receive(:prerequisites).and_return([]) + end + + it { is_expected.to be_falsey } + end + end + describe '#yaml_variables' do let(:build) { create(:ci_build, pipeline: pipeline, yaml_variables: variables) } @@ -2928,6 +3003,20 @@ describe Ci::Build do end end + describe 'state transition: any => [:preparing]' do + let(:build) { create(:ci_build, :created) } + + before do + allow(build).to receive(:prerequisites).and_return([double]) + end + + it 'queues BuildPrepareWorker' do + expect(Ci::BuildPrepareWorker).to receive(:perform_async).with(build.id) + + build.enqueue + end + end + describe 'state transition: any => [:pending]' do let(:build) { create(:ci_build, :created) } @@ -3539,6 +3628,24 @@ describe Ci::Build do it { is_expected.to be_falsey } end end + + context 'when refspecs feature is required by build' do + before do + allow(build).to receive(:merge_request_ref?) { true } + end + + context 'when runner provides given feature' do + let(:runner_features) { { refspecs: true } } + + it { is_expected.to be_truthy } + end + + context 'when runner does not provide given feature' do + let(:runner_features) { {} } + + it { is_expected.to be_falsey } + end + end end describe '#deployment_status' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 7eeaa7a18ef..e5f3a9ce67a 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -362,6 +362,66 @@ describe Ci::Pipeline, :mailer do end end + describe '#merge_request_ref?' do + subject { pipeline.merge_request_ref? } + + it 'calls MergeRequest#merge_request_ref?' do + expect(MergeRequest).to receive(:merge_request_ref?).with(pipeline.ref) + + subject + end + end + + describe '#legacy_detached_merge_request_pipeline?' do + subject { pipeline.legacy_detached_merge_request_pipeline? } + + set(:merge_request) { create(:merge_request) } + let(:ref) { 'feature' } + let(:target_sha) { nil } + + let(:pipeline) do + build(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: ref, target_sha: target_sha) + end + + it { is_expected.to be_truthy } + + context 'when pipeline ref is a merge request ref' do + let(:ref) { 'refs/merge-requests/1/head' } + + it { is_expected.to be_falsy } + end + + context 'when target sha is set' do + let(:target_sha) { 'target-sha' } + + it { is_expected.to be_falsy } + end + end + + describe '#matches_sha_or_source_sha?' do + subject { pipeline.matches_sha_or_source_sha?(sample_sha) } + + let(:sample_sha) { Digest::SHA1.hexdigest(SecureRandom.hex) } + + context 'when sha matches' do + let(:pipeline) { build(:ci_pipeline, sha: sample_sha) } + + it { is_expected.to be_truthy } + end + + context 'when source_sha matches' do + let(:pipeline) { build(:ci_pipeline, source_sha: sample_sha) } + + it { is_expected.to be_truthy } + end + + context 'when both sha and source_sha do not matche' do + let(:pipeline) { build(:ci_pipeline, sha: 'test', source_sha: 'test') } + + it { is_expected.to be_falsy } + end + end + describe '.triggered_for_branch' do subject { described_class.triggered_for_branch(ref) } @@ -1201,16 +1261,28 @@ describe Ci::Pipeline, :mailer do end describe '#started_at' do - it 'updates on transitioning to running' do - build.run + let(:pipeline) { create(:ci_empty_pipeline, status: from_status) } + + %i[created preparing pending].each do |status| + context "from #{status}" do + let(:from_status) { status } - expect(pipeline.reload.started_at).not_to be_nil + it 'updates on transitioning to running' do + pipeline.run + + expect(pipeline.started_at).not_to be_nil + end + end end - it 'does not update on transitioning to success' do - build.success + context 'from created' do + let(:from_status) { :created } + + it 'does not update on transitioning to success' do + pipeline.succeed - expect(pipeline.reload.started_at).to be_nil + expect(pipeline.started_at).to be_nil + end end end @@ -1229,27 +1301,49 @@ describe Ci::Pipeline, :mailer do end describe 'merge request metrics' do - let(:project) { create(:project, :repository) } - let(:pipeline) { FactoryBot.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) } - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) } + let(:pipeline) { create(:ci_empty_pipeline, status: from_status) } before do expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id) end context 'when transitioning to running' do - it 'schedules metrics workers' do - pipeline.run + %i[created preparing pending].each do |status| + context "from #{status}" do + let(:from_status) { status } + + it 'schedules metrics workers' do + pipeline.run + end + end end end context 'when transitioning to success' do + let(:from_status) { 'created' } + it 'schedules metrics workers' do pipeline.succeed end end end + describe 'merge on success' do + let(:pipeline) { create(:ci_empty_pipeline, status: from_status) } + + %i[created preparing pending running].each do |status| + context "from #{status}" do + let(:from_status) { status } + + it 'schedules pipeline success worker' do + expect(PipelineSuccessWorker).to receive(:perform_async).with(pipeline.id) + + pipeline.succeed + end + end + end + end + describe 'pipeline caching' do it 'performs ExpirePipelinesCacheWorker' do expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) @@ -1402,6 +1496,14 @@ describe Ci::Pipeline, :mailer do end end + context 'with a branch name as the ref' do + it 'looks up commit with the full ref name' do + expect(pipeline.project).to receive(:commit).with('refs/heads/master').and_call_original + + expect(pipeline).to be_latest + end + end + context 'with not latest sha' do before do pipeline.update( @@ -1768,6 +1870,18 @@ describe Ci::Pipeline, :mailer do subject { pipeline.reload.status } + context 'on prepare' do + before do + # Prevent skipping directly to 'pending' + allow(build).to receive(:prerequisites).and_return([double]) + allow(Ci::BuildPrepareWorker).to receive(:perform_async) + + build.enqueue + end + + it { is_expected.to eq('preparing') } + end + context 'on queuing' do before do build.enqueue diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index bf425a2617c..054ed0be240 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -107,7 +107,7 @@ describe Clusters::Applications::Knative do subject { knative.install_command } it 'should be initialized with latest version' do - expect(subject.version).to eq('0.2.2') + expect(subject.version).to eq('0.3.0') end it_behaves_like 'a command' diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 6972fc03415..3ce8aa1c7bc 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -22,7 +22,7 @@ describe Clusters::Applications::Runner do it 'should be initialized with 4 arguments' do expect(subject.name).to eq('runner') expect(subject.chart).to eq('runner/gitlab-runner') - expect(subject.version).to eq('0.2.0') + expect(subject.version).to eq('0.3.0') expect(subject).to be_rbac expect(subject.repository).to eq('https://charts.gitlab.io') expect(subject.files).to eq(gitlab_runner.files) @@ -40,7 +40,7 @@ describe Clusters::Applications::Runner do let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') } it 'should be initialized with the locked version' do - expect(subject.version).to eq('0.2.0') + expect(subject.version).to eq('0.3.0') end end end @@ -64,24 +64,45 @@ describe Clusters::Applications::Runner do end context 'without a runner' do - let(:project) { create(:project) } - let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } let(:application) { create(:clusters_applications_runner, runner: nil, cluster: cluster) } + let(:runner) { application.runner } - it 'creates a runner' do - expect do - subject - end.to change { Ci::Runner.count }.by(1) + shared_examples 'runner creation' do + it 'creates a runner' do + expect { subject }.to change { Ci::Runner.count }.by(1) + end + + it 'uses the new runner token' do + expect(values).to match(/runnerToken: '?#{runner.token}/) + end end - it 'uses the new runner token' do - expect(values).to match(/runnerToken: '?#{application.reload.runner.token}/) + context 'project cluster' do + let(:project) { create(:project) } + let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } + + include_examples 'runner creation' + + it 'creates a project runner' do + subject + + expect(runner).to be_project_type + expect(runner.projects).to eq [project] + end end - it 'assigns the new runner to runner' do - subject + context 'group cluster' do + let(:group) { create(:group) } + let(:cluster) { create(:cluster, :with_installed_helm, cluster_type: :group_type, groups: [group]) } + + include_examples 'runner creation' + + it 'creates a group runner' do + subject - expect(application.reload.runner).to be_project_type + expect(runner).to be_group_type + expect(runner.groups).to eq [group] + end end end diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index acbcdc7d170..fabd2806d9a 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -620,4 +620,20 @@ describe Clusters::Cluster do end end end + + describe '#provided_by_user?' do + subject { cluster.provided_by_user? } + + context 'with a GCP provider' do + let(:cluster) { create(:cluster, :provided_by_gcp) } + + it { is_expected.to be_falsy } + end + + context 'with an user provider' do + let(:cluster) { create(:cluster, :provided_by_user) } + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index cc93a1b4965..a79b436c22a 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -15,7 +15,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching it { is_expected.to delegate_method(:project).to(:cluster) } it { is_expected.to delegate_method(:enabled?).to(:cluster) } - it { is_expected.to delegate_method(:managed?).to(:cluster) } + it { is_expected.to delegate_method(:provided_by_user?).to(:cluster) } it { is_expected.to delegate_method(:kubernetes_namespace).to(:cluster) } it_behaves_like 'having unique enum values' @@ -375,14 +375,14 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end context 'with valid pods' do - let(:pod) { kube_pod(app: environment.slug) } - let(:pod_with_no_terminal) { kube_pod(app: environment.slug, status: "Pending") } + let(:pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug) } + let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") } let(:terminals) { kube_terminals(service, pod) } before do stub_reactive_cache( service, - pods: [pod, pod, pod_with_no_terminal, kube_pod(app: "should-be-filtered-out")] + pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] ) end diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb index 12e59b35428..dcbe36de1e2 100644 --- a/spec/models/commit_collection_spec.rb +++ b/spec/models/commit_collection_spec.rb @@ -37,12 +37,92 @@ describe CommitCollection do describe '#without_merge_commits' do it 'returns all commits except merge commits' do + merge_commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98") + expect(merge_commit).to receive(:merge_commit?).and_return(true) + collection = described_class.new(project, [ - build(:commit), - build(:commit, :merge_commit) + commit, + merge_commit ]) - expect(collection.without_merge_commits.size).to eq(1) + expect(collection.without_merge_commits).to contain_exactly(commit) + end + end + + describe 'enrichment methods' do + let(:gitaly_commit) { commit } + let(:hash_commit) { Commit.from_hash(gitaly_commit.to_hash, project) } + + describe '#unenriched' do + it 'returns all commits that are not backed by gitaly data' do + collection = described_class.new(project, [gitaly_commit, hash_commit]) + + expect(collection.unenriched).to contain_exactly(hash_commit) + end + end + + describe '#fully_enriched?' do + it 'returns true when all commits are backed by gitaly data' do + collection = described_class.new(project, [gitaly_commit, gitaly_commit]) + + expect(collection.fully_enriched?).to eq(true) + end + + it 'returns false when any commits are not backed by gitaly data' do + collection = described_class.new(project, [gitaly_commit, hash_commit]) + + expect(collection.fully_enriched?).to eq(false) + end + + it 'returns true when the collection is empty' do + collection = described_class.new(project, []) + + expect(collection.fully_enriched?).to eq(true) + end + end + + describe '#enrich!' do + it 'replaces commits in the collection with those backed by gitaly data' do + collection = described_class.new(project, [hash_commit]) + + collection.enrich! + + new_commit = collection.commits.first + expect(new_commit.id).to eq(hash_commit.id) + expect(hash_commit.gitaly_commit?).to eq(false) + expect(new_commit.gitaly_commit?).to eq(true) + end + + it 'maintains the original order of the commits' do + gitaly_commits = [gitaly_commit] * 3 + hash_commits = [hash_commit] * 3 + # Interleave the gitaly and hash commits together + original_commits = gitaly_commits.zip(hash_commits).flatten + collection = described_class.new(project, original_commits) + + collection.enrich! + + original_commits.each_with_index do |original_commit, i| + new_commit = collection.commits[i] + expect(original_commit.id).to eq(new_commit.id) + end + end + + it 'fetches data if there are unenriched commits' do + collection = described_class.new(project, [hash_commit]) + + expect(Commit).to receive(:lazy).exactly(:once) + + collection.enrich! + end + + it 'does not fetch data if all commits are enriched' do + collection = described_class.new(project, [gitaly_commit]) + + expect(Commit).not_to receive(:lazy) + + collection.enrich! + end end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 8b7c88805c1..e2b7f5c6ee2 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -49,6 +49,16 @@ describe CommitStatus do commit_status.success! end + + describe 'transitioning to running' do + let(:commit_status) { create(:commit_status, :pending, started_at: nil) } + + it 'records the started at time' do + commit_status.run! + + expect(commit_status.started_at).to be_present + end + end end describe '#started?' do @@ -479,6 +489,12 @@ describe CommitStatus do it { is_expected.to be_script_failure } end + + context 'when failure_reason is unmet_prerequisites' do + let(:reason) { :unmet_prerequisites } + + it { is_expected.to be_unmet_prerequisites } + end end describe 'ensure stage assignment' do @@ -555,6 +571,7 @@ describe CommitStatus do before do allow(Time).to receive(:now).and_return(current_time) + expect(commit_status.any_unmet_prerequisites?).to eq false end shared_examples 'commit status enqueued' do @@ -569,6 +586,12 @@ describe CommitStatus do it_behaves_like 'commit status enqueued' end + context 'when initial state is :preparing' do + let(:commit_status) { create(:commit_status, :preparing) } + + it_behaves_like 'commit status enqueued' + end + context 'when initial state is :skipped' do let(:commit_status) { create(:commit_status, :skipped) } diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb index 447279f19a8..7d555f15e39 100644 --- a/spec/models/concerns/cache_markdown_field_spec.rb +++ b/spec/models/concerns/cache_markdown_field_spec.rb @@ -23,6 +23,7 @@ describe CacheMarkdownField do include CacheMarkdownField cache_markdown_field :foo cache_markdown_field :baz, pipeline: :single_line + cache_markdown_field :zoo, whitelisted: true def self.add_attr(name) self.attribute_names += [name] @@ -35,7 +36,7 @@ describe CacheMarkdownField do add_attr :cached_markdown_version - [:foo, :foo_html, :bar, :baz, :baz_html].each do |name| + [:foo, :foo_html, :bar, :baz, :baz_html, :zoo, :zoo_html].each do |name| add_attr(name) end @@ -84,8 +85,8 @@ describe CacheMarkdownField do end describe '.attributes' do - it 'excludes cache attributes' do - expect(thing.attributes.keys.sort).to eq(%w[bar baz foo]) + it 'excludes cache attributes that is blacklisted by default' do + expect(thing.attributes.keys.sort).to eq(%w[bar baz cached_markdown_version foo zoo zoo_html]) end end @@ -297,7 +298,12 @@ describe CacheMarkdownField do it 'saves the changes using #update_columns' do expect(thing).to receive(:persisted?).and_return(true) expect(thing).to receive(:update_columns) - .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version) + .with( + "foo_html" => updated_html, + "baz_html" => "", + "zoo_html" => "", + "cached_markdown_version" => cache_version + ) thing.refresh_markdown_cache! end diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb index 6b1038cb8fd..e8b1eba67cc 100644 --- a/spec/models/concerns/has_status_spec.rb +++ b/spec/models/concerns/has_status_spec.rb @@ -34,6 +34,22 @@ describe HasStatus do it { is_expected.to eq 'running' } end + context 'all preparing' do + let!(:statuses) do + [create(type, status: :preparing), create(type, status: :preparing)] + end + + it { is_expected.to eq 'preparing' } + end + + context 'at least one preparing' do + let!(:statuses) do + [create(type, status: :success), create(type, status: :preparing)] + end + + it { is_expected.to eq 'preparing' } + end + context 'success and failed but allowed to fail' do let!(:statuses) do [create(type, status: :success), @@ -188,7 +204,7 @@ describe HasStatus do end end - %i[created running pending success + %i[created preparing running pending success failed canceled skipped].each do |status| it_behaves_like 'having a job', status end @@ -234,7 +250,7 @@ describe HasStatus do describe '.alive' do subject { CommitStatus.alive } - %i[running pending created].each do |status| + %i[running pending preparing created].each do |status| it_behaves_like 'containing the job', status end @@ -270,7 +286,7 @@ describe HasStatus do describe '.cancelable' do subject { CommitStatus.cancelable } - %i[running pending created scheduled].each do |status| + %i[running pending preparing created scheduled].each do |status| it_behaves_like 'containing the job', status end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index a8d53cfcd7d..5fce9504334 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -356,4 +356,32 @@ describe Deployment do end end end + + describe '#cluster' do + let(:deployment) { create(:deployment) } + let(:project) { deployment.project } + let(:environment) { deployment.environment } + + subject { deployment.cluster } + + before do + expect(project).to receive(:deployment_platform) + .with(environment: environment.name).and_call_original + end + + context 'project has no deployment platform' do + before do + expect(project.clusters).to be_empty + end + + it { is_expected.to be_nil } + end + + context 'project has a deployment platform' do + let!(:cluster) { create(:cluster, projects: [project]) } + let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) } + + it { is_expected.to eq cluster } + end + end end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index fda00a693f0..67e5f4f7e41 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -336,6 +336,16 @@ describe DiffNote do end end + describe '#banzai_render_context' do + let(:note) { create(:diff_note_on_merge_request) } + + it 'includes expected context' do + context = note.banzai_render_context(:note) + + expect(context).to include(suggestions_filter_enabled: true, noteable: note.noteable, project: note.project) + end + end + describe "image diff notes" do subject { build(:image_diff_note_on_merge_request, project: project, noteable: merge_request) } diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb index 9da16dea929..2576a9aba06 100644 --- a/spec/models/environment_status_spec.rb +++ b/spec/models/environment_status_spec.rb @@ -64,8 +64,8 @@ describe EnvironmentStatus do end describe '.for_merge_request' do - let(:admin) { create(:admin) } - let(:pipeline) { create(:ci_pipeline, sha: sha) } + let(:admin) { create(:admin) } + let!(:pipeline) { create(:ci_pipeline, sha: sha, merge_requests_as_head_pipeline: [merge_request]) } it 'is based on merge_request.diff_head_sha' do expect(merge_request).to receive(:diff_head_sha) diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb index cbde13a2c7a..21e381d9fb7 100644 --- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb +++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb @@ -167,7 +167,7 @@ describe ErrorTracking::ProjectErrorTrackingSetting do end end - context 'when sentry client raises exception' do + context 'when sentry client raises Sentry::Client::Error' do let(:sentry_client) { spy(:sentry_client) } before do @@ -179,7 +179,31 @@ describe ErrorTracking::ProjectErrorTrackingSetting do end it 'returns error' do - expect(result).to eq(error: 'error message') + expect(result).to eq( + error: 'error message', + error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE + ) + expect(subject).to have_received(:sentry_client) + expect(sentry_client).to have_received(:list_issues) + end + end + + context 'when sentry client raises Sentry::Client::MissingKeysError' do + let(:sentry_client) { spy(:sentry_client) } + + before do + synchronous_reactive_cache(subject) + + allow(subject).to receive(:sentry_client).and_return(sentry_client) + allow(sentry_client).to receive(:list_issues).with(opts) + .and_raise(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"') + end + + it 'returns error' do + expect(result).to eq( + error: 'Sentry API response is missing keys. key not found: "id"', + error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS + ) expect(subject).to have_received(:sentry_client) expect(sentry_client).to have_received(:list_issues) end diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index a3451c67bd8..bc937368cff 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -1,6 +1,22 @@ require 'spec_helper' describe GroupMember do + describe '.count_users_by_group_id' do + it 'counts users by group ID' do + user_1 = create(:user) + user_2 = create(:user) + group_1 = create(:group) + group_2 = create(:group) + + group_1.add_owner(user_1) + group_1.add_owner(user_2) + group_2.add_owner(user_1) + + expect(described_class.count_users_by_group_id).to eq(group_1.id => 2, + group_2.id => 1) + end + end + describe '.access_level_roles' do it 'returns Gitlab::Access.options_with_owner' do expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner) diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 53f5307ea0b..0f00ea7e85e 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -51,7 +51,104 @@ describe MergeRequestDiff do end end - describe '#latest' do + describe '.ids_for_external_storage_migration' do + set(:merge_request) { create(:merge_request) } + set(:outdated) { merge_request.merge_request_diff } + set(:latest) { merge_request.create_merge_request_diff } + + set(:closed_mr) { create(:merge_request, :closed_last_month) } + let(:closed) { closed_mr.merge_request_diff } + + set(:merged_mr) { create(:merge_request, :merged_last_month) } + let(:merged) { merged_mr.merge_request_diff } + + set(:recently_closed_mr) { create(:merge_request, :closed) } + let(:closed_recently) { recently_closed_mr.merge_request_diff } + + set(:recently_merged_mr) { create(:merge_request, :merged) } + let(:merged_recently) { recently_merged_mr.merge_request_diff } + + before do + merge_request.update!(latest_merge_request_diff: latest) + end + + subject { described_class.ids_for_external_storage_migration(limit: 1000) } + + context 'external diffs are disabled' do + before do + stub_external_diffs_setting(enabled: false) + end + + it { is_expected.to be_empty } + end + + context 'external diffs are misconfigured' do + before do + stub_external_diffs_setting(enabled: true, when: 'every second tuesday') + end + + it { is_expected.to be_empty } + end + + context 'external diffs are enabled unconditionally' do + before do + stub_external_diffs_setting(enabled: true) + end + + it { is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id, closed_recently.id, merged_recently.id) } + end + + context 'external diffs are enabled for outdated diffs' do + before do + stub_external_diffs_setting(enabled: true, when: 'outdated') + end + + it 'returns records for outdated merge request versions' do + is_expected.to contain_exactly(outdated.id, closed.id, merged.id) + end + end + + context 'with limit' do + it 'respects the limit' do + stub_external_diffs_setting(enabled: true) + + expect(described_class.ids_for_external_storage_migration(limit: 3).count).to eq(3) + end + end + end + + describe '#migrate_files_to_external_storage!' do + let(:diff) { create(:merge_request).merge_request_diff } + + it 'converts from in-database to external storage' do + expect(diff).not_to be_stored_externally + + stub_external_diffs_setting(enabled: true) + expect(diff).to receive(:save!) + + diff.migrate_files_to_external_storage! + + expect(diff).to be_stored_externally + end + + it 'does nothing with an external diff' do + stub_external_diffs_setting(enabled: true) + + expect(diff).to be_stored_externally + expect(diff).not_to receive(:save!) + + diff.migrate_files_to_external_storage! + end + + it 'does nothing if external diffs are disabled' do + expect(diff).not_to be_stored_externally + expect(diff).not_to receive(:save!) + + diff.migrate_files_to_external_storage! + end + end + + describe '#latest?' do let!(:mr) { create(:merge_request, :with_diffs) } let!(:first_diff) { mr.merge_request_diff } let!(:last_diff) { mr.create_merge_request_diff } @@ -222,14 +319,58 @@ describe MergeRequestDiff do include_examples 'merge request diffs' end - describe 'external diffs configured' do + describe 'external diffs always enabled' do before do - stub_external_diffs_setting(enabled: true) + stub_external_diffs_setting(enabled: true, when: 'always') end include_examples 'merge request diffs' end + describe 'exernal diffs enabled for outdated diffs' do + before do + stub_external_diffs_setting(enabled: true, when: 'outdated') + end + + include_examples 'merge request diffs' + + it 'stores up-to-date diffs in the database' do + expect(diff).not_to be_stored_externally + end + + it 'stores diffs for recently closed MRs in the database' do + mr = create(:merge_request, :closed) + + expect(mr.merge_request_diff).not_to be_stored_externally + end + + it 'stores diffs for recently merged MRs in the database' do + mr = create(:merge_request, :merged) + + expect(mr.merge_request_diff).not_to be_stored_externally + end + + it 'stores diffs for old MR versions in external storage' do + old_diff = diff + merge_request.create_merge_request_diff + old_diff.migrate_files_to_external_storage! + + expect(old_diff).to be_stored_externally + end + + it 'stores diffs for old closed MRs in external storage' do + mr = create(:merge_request, :closed_last_month) + + expect(mr.merge_request_diff).to be_stored_externally + end + + it 'stores diffs for old merged MRs in external storage' do + mr = create(:merge_request, :merged_last_month) + + expect(mr.merge_request_diff).to be_stored_externally + end + end + describe '#commit_shas' do it 'returns all commit SHAs using commits from the DB' do expect(diff_with_commits.commit_shas).not_to be_empty diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c3f87edb0c6..94825af42c7 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -84,32 +84,27 @@ describe MergeRequest do describe '#default_squash_commit_message' do let(:project) { subject.project } - - def commit_collection(commit_hashes) - raw_commits = commit_hashes.map { |raw| Commit.from_hash(raw, project) } - - CommitCollection.new(project, raw_commits) - end + let(:is_multiline) { -> (c) { c.description.present? } } + let(:multiline_commits) { subject.commits.select(&is_multiline) } + let(:singleline_commits) { subject.commits.reject(&is_multiline) } it 'returns the oldest multiline commit message' do - commits = commit_collection([ - { message: 'Singleline', parent_ids: [] }, - { message: "Second multiline\nCommit message", parent_ids: [] }, - { message: "First multiline\nCommit message", parent_ids: [] } - ]) - - expect(subject).to receive(:commits).and_return(commits) - - expect(subject.default_squash_commit_message).to eq("First multiline\nCommit message") + expect(subject.default_squash_commit_message).to eq(multiline_commits.last.message) end it 'returns the merge request title if there are no multiline commits' do - commits = commit_collection([ - { message: 'Singleline', parent_ids: [] } - ]) + expect(subject).to receive(:commits).and_return( + CommitCollection.new(project, singleline_commits) + ) - expect(subject).to receive(:commits).and_return(commits) + expect(subject.default_squash_commit_message).to eq(subject.title) + end + + it 'does not return commit messages from multiline merge commits' do + collection = CommitCollection.new(project, multiline_commits).enrich! + expect(collection.commits).to all( receive(:merge_commit?).and_return(true) ) + expect(subject).to receive(:commits).and_return(collection) expect(subject.default_squash_commit_message).to eq(subject.title) end end @@ -184,6 +179,31 @@ describe MergeRequest do expect(MergeRequest::Metrics.count).to eq(1) end end + + describe '#refresh_merge_request_assignees' do + set(:user) { create(:user) } + + it 'creates merge request assignees relation upon MR creation' do + merge_request = create(:merge_request, assignee: nil) + + expect(merge_request.merge_request_assignees).to be_empty + + expect { merge_request.update!(assignee: user) } + .to change { merge_request.reload.merge_request_assignees.count } + .from(0).to(1) + end + + it 'updates merge request assignees relation upon MR assignee change' do + another_user = create(:user) + merge_request = create(:merge_request, assignee: user) + + expect { merge_request.update!(assignee: another_user) } + .to change { merge_request.reload.merge_request_assignees.first.assignee } + .from(user).to(another_user) + + expect(merge_request.merge_request_assignees.count).to eq(1) + end + end end describe 'respond to' do @@ -1044,7 +1064,7 @@ describe MergeRequest do describe '#committers' do it 'returns all the committers of every commit in the merge request' do - users = subject.commits.map(&:committer_email).uniq.map do |email| + users = subject.commits.without_merge_commits.map(&:committer_email).uniq.map do |email| create(:user, email: email) end @@ -1172,8 +1192,10 @@ describe MergeRequest do end context 'head pipeline' do + let(:diff_head_sha) { Digest::SHA1.hexdigest(SecureRandom.hex) } + before do - allow(subject).to receive(:diff_head_sha).and_return('lastsha') + allow(subject).to receive(:diff_head_sha).and_return(diff_head_sha) end describe '#head_pipeline' do @@ -1201,7 +1223,15 @@ describe MergeRequest do end it 'returns the pipeline for MR with recent pipeline' do - pipeline = create(:ci_empty_pipeline, sha: 'lastsha') + pipeline = create(:ci_empty_pipeline, sha: diff_head_sha) + subject.update_attribute(:head_pipeline_id, pipeline.id) + + expect(subject.actual_head_pipeline).to eq(subject.head_pipeline) + expect(subject.actual_head_pipeline).to eq(pipeline) + end + + it 'returns the pipeline for MR with recent merge request pipeline' do + pipeline = create(:ci_empty_pipeline, sha: 'merge-sha', source_sha: diff_head_sha) subject.update_attribute(:head_pipeline_id, pipeline.id) expect(subject.actual_head_pipeline).to eq(subject.head_pipeline) @@ -3070,4 +3100,32 @@ describe MergeRequest do end end end + + describe '.merge_request_ref?' do + subject { described_class.merge_request_ref?(ref) } + + context 'when ref is ref name of a branch' do + let(:ref) { 'feature' } + + it { is_expected.to be_falsey } + end + + context 'when ref is HEAD ref path of a branch' do + let(:ref) { 'refs/heads/feature' } + + it { is_expected.to be_falsey } + end + + context 'when ref is HEAD ref path of a merge request' do + let(:ref) { 'refs/merge-requests/1/head' } + + it { is_expected.to be_truthy } + end + + context 'when ref is merge ref path of a merge request' do + let(:ref) { 'refs/merge-requests/1/merge' } + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 47f70e6648a..56e587262ef 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -323,13 +323,14 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do end context 'with valid pods' do - let(:pod) { kube_pod(app: environment.slug) } + let(:pod) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug) } + let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") } let(:terminals) { kube_terminals(service, pod) } before do stub_reactive_cache( service, - pods: [pod, pod, kube_pod(app: "should-be-filtered-out")] + pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")] ) end @@ -360,14 +361,16 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when kubernetes responds with valid pods' do before do stub_kubeclient_pods + stub_kubeclient_deployments # Used by EE end - it { is_expected.to eq(pods: [kube_pod]) } + it { is_expected.to include(pods: [kube_pod]) } end context 'when kubernetes responds with 500s' do before do stub_kubeclient_pods(status: 500) + stub_kubeclient_deployments(status: 500) # Used by EE end it { expect { subject }.to raise_error(Kubeclient::HttpError) } @@ -376,9 +379,10 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when kubernetes responds with 404s' do before do stub_kubeclient_pods(status: 404) + stub_kubeclient_deployments(status: 404) # Used by EE end - it { is_expected.to eq(pods: []) } + it { is_expected.to include(pods: []) } end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 328133e5c3c..90dcf861849 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2379,6 +2379,12 @@ describe Project do project.change_head(project.default_branch) end + it 'updates commit count' do + expect(ProjectCacheWorker).to receive(:perform_async).with(project.id, [], [:commit_count]) + + project.change_head(project.default_branch) + end + it 'copies the gitattributes' do expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch) project.change_head(project.default_branch) @@ -2704,7 +2710,7 @@ describe Project do end describe '#any_lfs_file_locks?', :request_store do - set(:project) { create(:project) } + let!(:project) { create(:project) } it 'returns false when there are no LFS file locks' do expect(project.any_lfs_file_locks?).to be_falsey @@ -3142,6 +3148,53 @@ describe Project do expect(projects).to eq([public_project]) end end + + context 'with requested visibility levels' do + set(:internal_project) { create(:project, :internal, :repository) } + set(:private_project_2) { create(:project, :private) } + + context 'with admin user' do + set(:admin) { create(:admin) } + + it 'returns all projects' do + projects = described_class.all.public_or_visible_to_user(admin, []) + + expect(projects).to match_array([public_project, private_project, private_project_2, internal_project]) + end + + it 'returns all public and private projects' do + projects = described_class.all.public_or_visible_to_user(admin, [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) + + expect(projects).to match_array([public_project, private_project, private_project_2]) + end + + it 'returns all private projects' do + projects = described_class.all.public_or_visible_to_user(admin, [Gitlab::VisibilityLevel::PRIVATE]) + + expect(projects).to match_array([private_project, private_project_2]) + end + end + + context 'with regular user' do + it 'returns authorized projects' do + projects = described_class.all.public_or_visible_to_user(user, []) + + expect(projects).to match_array([public_project, private_project, internal_project]) + end + + it "returns user's public and private projects" do + projects = described_class.all.public_or_visible_to_user(user, [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::PRIVATE]) + + expect(projects).to match_array([public_project, private_project]) + end + + it 'returns one private project' do + projects = described_class.all.public_or_visible_to_user(user, [Gitlab::VisibilityLevel::PRIVATE]) + + expect(projects).to eq([private_project]) + end + end + end end describe '.with_feature_available_for_user' do @@ -3422,7 +3475,7 @@ describe Project do end it 'schedules HashedStorage::ProjectMigrateWorker with delayed start when the project repo is in use' do - Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: false)).increase + Gitlab::ReferenceCounter.new(Gitlab::GlRepository::PROJECT.identifier_for_subject(project)).increase expect(HashedStorage::ProjectMigrateWorker).to receive(:perform_in) @@ -3430,7 +3483,7 @@ describe Project do end it 'schedules HashedStorage::ProjectMigrateWorker with delayed start when the wiki repo is in use' do - Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: true)).increase + Gitlab::ReferenceCounter.new(Gitlab::GlRepository::WIKI.identifier_for_subject(project)).increase expect(HashedStorage::ProjectMigrateWorker).to receive(:perform_in) @@ -3563,16 +3616,6 @@ describe Project do end end - describe '#gl_repository' do - let(:project) { create(:project) } - - it 'delegates to Gitlab::GlRepository.gl_repository' do - expect(Gitlab::GlRepository).to receive(:gl_repository).with(project, true) - - project.gl_repository(is_wiki: true) - end - end - describe '#has_ci?' do set(:project) { create(:project) } let(:repository) { double } diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 3ccc706edf2..7be8d67ba9e 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -71,6 +71,14 @@ describe ProjectWiki do expect(project_wiki.create_page("index", "test content")).to be_truthy end + it "creates a new wiki repo with a default commit message" do + expect(project_wiki.create_page("index", "test content", :markdown, "")).to be_truthy + + page = project_wiki.find_page('index') + + expect(page.last_version.message).to eq("#{user.username} created page: index") + end + it "raises CouldNotCreateWikiError if it can't create the wiki repository" do # Create a fresh project which will not have a wiki project_wiki = described_class.new(create(:project), user) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 85b157a9435..1be29d039a7 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -660,6 +660,68 @@ describe User do end end + describe '#highest_role' do + let(:user) { create(:user) } + + let(:group) { create(:group) } + + it 'returns NO_ACCESS if none has been set' do + expect(user.highest_role).to eq(Gitlab::Access::NO_ACCESS) + end + + it 'returns MAINTAINER if user is maintainer of a project' do + create(:project, group: group) do |project| + project.add_maintainer(user) + end + + expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER) + end + + it 'returns the highest role if user is member of multiple projects' do + create(:project, group: group) do |project| + project.add_maintainer(user) + end + + create(:project, group: group) do |project| + project.add_developer(user) + end + + expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER) + end + + it 'returns MAINTAINER if user is maintainer of a group' do + create(:group) do |group| + group.add_user(user, GroupMember::MAINTAINER) + end + + expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER) + end + + it 'returns the highest role if user is member of multiple groups' do + create(:group) do |group| + group.add_user(user, GroupMember::MAINTAINER) + end + + create(:group) do |group| + group.add_user(user, GroupMember::DEVELOPER) + end + + expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER) + end + + it 'returns the highest role if user is member of multiple groups and projects' do + create(:group) do |group| + group.add_user(user, GroupMember::DEVELOPER) + end + + create(:project, group: group) do |project| + project.add_maintainer(user) + end + + expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER) + end + end + describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do let(:request) { OpenStruct.new(remote_ip: "127.0.0.1") } let(:user) { create(:user) } diff --git a/spec/policies/board_policy_spec.rb b/spec/policies/board_policy_spec.rb index 4b76d65ef69..52c23951e37 100644 --- a/spec/policies/board_policy_spec.rb +++ b/spec/policies/board_policy_spec.rb @@ -17,14 +17,6 @@ describe BoardPolicy do ] end - def expect_allowed(*permissions) - permissions.each { |p| is_expected.to be_allowed(p) } - end - - def expect_disallowed(*permissions) - permissions.each { |p| is_expected.not_to be_allowed(p) } - end - context 'group board' do subject { described_class.new(user, group_board) } diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 92bdaa8b8b8..dc98baca6dc 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -1,63 +1,7 @@ require 'spec_helper' describe GroupPolicy do - let(:guest) { create(:user) } - let(:reporter) { create(:user) } - let(:developer) { create(:user) } - let(:maintainer) { create(:user) } - let(:owner) { create(:user) } - let(:admin) { create(:admin) } - let(:group) { create(:group, :private) } - - let(:guest_permissions) do - [:read_label, :read_group, :upload_file, :read_namespace, :read_group_activity, - :read_group_issues, :read_group_boards, :read_group_labels, :read_group_milestones, - :read_group_merge_requests] - end - - let(:reporter_permissions) { [:admin_label] } - - let(:developer_permissions) { [:admin_milestone] } - - let(:maintainer_permissions) do - [ - :create_projects, - :read_cluster, - :create_cluster, - :update_cluster, - :admin_cluster, - :add_cluster - ] - end - - let(:owner_permissions) do - [ - :admin_group, - :admin_namespace, - :admin_group_member, - :change_visibility_level, - :set_note_created_at, - (Gitlab::Database.postgresql? ? :create_subgroup : nil) - ].compact - end - - before do - group.add_guest(guest) - group.add_reporter(reporter) - group.add_developer(developer) - group.add_maintainer(maintainer) - group.add_owner(owner) - end - - subject { described_class.new(current_user, group) } - - def expect_allowed(*permissions) - permissions.each { |p| is_expected.to be_allowed(p) } - end - - def expect_disallowed(*permissions) - permissions.each { |p| is_expected.not_to be_allowed(p) } - end + include_context 'GroupPolicy context' context 'with no user' do let(:group) { create(:group, :public) } diff --git a/spec/policies/identity_provider_policy_spec.rb b/spec/policies/identity_provider_policy_spec.rb new file mode 100644 index 00000000000..2520469d4e7 --- /dev/null +++ b/spec/policies/identity_provider_policy_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe IdentityProviderPolicy do + subject(:policy) { described_class.new(user, provider) } + let(:user) { User.new } + let(:provider) { :a_provider } + + describe '#rules' do + it { is_expected.to be_allowed(:link) } + it { is_expected.to be_allowed(:unlink) } + + context 'when user is anonymous' do + let(:user) { nil } + + it { is_expected.not_to be_allowed(:link) } + it { is_expected.not_to be_allowed(:unlink) } + end + + %w[saml cas3].each do |provider_name| + context "when provider is #{provider_name}" do + let(:provider) { provider_name } + + it { is_expected.to be_allowed(:link) } + it { is_expected.not_to be_allowed(:unlink) } + end + end + end +end diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb index 1fdf95ad716..99fa8b1fe44 100644 --- a/spec/policies/namespace_policy_spec.rb +++ b/spec/policies/namespace_policy_spec.rb @@ -30,7 +30,7 @@ describe NamespacePolicy do context 'user who has exceeded project limit' do let(:owner) { create(:user, projects_limit: 0) } - it { is_expected.not_to be_allowed(:create_projects) } + it { is_expected.to be_disallowed(:create_projects) } end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 772d1fbee2b..726ccba8807 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1,96 +1,7 @@ require 'spec_helper' describe ProjectPolicy do - set(:guest) { create(:user) } - set(:reporter) { create(:user) } - set(:developer) { create(:user) } - set(:maintainer) { create(:user) } - set(:owner) { create(:user) } - set(:admin) { create(:admin) } - let(:project) { create(:project, :public, namespace: owner.namespace) } - - let(:base_guest_permissions) do - %i[ - read_project read_board read_list read_wiki read_issue - read_project_for_iids read_issue_iid read_label - read_milestone read_project_snippet read_project_member read_note - create_project create_issue create_note upload_file create_merge_request_in - award_emoji read_release - ] - end - - let(:base_reporter_permissions) do - %i[ - download_code fork_project create_project_snippet update_issue - admin_issue admin_label admin_list read_commit_status read_build - read_container_image read_pipeline read_environment read_deployment - read_merge_request download_wiki_code read_sentry_issue - ] - end - - let(:team_member_reporter_permissions) do - %i[build_download_code build_read_container_image] - end - - let(:developer_permissions) do - %i[ - admin_milestone admin_merge_request update_merge_request create_commit_status - update_commit_status create_build update_build create_pipeline - update_pipeline create_merge_request_from create_wiki push_code - resolve_note create_container_image update_container_image - create_environment create_deployment create_release update_release - ] - end - - let(:base_maintainer_permissions) do - %i[ - push_to_delete_protected_branch update_project_snippet update_environment - update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project - admin_commit_status admin_build admin_container_image - admin_pipeline admin_environment admin_deployment destroy_release add_cluster - daily_statistics - ] - end - - let(:public_permissions) do - %i[ - download_code fork_project read_commit_status read_pipeline - read_container_image build_download_code build_read_container_image - download_wiki_code read_release - ] - end - - let(:owner_permissions) do - %i[ - change_namespace change_visibility_level rename_project remove_project - archive_project remove_fork_project destroy_merge_request destroy_issue - set_issue_iid set_issue_created_at set_note_created_at - ] - end - - # Used in EE specs - let(:additional_guest_permissions) { [] } - let(:additional_reporter_permissions) { [] } - let(:additional_maintainer_permissions) { [] } - - let(:guest_permissions) { base_guest_permissions + additional_guest_permissions } - let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions } - let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions } - - before do - project.add_guest(guest) - project.add_maintainer(maintainer) - project.add_developer(developer) - project.add_reporter(reporter) - end - - def expect_allowed(*permissions) - permissions.each { |p| is_expected.to be_allowed(p) } - end - - def expect_disallowed(*permissions) - permissions.each { |p| is_expected.not_to be_allowed(p) } - end + include_context 'ProjectPolicy context' it 'does not include the read_issue permission when the issue author is not a member of the private project' do project = create(:project, :private) @@ -140,7 +51,7 @@ describe ProjectPolicy do end it 'disables boards and lists permissions' do - expect_disallowed :read_board, :create_board, :update_board, :admin_board + expect_disallowed :read_board, :create_board, :update_board expect_disallowed :read_list, :create_list, :update_list, :admin_list end @@ -237,237 +148,6 @@ describe ProjectPolicy do end end - shared_examples 'archived project policies' do - let(:feature_write_abilities) do - described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature| - described_class.create_update_admin_destroy(feature) - end - end - - let(:other_write_abilities) do - %i[ - create_merge_request_in - create_merge_request_from - push_to_delete_protected_branch - push_code - request_access - upload_file - resolve_note - award_emoji - ] - end - - context 'when the project is archived' do - before do - project.archived = true - end - - it 'disables write actions on all relevant project features' do - expect_disallowed(*feature_write_abilities) - end - - it 'disables some other important write actions' do - expect_disallowed(*other_write_abilities) - end - - it 'does not disable other abilities' do - expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities)) - end - end - end - - shared_examples 'project policies as anonymous' do - context 'abilities for public projects' do - context 'when a project has pending invites' do - let(:group) { create(:group, :public) } - let(:project) { create(:project, :public, namespace: group) } - let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } - let(:anonymous_permissions) { guest_permissions - user_permissions } - - subject { described_class.new(nil, project) } - - before do - create(:group_member, :invited, group: group) - end - - it 'does not grant owner access' do - expect_allowed(*anonymous_permissions) - expect_disallowed(*user_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { anonymous_permissions } - end - end - end - - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(nil, project) } - - it { is_expected.to be_banned } - end - end - - shared_examples 'project policies as guest' do - subject { described_class.new(guest, project) } - - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - let(:reporter_public_build_permissions) do - reporter_permissions - [:read_build, :read_pipeline] - end - - it do - expect_allowed(*guest_permissions) - expect_disallowed(*reporter_public_build_permissions) - expect_disallowed(*team_member_reporter_permissions) - expect_disallowed(*developer_permissions) - expect_disallowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { guest_permissions } - end - - context 'public builds enabled' do - it do - expect_allowed(*guest_permissions) - expect_allowed(:read_build, :read_pipeline) - end - end - - context 'when public builds disabled' do - before do - project.update(public_builds: false) - end - - it do - expect_allowed(*guest_permissions) - expect_disallowed(:read_build, :read_pipeline) - end - end - - context 'when builds are disabled' do - before do - project.project_feature.update(builds_access_level: ProjectFeature::DISABLED) - end - - it do - expect_disallowed(:read_build) - expect_allowed(:read_pipeline) - end - end - end - end - - shared_examples 'project policies as reporter' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(reporter, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_allowed(*team_member_reporter_permissions) - expect_disallowed(*developer_permissions) - expect_disallowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { reporter_permissions } - end - end - end - - shared_examples 'project policies as developer' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(developer, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_allowed(*team_member_reporter_permissions) - expect_allowed(*developer_permissions) - expect_disallowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { developer_permissions } - end - end - end - - shared_examples 'project policies as maintainer' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(maintainer, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_allowed(*team_member_reporter_permissions) - expect_allowed(*developer_permissions) - expect_allowed(*maintainer_permissions) - expect_disallowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { maintainer_permissions } - end - end - end - - shared_examples 'project policies as owner' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(owner, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_allowed(*team_member_reporter_permissions) - expect_allowed(*developer_permissions) - expect_allowed(*maintainer_permissions) - expect_allowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { owner_permissions } - end - end - end - - shared_examples 'project policies as admin' do - context 'abilities for non-public projects' do - let(:project) { create(:project, namespace: owner.namespace) } - - subject { described_class.new(admin, project) } - - it do - expect_allowed(*guest_permissions) - expect_allowed(*reporter_permissions) - expect_disallowed(*team_member_reporter_permissions) - expect_allowed(*developer_permissions) - expect_allowed(*maintainer_permissions) - expect_allowed(*owner_permissions) - end - - it_behaves_like 'archived project policies' do - let(:regular_abilities) { owner_permissions } - end - end - end - it_behaves_like 'project policies as anonymous' it_behaves_like 'project policies as guest' it_behaves_like 'project policies as reporter' diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index d6329e84579..2e9ef1e89fd 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -5,7 +5,7 @@ describe ProjectSnippetPolicy do let(:regular_user) { create(:user) } let(:external_user) { create(:user, :external) } let(:project) { create(:project, :public) } - + let(:snippet) { create(:project_snippet, snippet_visibility, project: project) } let(:author_permissions) do [ :update_project_snippet, @@ -13,23 +13,13 @@ describe ProjectSnippetPolicy do ] end - def abilities(user, snippet_visibility) - snippet = create(:project_snippet, snippet_visibility, project: project) - - described_class.new(user, snippet) - end - - def expect_allowed(*permissions) - permissions.each { |p| is_expected.to be_allowed(p) } - end - - def expect_disallowed(*permissions) - permissions.each { |p| is_expected.not_to be_allowed(p) } - end + subject { described_class.new(current_user, snippet) } context 'public snippet' do + let(:snippet_visibility) { :public } + context 'no user' do - subject { abilities(nil, :public) } + let(:current_user) { nil } it do expect_allowed(:read_project_snippet) @@ -38,7 +28,7 @@ describe ProjectSnippetPolicy do end context 'regular user' do - subject { abilities(regular_user, :public) } + let(:current_user) { regular_user } it do expect_allowed(:read_project_snippet, :create_note) @@ -47,7 +37,7 @@ describe ProjectSnippetPolicy do end context 'external user' do - subject { abilities(external_user, :public) } + let(:current_user) { external_user } it do expect_allowed(:read_project_snippet, :create_note) @@ -57,8 +47,10 @@ describe ProjectSnippetPolicy do end context 'internal snippet' do + let(:snippet_visibility) { :internal } + context 'no user' do - subject { abilities(nil, :internal) } + let(:current_user) { nil } it do expect_disallowed(:read_project_snippet) @@ -67,7 +59,7 @@ describe ProjectSnippetPolicy do end context 'regular user' do - subject { abilities(regular_user, :internal) } + let(:current_user) { regular_user } it do expect_allowed(:read_project_snippet, :create_note) @@ -76,31 +68,31 @@ describe ProjectSnippetPolicy do end context 'external user' do - subject { abilities(external_user, :internal) } + let(:current_user) { external_user } it do expect_disallowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end - end - context 'project team member external user' do - subject { abilities(external_user, :internal) } - - before do - project.add_developer(external_user) - end + context 'project team member' do + before do + project.add_developer(external_user) + end - it do - expect_allowed(:read_project_snippet, :create_note) - expect_disallowed(*author_permissions) + it do + expect_allowed(:read_project_snippet, :create_note) + expect_disallowed(*author_permissions) + end end end end context 'private snippet' do + let(:snippet_visibility) { :private } + context 'no user' do - subject { abilities(nil, :private) } + let(:current_user) { nil } it do expect_disallowed(:read_project_snippet) @@ -109,53 +101,52 @@ describe ProjectSnippetPolicy do end context 'regular user' do - subject { abilities(regular_user, :private) } + let(:current_user) { regular_user } it do expect_disallowed(:read_project_snippet, :create_note) expect_disallowed(*author_permissions) end - end - - context 'snippet author' do - let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } - subject { described_class.new(regular_user, snippet) } + context 'snippet author' do + let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } - it do - expect_allowed(:read_project_snippet, :create_note) - expect_allowed(*author_permissions) + it do + expect_allowed(:read_project_snippet, :create_note) + expect_allowed(*author_permissions) + end end - end - context 'project team member normal user' do - subject { abilities(regular_user, :private) } - - before do - project.add_developer(regular_user) - end + context 'project team member normal user' do + before do + project.add_developer(regular_user) + end - it do - expect_allowed(:read_project_snippet, :create_note) - expect_disallowed(*author_permissions) + it do + expect_allowed(:read_project_snippet, :create_note) + expect_disallowed(*author_permissions) + end end end - context 'project team member external user' do - subject { abilities(external_user, :private) } + context 'external user' do + context 'project team member' do + let(:current_user) { external_user } - before do - project.add_developer(external_user) - end + before do + project.add_developer(external_user) + end - it do - expect_allowed(:read_project_snippet, :create_note) - expect_disallowed(*author_permissions) + it do + expect_allowed(:read_project_snippet, :create_note) + expect_disallowed(*author_permissions) + end end end context 'admin user' do - subject { abilities(create(:admin), :private) } + let(:snippet_visibility) { :private } + let(:current_user) { create(:admin) } it do expect_allowed(:read_project_snippet, :create_note) diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb index f50bcf54b46..ad6cb012d0b 100644 --- a/spec/presenters/ci/build_runner_presenter_spec.rb +++ b/spec/presenters/ci/build_runner_presenter_spec.rb @@ -136,6 +136,24 @@ describe Ci::BuildRunnerPresenter do is_expected.to eq(1) end end + + context 'when pipeline is detached merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.first } + let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) } + + it 'returns the default git depth for pipelines for merge requests' do + is_expected.to eq(described_class::DEFAULT_GIT_DEPTH_MERGE_REQUEST) + end + + context 'when pipeline is legacy detached merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) } + + it 'behaves as branch pipeline' do + is_expected.to eq(0) + end + end + end end describe '#refspecs' do @@ -165,5 +183,25 @@ describe Ci::BuildRunnerPresenter do end end end + + context 'when pipeline is detached merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.first } + let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) } + + it 'returns the correct refspecs' do + is_expected + .to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head') + end + + context 'when pipeline is legacy detached merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) } + + it 'returns the correct refspecs' do + is_expected.to contain_exactly('+refs/tags/*:refs/tags/*', + '+refs/heads/*:refs/remotes/origin/*') + end + end + end end end diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb index f7ceaf844be..cda07a0ae09 100644 --- a/spec/presenters/ci/pipeline_presenter_spec.rb +++ b/spec/presenters/ci/pipeline_presenter_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' describe Ci::PipelinePresenter do + include Gitlab::Routing + + let(:user) { create(:user) } let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -8,6 +11,11 @@ describe Ci::PipelinePresenter do described_class.new(pipeline) end + before do + project.add_developer(user) + allow(presenter).to receive(:current_user) { user } + end + it 'inherits from Gitlab::View::Presenter::Delegated' do expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) end @@ -68,4 +76,130 @@ describe Ci::PipelinePresenter do end end end + + describe '#ref_text' do + subject { presenter.ref_text } + + context 'when pipeline is detached merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct ref text' do + is_expected.to eq("for <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \ + "with <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a>") + end + end + + context 'when pipeline is merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct ref text' do + is_expected.to eq("for <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \ + "with <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a> " \ + "into <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.target_project, merge_request.target_branch)}\">#{merge_request.target_branch}</a>") + end + end + + context 'when pipeline is branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when ref exists in the repository' do + before do + allow(pipeline).to receive(:ref_exists?) { true } + end + + it 'returns a correct ref text' do + is_expected.to eq("for <a class=\"ref-name\" href=\"#{project_commits_path(pipeline.project, pipeline.ref)}\">#{pipeline.ref}</a>") + end + + context 'when ref contains malicious script' do + let(:pipeline) { create(:ci_pipeline, ref: "<script>alter('1')</script>", project: project) } + + it 'does not include the malicious script' do + is_expected.not_to include("<script>alter('1')</script>") + end + end + end + + context 'when ref exists in the repository' do + before do + allow(pipeline).to receive(:ref_exists?) { false } + end + + it 'returns a correct ref text' do + is_expected.to eq("for <span class=\"ref-name\">#{pipeline.ref}</span>") + end + + context 'when ref contains malicious script' do + let(:pipeline) { create(:ci_pipeline, ref: "<script>alter('1')</script>", project: project) } + + it 'does not include the malicious script' do + is_expected.not_to include("<script>alter('1')</script>") + end + end + end + end + end + + describe '#link_to_merge_request' do + subject { presenter.link_to_merge_request } + + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct link' do + is_expected + .to include(project_merge_request_path(merge_request.project, merge_request)) + end + + context 'when pipeline is branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + it 'returns nothing' do + is_expected.to be_nil + end + end + end + + describe '#link_to_merge_request_source_branch' do + subject { presenter.link_to_merge_request_source_branch } + + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct link' do + is_expected + .to include(project_commits_path(merge_request.source_project, + merge_request.source_branch)) + end + + context 'when pipeline is branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + it 'returns nothing' do + is_expected.to be_nil + end + end + end + + describe '#link_to_merge_request_target_branch' do + subject { presenter.link_to_merge_request_target_branch } + + let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct link' do + is_expected + .to include(project_commits_path(merge_request.target_project, merge_request.target_branch)) + end + + context 'when pipeline is branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + it 'returns nothing' do + is_expected.to be_nil + end + end + end end diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb index 754ba0a594c..a9d786bc872 100644 --- a/spec/presenters/clusters/cluster_presenter_spec.rb +++ b/spec/presenters/clusters/cluster_presenter_spec.rb @@ -228,4 +228,20 @@ describe Clusters::ClusterPresenter do it { is_expected.to eq(group_cluster_path(group, cluster)) } end end + + describe '#read_only_kubernetes_platform_fields?' do + subject { described_class.new(cluster).read_only_kubernetes_platform_fields? } + + context 'with a user-provided cluster' do + let(:cluster) { build_stubbed(:cluster, :provided_by_user) } + + it { is_expected.to be_falsy } + end + + context 'with a GCP-provided cluster' do + let(:cluster) { build_stubbed(:cluster, :provided_by_gcp) } + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 02cefcbc916..4a0f91c4c7a 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' describe MergeRequestPresenter do - let(:resource) { create :merge_request, source_project: project } - let(:project) { create :project } + let(:resource) { create(:merge_request, source_project: project) } + let(:project) { create(:project) } let(:user) { create(:user) } describe '#ci_status' do @@ -345,6 +345,30 @@ describe MergeRequestPresenter do end end + describe '#source_branch_commits_path' do + subject do + described_class.new(resource, current_user: user) + .source_branch_commits_path + end + + context 'when source branch exists' do + it 'returns path' do + allow(resource).to receive(:source_branch_exists?) { true } + + is_expected + .to eq("/#{resource.source_project.full_path}/commits/#{resource.source_branch}") + end + end + + context 'when source branch does not exist' do + it 'returns nil' do + allow(resource).to receive(:source_branch_exists?) { false } + + is_expected.to be_nil + end + end + end + describe '#target_branch_tree_path' do subject do described_class.new(resource, current_user: user) @@ -499,4 +523,46 @@ describe MergeRequestPresenter do end end end + + describe '#can_push_to_source_branch' do + before do + allow(resource).to receive(:source_branch_exists?) { source_branch_exists } + + allow_any_instance_of(Gitlab::UserAccess::RequestCacheExtension) + .to receive(:can_push_to_branch?) + .with(resource.source_branch) + .and_return(can_push_to_branch) + end + + subject do + described_class.new(resource, current_user: user).can_push_to_source_branch? + end + + context 'when source branch exists AND user can push to source branch' do + let(:source_branch_exists) { true } + let(:can_push_to_branch) { true } + + it 'returns true' do + is_expected.to eq(true) + end + end + + context 'when source branch does not exists' do + let(:source_branch_exists) { false } + let(:can_push_to_branch) { true } + + it 'returns false' do + is_expected.to eq(false) + end + end + + context 'when user cannot push to source branch' do + let(:source_branch_exists) { true } + let(:can_push_to_branch) { false } + + it 'returns false' do + is_expected.to eq(false) + end + end + end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index b38cd66986f..8b503777443 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -20,9 +20,9 @@ describe API::Branches do let(:route) { "/projects/#{project_id}/repository/branches" } shared_examples_for 'repository branches' do - RSpec::Matchers.define :has_merged_branch_names_count do |expected| + RSpec::Matchers.define :has_up_to_merged_branch_names_count do |expected| match do |actual| - actual[:merged_branch_names].count == expected + expected >= actual[:merged_branch_names].count end end @@ -36,10 +36,30 @@ describe API::Branches do expect(branch_names).to match_array(project.repository.branch_names) end + def check_merge_status(json_response) + merged, unmerged = json_response.partition { |branch| branch['merged'] } + merged_branches = merged.map { |branch| branch['name'] } + unmerged_branches = unmerged.map { |branch| branch['name'] } + expect(Set.new(merged_branches)).to eq(project.repository.merged_branch_names(merged_branches + unmerged_branches)) + expect(project.repository.merged_branch_names(unmerged_branches)).to be_empty + end + it 'determines only a limited number of merged branch names' do - expect(API::Entities::Branch).to receive(:represent).with(anything, has_merged_branch_names_count(2)) + expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original get api(route, current_user), params: { per_page: 2 } + + expect(response).to have_gitlab_http_status(200) + + check_merge_status(json_response) + end + + it 'merge status matches reality on paginated input' do + get api(route, current_user), params: { per_page: 20, page: 2 } + + expect(response).to have_gitlab_http_status(200) + + check_merge_status(json_response) end context 'when repository is disabled' do diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb index deb6abbc026..74820d39102 100644 --- a/spec/requests/api/graphql/project/merge_request_spec.rb +++ b/spec/requests/api/graphql/project/merge_request_spec.rb @@ -70,13 +70,13 @@ describe 'getting merge request information nested in a project' do context 'when there are pipelines' do before do - pipeline = create( + create( :ci_pipeline, project: merge_request.source_project, ref: merge_request.source_branch, sha: merge_request.diff_head_sha ) - merge_request.update!(head_pipeline: pipeline) + merge_request.update_head_pipeline end it 'has a head pipeline' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index b184c92824a..537194b8e11 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -321,7 +321,7 @@ describe API::Internal do end context 'with env passed as a JSON' do - let(:gl_repository) { project.gl_repository(is_wiki: true) } + let(:gl_repository) { Gitlab::GlRepository::WIKI.identifier_for_subject(project) } it 'sets env in RequestStore' do obj_dir_relative = './objects' @@ -975,9 +975,9 @@ describe API::Internal do def gl_repository_for(project_or_wiki) case project_or_wiki when ProjectWiki - project_or_wiki.project.gl_repository(is_wiki: true) + Gitlab::GlRepository::WIKI.identifier_for_subject(project_or_wiki.project) when Project - project_or_wiki.gl_repository(is_wiki: false) + Gitlab::GlRepository::PROJECT.identifier_for_subject(project_or_wiki) else nil end diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb index 9bab1f95150..4e42e233b4c 100644 --- a/spec/requests/api/project_clusters_spec.rb +++ b/spec/requests/api/project_clusters_spec.rb @@ -331,7 +331,6 @@ describe API::ProjectClusters do it 'should update cluster attributes' do expect(cluster.platform_kubernetes.namespace).to eq('new-namespace') - expect(cluster.kubernetes_namespace.namespace).to eq('new-namespace') end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 60d9d7fed13..4c3c088b307 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1183,6 +1183,16 @@ describe API::Projects do expect(response).to have_gitlab_http_status(200) expect(json_response).to include 'statistics' end + + it "includes statistics also when repository is disabled" do + project.add_developer(user) + project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED) + + get api("/projects/#{project.id}", user), params: { statistics: true } + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include 'statistics' + end end it "includes import_error if user can admin project" do diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 9087cccb759..3ccedd8dd06 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -918,6 +918,15 @@ describe API::Runner, :clean_gitlab_redis_shared_state do it { expect(job).to be_job_execution_timeout } end + + context 'when failure_reason is unmet_prerequisites' do + before do + update_job(state: 'failed', failure_reason: 'unmet_prerequisites') + job.reload + end + + it { expect(job).to be_unmet_prerequisites } + end end context 'when trace is given' do diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index c48ca832c85..49672591b3b 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -77,6 +77,28 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + context 'for users scope' do + before do + create(:user, name: 'billy') + + get api('/search', user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api('/search', user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + context 'for snippet_titles scope' do before do create(:snippet, :public, title: 'awesome snippet', content: 'snippet content') @@ -192,6 +214,40 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + + context 'for users scope' do + before do + user = create(:user, name: 'billy') + create(:group_member, :developer, user: user, group: group) + + get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + + context 'for users scope with group path as id' do + before do + user1 = create(:user, name: 'billy') + create(:group_member, :developer, user: user1, group: group) + + get api("/groups/#{CGI.escape(group.full_path)}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + end end end @@ -269,6 +325,29 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + context 'for users scope' do + before do + user1 = create(:user, name: 'billy') + create(:project_member, :developer, user: user1, project: project) + + get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + context 'for notes scope' do before do create(:note_on_merge_request, project: project, note: 'awesome note') diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb index 3c2842e5725..5b07e598b8d 100644 --- a/spec/requests/api/suggestions_spec.rb +++ b/spec/requests/api/suggestions_spec.rb @@ -42,8 +42,7 @@ describe API::Suggestions do expect(response).to have_gitlab_http_status(200) expect(json_response) - .to include('id', 'from_original_line', 'to_original_line', - 'from_line', 'to_line', 'appliable', 'applied', + .to include('id', 'from_line', 'to_line', 'appliable', 'applied', 'from_content', 'to_content') end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index a879426589d..b84202364e1 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -68,6 +68,13 @@ describe API::Users do expect(json_response.size).to eq(0) end + it "does not return the highest role" do + get api("/users"), params: { username: user.username } + + expect(response).to match_response_schema('public_api/v4/user/basics') + expect(json_response.first.keys).not_to include 'highest_role' + end + context "when public level is restricted" do before do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) @@ -286,6 +293,13 @@ describe API::Users do expect(json_response.keys).not_to include 'is_admin' end + it "does not return the user's `highest_role`" do + get api("/users/#{user.id}", user) + + expect(response).to match_response_schema('public_api/v4/user/basic') + expect(json_response.keys).not_to include 'highest_role' + end + context 'when authenticated as admin' do it 'includes the `is_admin` field' do get api("/users/#{user.id}", admin) @@ -300,6 +314,12 @@ describe API::Users do expect(response).to match_response_schema('public_api/v4/user/admin') expect(json_response.keys).to include 'created_at' end + it 'includes the `highest_role` field' do + get api("/users/#{user.id}", admin) + + expect(response).to match_response_schema('public_api/v4/user/admin') + expect(json_response['highest_role']).to be(0) + end end context 'for an anonymous user' do diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb index 5fde4bd885b..3c48ead4ff2 100644 --- a/spec/routing/api_routing_spec.rb +++ b/spec/routing/api_routing_spec.rb @@ -7,25 +7,17 @@ describe 'api', 'routing' do end it 'does not route to the GraphqlController' do - expect(get('/api/graphql')).not_to route_to('graphql#execute') - end - - it 'does not expose graphiql' do - expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show') + expect(post('/api/graphql')).not_to route_to('graphql#execute') end end - context 'when graphql is disabled' do + context 'when graphql is enabled' do before do stub_feature_flags(graphql: true) end it 'routes to the GraphqlController' do - expect(get('/api/graphql')).not_to route_to('graphql#execute') - end - - it 'exposes graphiql' do - expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show') + expect(post('/api/graphql')).to route_to('graphql#execute') end end end diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb index 1c8ab0ad5d2..cba01400d85 100644 --- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb +++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb @@ -93,4 +93,22 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do it_behaves_like 'a migration file with no spec file' it_behaves_like 'a migration file with a spec file' end + + context 'EE migrations' do + let(:spec_filepath) { tmp_rails_root.join('ee', 'spec', 'migrations', 'my_super_migration_spec.rb') } + + context 'in a migration' do + let(:migration_filepath) { tmp_rails_root.join('ee', 'db', 'migrate', '20121220064453_my_super_migration.rb') } + + it_behaves_like 'a migration file with no spec file' + it_behaves_like 'a migration file with a spec file' + end + + context 'in a post migration' do + let(:migration_filepath) { tmp_rails_root.join('ee', 'db', 'post_migrate', '20121220064453_my_super_migration.rb') } + + it_behaves_like 'a migration file with no spec file' + it_behaves_like 'a migration file with a spec file' + end + end end diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 4dbd79f2fc0..727fd8951f2 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -279,13 +279,18 @@ describe MergeRequestWidgetEntity do end describe 'commits_without_merge_commits' do + def find_matching_commit(short_id) + resource.commits.find { |c| c.short_id == short_id } + end + it 'should not include merge commits' do - # Mock all but the first 5 commits to be merge commits - resource.commits.each_with_index do |commit, i| - expect(commit).to receive(:merge_commit?).at_least(:once).and_return(i > 4) - end + commits_in_widget = subject[:commits_without_merge_commits] - expect(subject[:commits_without_merge_commits].size).to eq(5) + expect(commits_in_widget.length).to be < resource.commits.length + expect(commits_in_widget.length).to eq(resource.commits.without_merge_commits.length) + commits_in_widget.each do |c| + expect(find_matching_commit(c[:short_id]).merge_commit?).to eq(false) + end end end end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index c8308a0ae85..1d992e8a483 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -159,13 +159,13 @@ describe PipelineEntity do expect(subject[:merge_request][:source_branch]) .to eq(merge_request.source_branch) - expect(project_branch_path(project, merge_request.source_branch)) + expect(project_commits_path(project, merge_request.source_branch)) .to include(subject[:merge_request][:source_branch_path]) expect(subject[:merge_request][:target_branch]) .to eq(merge_request.target_branch) - expect(project_branch_path(project, merge_request.target_branch)) + expect(project_commits_path(project, merge_request.target_branch)) .to include(subject[:merge_request][:target_branch_path]) end end diff --git a/spec/serializers/suggestion_entity_spec.rb b/spec/serializers/suggestion_entity_spec.rb index 047571f161c..d38fc2b132b 100644 --- a/spec/serializers/suggestion_entity_spec.rb +++ b/spec/serializers/suggestion_entity_spec.rb @@ -13,8 +13,8 @@ describe SuggestionEntity do subject { entity.as_json } it 'exposes correct attributes' do - expect(subject).to include(:id, :from_original_line, :to_original_line, :from_line, - :to_line, :appliable, :applied, :from_content, :to_content) + expect(subject).to include(:id, :from_line, :to_line, :appliable, + :applied, :from_content, :to_content) end it 'exposes current user abilities' do diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 8021bd338e0..c9d85e96750 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -88,6 +88,12 @@ describe Auth::ContainerRegistryAuthenticationService do end end + shared_examples 'a deletable since registry 2.7' do + it_behaves_like 'an accessible' do + let(:actions) { ['delete'] } + end + end + shared_examples 'a pullable' do it_behaves_like 'an accessible' do let(:actions) { ['pull'] } @@ -184,6 +190,19 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end + context 'disallow developer to delete images since registry 2.7' do + before do + project.add_developer(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + context 'allow reporter to pull images' do before do project.add_reporter(current_user) @@ -212,6 +231,19 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end + context 'disallow reporter to delete images since registry 2.7' do + before do + project.add_reporter(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + context 'return a least of privileges' do before do project.add_reporter(current_user) @@ -250,6 +282,19 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow guest to delete images since regsitry 2.7' do + before do + project.add_guest(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end end context 'for public project' do @@ -282,6 +327,15 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end + context 'disallow anyone to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + context 'when repository name is invalid' do let(:current_params) do { scopes: ['repository:invalid:push'] } @@ -322,6 +376,15 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow anyone to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end end context 'for external user' do @@ -344,6 +407,16 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow anyone to delete images since registry 2.7' do + let(:current_user) { create(:user, external: true) } + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end end end end @@ -371,6 +444,16 @@ describe Auth::ContainerRegistryAuthenticationService do let(:project) { current_project } end end + + context 'allow to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:delete"] } + end + + it_behaves_like 'a deletable since registry 2.7' do + let(:project) { current_project } + end + end end context 'build authorized as user' do @@ -419,6 +502,16 @@ describe Auth::ContainerRegistryAuthenticationService do end end + context 'disallow to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' do + let(:project) { current_project } + end + end + context 'for other projects' do context 'when pulling' do let(:current_params) do diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb index d896f990470..bff2b3179fb 100644 --- a/spec/services/ci/destroy_pipeline_service_spec.rb +++ b/spec/services/ci/destroy_pipeline_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe ::Ci::DestroyPipelineService do - let(:project) { create(:project) } - let!(:pipeline) { create(:ci_pipeline, project: project) } + let(:project) { create(:project, :repository) } + let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.id) } subject { described_class.new(project, user).execute(pipeline) } @@ -17,6 +17,17 @@ describe ::Ci::DestroyPipelineService do expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound) end + it 'clears the cache', :use_clean_rails_memory_store_caching do + create(:commit_status, :success, pipeline: pipeline, ref: pipeline.ref) + + expect(project.pipeline_status.has_status?).to be_truthy + + subject + + # Need to use find to avoid memoization + expect(Project.find(project.id).pipeline_status.has_status?).to be_falsey + end + it 'does not log an audit event' do expect { subject }.not_to change { SecurityEvent.count } end diff --git a/spec/services/ci/prepare_build_service_spec.rb b/spec/services/ci/prepare_build_service_spec.rb new file mode 100644 index 00000000000..1797f8f964f --- /dev/null +++ b/spec/services/ci/prepare_build_service_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::PrepareBuildService do + describe '#execute' do + let(:build) { create(:ci_build, :preparing) } + + subject { described_class.new(build).execute } + + before do + allow(build).to receive(:prerequisites).and_return(prerequisites) + end + + shared_examples 'build enqueueing' do + it 'enqueues the build' do + expect(build).to receive(:enqueue).once + + subject + end + end + + context 'build has unmet prerequisites' do + let(:prerequisite) { double(complete!: true) } + let(:prerequisites) { [prerequisite] } + + it 'completes each prerequisite' do + expect(prerequisites).to all(receive(:complete!)) + + subject + end + + include_examples 'build enqueueing' + + context 'prerequisites fail to complete' do + before do + allow(build).to receive(:enqueue).and_return(false) + end + + it 'drops the build' do + expect(build).to receive(:drop!).with(:unmet_prerequisites).once + + subject + end + end + end + + context 'build has no prerequisites' do + let(:prerequisites) { [] } + + include_examples 'build enqueueing' + end + end +end diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb index cbdef008b07..20555873503 100644 --- a/spec/services/clusters/applications/create_service_spec.rb +++ b/spec/services/clusters/applications/create_service_spec.rb @@ -150,7 +150,7 @@ describe Clusters::Applications::CreateService do where(:application, :association, :allowed, :pre_create_helm) do 'helm' | :application_helm | true | false 'ingress' | :application_ingress | true | true - 'runner' | :application_runner | false | true + 'runner' | :application_runner | true | true 'jupyter' | :application_jupyter | false | true 'prometheus' | :application_prometheus | false | true end diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb index 54692c88623..87f93ec97c9 100644 --- a/spec/services/emails/create_service_spec.rb +++ b/spec/services/emails/create_service_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Emails::CreateService do diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb index c3204fac3df..5abe8da2529 100644 --- a/spec/services/emails/destroy_service_spec.rb +++ b/spec/services/emails/destroy_service_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'spec_helper' describe Emails::DestroyService do diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb index 9d4fc62f923..3a8f3069911 100644 --- a/spec/services/error_tracking/list_issues_service_spec.rb +++ b/spec/services/error_tracking/list_issues_service_spec.rb @@ -53,7 +53,10 @@ describe ErrorTracking::ListIssuesService do before do allow(error_tracking_setting) .to receive(:list_sentry_issues) - .and_return(error: 'Sentry response status code: 401') + .and_return( + error: 'Sentry response status code: 401', + error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE + ) end it 'returns the error' do @@ -64,6 +67,25 @@ describe ErrorTracking::ListIssuesService do ) end end + + context 'when list_sentry_issues returns error with http_status' do + before do + allow(error_tracking_setting) + .to receive(:list_sentry_issues) + .and_return( + error: 'Sentry API response is missing keys. key not found: "id"', + error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS + ) + end + + it 'returns the error with correct http_status' do + expect(result).to eq( + status: :error, + http_status: :internal_server_error, + message: 'Sentry API response is missing keys. key not found: "id"' + ) + end + end end context 'with unauthorized user' do diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index e8fce951155..d0e2169b4a6 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe GitPushService, services: true do +describe Git::BranchPushService, services: true do include RepoHelpers set(:user) { create(:user) } diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb index 2699f6e7bcd..e151db5827f 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git/tag_push_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe GitTagPushService do +describe Git::TagPushService do include RepoHelpers include GitHelpers diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 715b1168bfb..d50412b6d2c 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -138,6 +138,20 @@ describe Issues::CreateService do end end + context 'when duplicate label titles are given' do + let(:label) { create(:label, project: project) } + + let(:opts) do + { title: 'Title', + description: 'Description', + labels: [label.title, label.title] } + end + + it 'assigns the label once' do + expect(issue.labels).to contain_exactly(label) + end + end + it 'executes issue hooks when issue is not confidential' do opts = { title: 'Title', description: 'Description', confidential: false } diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index f1684209729..1c8a4b608d5 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -592,6 +592,16 @@ describe Issues::UpdateService, :mailer do expect(result.label_ids).not_to include(label.id) end end + + context 'when duplicate label titles are given' do + let(:params) do + { labels: [label3.title, label3.title] } + end + + it 'assigns the label once' do + expect(result.labels).to contain_exactly(label3) + end + end end context 'updating asssignee_id' do diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index a04a4d5fc36..55e7b46248b 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -173,7 +173,7 @@ describe MergeRequests::CreateService do end end - describe 'Merge request pipelines' do + describe 'Pipelines for merge requests' do before do stub_ci_pipeline_yaml_file(YAML.dump(config)) end @@ -189,12 +189,46 @@ describe MergeRequests::CreateService do } end - it 'creates a merge request pipeline and sets it as a head pipeline' do + it 'creates a detached merge request pipeline and sets it as a head pipeline' do expect(merge_request).to be_persisted merge_request.reload expect(merge_request.merge_request_pipelines.count).to eq(1) - expect(merge_request.actual_head_pipeline).to be_merge_request_event + expect(merge_request.actual_head_pipeline).to be_detached_merge_request_pipeline + end + + context 'when merge request is submitted from forked project' do + let(:target_project) { fork_project(project, nil, repository: true) } + + let(:opts) do + { + title: 'Awesome merge_request', + source_branch: 'feature', + target_branch: 'master', + target_project_id: target_project.id + } + end + + before do + target_project.add_developer(assignee) + target_project.add_maintainer(user) + end + + it 'create legacy detached merge request pipeline for fork merge request' do + expect(merge_request.actual_head_pipeline) + .to be_legacy_detached_merge_request_pipeline + end + end + + context 'when ci_use_merge_request_ref feature flag is false' do + before do + stub_feature_flags(ci_use_merge_request_ref: false) + end + + it 'create legacy detached merge request pipeline for non-fork merge request' do + expect(merge_request.actual_head_pipeline) + .to be_legacy_detached_merge_request_pipeline + end end context 'when there are no commits between source branch and target branch' do @@ -207,7 +241,7 @@ describe MergeRequests::CreateService do } end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(merge_request).to be_persisted merge_request.reload @@ -225,7 +259,7 @@ describe MergeRequests::CreateService do merge_request end - it 'sets the latest merge request pipeline as the head pipeline' do + it 'sets the latest detached merge request pipeline as the head pipeline' do expect(merge_request.actual_head_pipeline).to be_merge_request_event end end @@ -235,7 +269,7 @@ describe MergeRequests::CreateService do stub_feature_flags(ci_merge_request_pipeline: false) end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(merge_request).to be_persisted merge_request.reload @@ -254,7 +288,7 @@ describe MergeRequests::CreateService do } end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect(merge_request).to be_persisted merge_request.reload diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb index fe673de46aa..1430e12a07e 100644 --- a/spec/services/merge_requests/ff_merge_service_spec.rb +++ b/spec/services/merge_requests/ff_merge_service_spec.rb @@ -72,7 +72,7 @@ describe MergeRequests::FfMergeService do it 'logs and saves error if there is an PreReceiveError exception' do error_message = 'error message' - allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, error_message) + allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "GitLab: #{error_message}") allow(service).to receive(:execute_hooks) service.execute(merge_request) diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index ede79b87bcc..887ec17171e 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -239,7 +239,7 @@ describe MergeRequests::MergeService do it 'logs and saves error if there is an PreReceiveError exception' do error_message = 'error message' - allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, error_message) + allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "GitLab: #{error_message}") allow(service).to receive(:execute_hooks) service.execute(merge_request) diff --git a/spec/services/merge_requests/migrate_external_diffs_service_spec.rb b/spec/services/merge_requests/migrate_external_diffs_service_spec.rb new file mode 100644 index 00000000000..40ac747e66f --- /dev/null +++ b/spec/services/merge_requests/migrate_external_diffs_service_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MergeRequests::MigrateExternalDiffsService do + let(:merge_request) { create(:merge_request) } + let(:diff) { merge_request.merge_request_diff } + + describe '.enqueue!', :sidekiq do + around do |example| + Sidekiq::Testing.fake! { example.run } + end + + it 'enqueues nothing if external diffs are disabled' do + expect(diff).not_to be_stored_externally + + expect { described_class.enqueue! } + .not_to change { MigrateExternalDiffsWorker.jobs.count } + end + + it 'enqueues eligible in-database diffs if external diffs are enabled' do + expect(diff).not_to be_stored_externally + + stub_external_diffs_setting(enabled: true) + + expect { described_class.enqueue! } + .to change { MigrateExternalDiffsWorker.jobs.count } + .by(1) + end + end + + describe '#execute' do + it 'migrates an in-database diff to the external store' do + expect(diff).not_to be_stored_externally + + stub_external_diffs_setting(enabled: true) + + described_class.new(diff).execute + + expect(diff).to be_stored_externally + end + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 43ceb1dcbee..25cbac6d7ee 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -97,6 +97,15 @@ describe MergeRequests::RefreshService do } end + it 'outdates MR suggestions' do + expect_next_instance_of(Suggestions::OutdateService) do |service| + expect(service).to receive(:execute).with(@merge_request).and_call_original + expect(service).to receive(:execute).with(@another_merge_request).and_call_original + end + + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + end + context 'when source branch ref does not exists' do before do DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch) @@ -132,7 +141,7 @@ describe MergeRequests::RefreshService do end end - describe 'Merge request pipelines' do + describe 'Pipelines for merge requests' do before do stub_ci_pipeline_yaml_file(YAML.dump(config)) end @@ -150,7 +159,7 @@ describe MergeRequests::RefreshService do } end - it 'create merge request pipeline with commits' do + it 'create detached merge request pipeline with commits' do expect { subject } .to change { @merge_request.merge_request_pipelines.count }.by(1) .and change { @fork_merge_request.merge_request_pipelines.count }.by(1) @@ -161,7 +170,34 @@ describe MergeRequests::RefreshService do expect(@another_merge_request.has_commits?).to be_falsy end - context "when branch pipeline was created before a merge request pipline has been created" do + it 'create detached merge request pipeline for non-fork merge request' do + subject + + expect(@merge_request.merge_request_pipelines.first) + .to be_detached_merge_request_pipeline + end + + it 'create legacy detached merge request pipeline for fork merge request' do + subject + + expect(@fork_merge_request.merge_request_pipelines.first) + .to be_legacy_detached_merge_request_pipeline + end + + context 'when ci_use_merge_request_ref feature flag is false' do + before do + stub_feature_flags(ci_use_merge_request_ref: false) + end + + it 'create legacy detached merge request pipeline for non-fork merge request' do + subject + + expect(@merge_request.merge_request_pipelines.first) + .to be_legacy_detached_merge_request_pipeline + end + end + + context "when branch pipeline was created before a detaced merge request pipeline has been created" do before do create(:ci_pipeline, project: @merge_request.source_project, sha: @merge_request.diff_head_sha, @@ -171,7 +207,7 @@ describe MergeRequests::RefreshService do subject end - it 'sets the latest merge request pipeline as a head pipeline' do + it 'sets the latest detached merge request pipeline as a head pipeline' do @merge_request.reload expect(@merge_request.actual_head_pipeline).to be_merge_request_event end @@ -184,7 +220,7 @@ describe MergeRequests::RefreshService do end context "when MergeRequestUpdateWorker is retried by an exception" do - it 'does not re-create a duplicate merge request pipeline' do + it 'does not re-create a duplicate detached merge request pipeline' do expect do service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') end.to change { @merge_request.merge_request_pipelines.count }.by(1) @@ -200,7 +236,7 @@ describe MergeRequests::RefreshService do stub_feature_flags(ci_merge_request_pipeline: false) end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect { subject } .not_to change { @merge_request.merge_request_pipelines.count } end @@ -217,7 +253,7 @@ describe MergeRequests::RefreshService do } end - it 'does not create a merge request pipeline' do + it 'does not create a detached merge request pipeline' do expect { subject } .not_to change { @merge_request.merge_request_pipelines.count } end @@ -329,14 +365,16 @@ describe MergeRequests::RefreshService do context 'push to fork repo source branch' do let(:refresh_service) { service.new(@fork_project, @user) } - context 'open fork merge request' do - before do - allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - reload_mrs - end + def refresh + allow(refresh_service).to receive(:execute_hooks) + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + reload_mrs + end + context 'open fork merge request' do it 'executes hooks with update action' do + refresh + expect(refresh_service).to have_received(:execute_hooks) .with(@fork_merge_request, 'update', old_rev: @oldrev) @@ -347,21 +385,30 @@ describe MergeRequests::RefreshService do expect(@build_failed_todo).to be_pending expect(@fork_build_failed_todo).to be_pending end + + it 'outdates opened forked MR suggestions' do + expect_next_instance_of(Suggestions::OutdateService) do |service| + expect(service).to receive(:execute).with(@fork_merge_request).and_call_original + end + + refresh + end end context 'closed fork merge request' do before do @fork_merge_request.close! - allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - reload_mrs end it 'do not execute hooks with update action' do + refresh + expect(refresh_service).not_to have_received(:execute_hooks) end it 'updates merge request to closed state' do + refresh + expect(@merge_request.notes).to be_empty expect(@merge_request).to be_open expect(@fork_merge_request.notes).to be_empty diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index d1b110b9806..e8418b09dc2 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -276,6 +276,7 @@ describe Projects::CreateService, '#execute' do before do group.add_owner(user) + stub_feature_flags(ci_preparing_state: false) expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) end diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb index 6040f9100f8..4b6d0c51363 100644 --- a/spec/services/projects/participants_service_spec.rb +++ b/spec/services/projects/participants_service_spec.rb @@ -2,29 +2,56 @@ require 'spec_helper' describe Projects::ParticipantsService do describe '#groups' do + set(:user) { create(:user) } + set(:project) { create(:project, :public) } + let(:service) { described_class.new(project, user) } + + it 'avoids N+1 queries' do + group_1 = create(:group) + group_1.add_owner(user) + + service.groups # Run general application warmup queries + control_count = ActiveRecord::QueryRecorder.new { service.groups }.count + + group_2 = create(:group) + group_2.add_owner(user) + + expect { service.groups }.not_to exceed_query_limit(control_count) + end + + it 'returns correct user counts for groups' do + group_1 = create(:group) + group_1.add_owner(user) + group_1.add_owner(create(:user)) + + group_2 = create(:group) + group_2.add_owner(user) + create(:group_member, :access_request, group: group_2, user: create(:user)) + + expect(service.groups).to contain_exactly( + a_hash_including(name: group_1.full_name, count: 2), + a_hash_including(name: group_2.full_name, count: 1) + ) + end + describe 'avatar_url' do - let(:project) { create(:project, :public) } let(:group) { create(:group, avatar: fixture_file_upload('spec/fixtures/dk.png')) } - let(:user) { create(:user) } - let!(:group_member) { create(:group_member, group: group, user: user) } - it 'should return an url for the avatar' do - participants = described_class.new(project, user) - groups = participants.groups + before do + group.add_owner(user) + end - expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png") + it 'should return an url for the avatar' do + expect(service.groups.size).to eq 1 + expect(service.groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png") end it 'should return an url for the avatar with relative url' do stub_config_setting(relative_url_root: '/gitlab') stub_config_setting(url: Settings.send(:build_gitlab_url)) - participants = described_class.new(project, user) - groups = participants.groups - - expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/-/system/group/avatar/#{group.id}/dk.png") + expect(service.groups.size).to eq 1 + expect(service.groups.first[:avatar_url]).to eq("/gitlab/uploads/-/system/group/avatar/#{group.id}/dk.png") end end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index aae50d5307f..4efd360cb30 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -83,6 +83,7 @@ describe Projects::TransferService do subject { transfer_project(project, user, group) } before do + stub_feature_flags(ci_preparing_state: false) expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator) expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher) end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index ea33d156c8a..8b0f9c8ade2 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -15,6 +15,7 @@ describe QuickActions::InterpretService do let(:service) { described_class.new(project, developer) } before do + stub_licensed_features(multiple_issue_assignees: false) project.add_developer(developer) end diff --git a/spec/services/releases/destroy_service_spec.rb b/spec/services/releases/destroy_service_spec.rb index dd5b8708f36..28663ca8853 100644 --- a/spec/services/releases/destroy_service_spec.rb +++ b/spec/services/releases/destroy_service_spec.rb @@ -28,13 +28,11 @@ describe Releases::DestroyService do end end - context 'when tag is not found' do + context 'when tag does not exist in the repository' do let(:tag) { 'v1.1.1' } - it 'returns an error' do - is_expected.to include(status: :error, - message: 'Tag does not exist', - http_status: 404) + it 'removes the orphaned release' do + expect { subject }.to change { project.releases.count }.by(-1) end end diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb index fe85b5c9065..80b5dcac6c7 100644 --- a/spec/services/suggestions/apply_service_spec.rb +++ b/spec/services/suggestions/apply_service_spec.rb @@ -5,6 +5,41 @@ require 'spec_helper' describe Suggestions::ApplyService do include ProjectForksHelper + shared_examples 'successfully creates commit and updates suggestion' do + def apply(suggestion) + result = subject.execute(suggestion) + expect(result[:status]).to eq(:success) + end + + it 'updates the file with the new contents' do + apply(suggestion) + + blob = project.repository.blob_at_branch(merge_request.source_branch, + position.new_path) + + expect(blob.data).to eq(expected_content) + end + + it 'updates suggestion applied and commit_id columns' do + expect { apply(suggestion) } + .to change(suggestion, :applied) + .from(false).to(true) + .and change(suggestion, :commit_id) + .from(nil) + end + + it 'created commit has users email and name' do + apply(suggestion) + + commit = project.repository.commit + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + expect(commit.author_name).to eq(user.name) + end + end + let(:project) { create(:project, :repository) } let(:user) { create(:user, :commit_email) } @@ -17,9 +52,8 @@ describe Suggestions::ApplyService do end let(:suggestion) do - create(:suggestion, note: diff_note, - from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n", - to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n") + create(:suggestion, :content_from_repo, note: diff_note, + to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n") end subject { described_class.new(user) } @@ -84,39 +118,7 @@ describe Suggestions::ApplyService do project.add_maintainer(user) end - it 'updates the file with the new contents' do - subject.execute(suggestion) - - blob = project.repository.blob_at_branch(merge_request.source_branch, - position.new_path) - - expect(blob.data).to eq(expected_content) - end - - it 'returns success status' do - result = subject.execute(suggestion) - - expect(result[:status]).to eq(:success) - end - - it 'updates suggestion applied and commit_id columns' do - expect { subject.execute(suggestion) } - .to change(suggestion, :applied) - .from(false).to(true) - .and change(suggestion, :commit_id) - .from(nil) - end - - it 'created commit has users email and name' do - subject.execute(suggestion) - - commit = project.repository.commit - - expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) - expect(commit.committer_email).to eq(user.commit_email) - expect(commit.author_name).to eq(user.name) - end + it_behaves_like 'successfully creates commit and updates suggestion' context 'when it fails to apply because the file was changed' do it 'returns error message' do @@ -212,11 +214,13 @@ describe Suggestions::ApplyService do end def apply_suggestion(suggestion) - suggestion.note.reload + suggestion.reload merge_request.reload merge_request.clear_memoized_shas result = subject.execute(suggestion) + expect(result[:status]).to eq(:success) + refresh = MergeRequests::RefreshService.new(project, user) refresh.execute(merge_request.diff_head_sha, suggestion.commit_id, @@ -241,7 +245,7 @@ describe Suggestions::ApplyService do suggestion_2_changes = { old_line: 24, new_line: 31, - from_content: " @cmd_output << stderr.read\n", + from_content: " @cmd_output << stderr.read\n", to_content: "# v2 change\n", path: path } @@ -368,7 +372,18 @@ describe Suggestions::ApplyService do result = subject.execute(suggestion) - expect(result).to eq(message: 'The file was not found', + expect(result).to eq(message: 'Suggestion is not appliable', + status: :error) + end + end + + context 'suggestion is eligible to be outdated' do + it 'returns error message' do + expect(suggestion).to receive(:outdated?) { true } + + result = subject.execute(suggestion) + + expect(result).to eq(message: 'Suggestion is not appliable', status: :error) end end diff --git a/spec/services/suggestions/create_service_spec.rb b/spec/services/suggestions/create_service_spec.rb index 1b4b15b8eaa..ce4990a34a4 100644 --- a/spec/services/suggestions/create_service_spec.rb +++ b/spec/services/suggestions/create_service_spec.rb @@ -40,6 +40,14 @@ describe Suggestions::CreateService do ```thing this is not a suggestion, it's a thing ``` + + ```suggestion:-3+2 + # multi-line suggestion 1 + ``` + + ```suggestion:-5 + # multi-line suggestion 1 + ``` MARKDOWN end @@ -54,7 +62,7 @@ describe Suggestions::CreateService do end it 'does not try to parse suggestions' do - expect(Banzai::SuggestionsParser).not_to receive(:parse) + expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse) subject.execute end @@ -71,7 +79,7 @@ describe Suggestions::CreateService do it 'does not try to parse suggestions' do allow(note).to receive(:on_text?) { false } - expect(Banzai::SuggestionsParser).not_to receive(:parse) + expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse) subject.execute end @@ -87,7 +95,9 @@ describe Suggestions::CreateService do end it 'creates no suggestion when diff file is not found' do - expect(note).to receive(:latest_diff_file) { nil } + expect_next_instance_of(DiffNote) do |diff_note| + expect(diff_note).to receive(:latest_diff_file).twice { nil } + end expect { subject.execute }.not_to change(Suggestion, :count) end @@ -101,43 +111,44 @@ describe Suggestions::CreateService do note: markdown) end - context 'single line suggestions' do - it 'persists suggestion records' do - expect { subject.execute } - .to change { note.suggestions.count } - .from(0) - .to(2) - end + let(:expected_suggestions) do + Gitlab::Diff::SuggestionsParser.parse(markdown, + project: note.project, + position: note.position) + end - it 'persists original from_content lines and suggested lines' do - subject.execute + it 'persists suggestion records' do + expect { subject.execute }.to change { note.suggestions.count } + .from(0).to(expected_suggestions.size) + end - suggestions = note.suggestions.order(:relative_order) + it 'persists suggestions data correctly' do + subject.execute - suggestion_1 = suggestions.first - suggestion_2 = suggestions.last + suggestions = note.suggestions.order(:relative_order) - expect(suggestion_1).to have_attributes(from_content: " vars = {\n", - to_content: " foo\n bar\n") + suggestions.zip(expected_suggestions) do |suggestion, expected_suggestion| + expected_data = expected_suggestion.to_hash - expect(suggestion_2).to have_attributes(from_content: " vars = {\n", - to_content: " xpto\n baz\n") + expect(suggestion.from_content).to eq(expected_data[:from_content]) + expect(suggestion.to_content).to eq(expected_data[:to_content]) + expect(suggestion.lines_above).to eq(expected_data[:lines_above]) + expect(suggestion.lines_below).to eq(expected_data[:lines_below]) end + end - context 'outdated position note' do - let!(:outdated_diff) { merge_request.merge_request_diff } - let!(:latest_diff) { merge_request.create_merge_request_diff } - let(:outdated_position) { build_position(diff_refs: outdated_diff.diff_refs) } - let(:position) { build_position(diff_refs: latest_diff.diff_refs) } + context 'outdated position note' do + let!(:outdated_diff) { merge_request.merge_request_diff } + let!(:latest_diff) { merge_request.create_merge_request_diff } + let(:outdated_position) { build_position(diff_refs: outdated_diff.diff_refs) } + let(:position) { build_position(diff_refs: latest_diff.diff_refs) } - it 'uses the correct position when creating the suggestion' do - expect(note.position) - .to receive(:diff_file) - .with(project_with_repo.repository) - .and_call_original + it 'uses the correct position when creating the suggestion' do + expect(Gitlab::Diff::SuggestionsParser).to receive(:parse) + .with(note.note, project: note.project, position: note.position) + .and_call_original - subject.execute - end + subject.execute end end end diff --git a/spec/services/suggestions/outdate_service_spec.rb b/spec/services/suggestions/outdate_service_spec.rb new file mode 100644 index 00000000000..bcc627013d8 --- /dev/null +++ b/spec/services/suggestions/outdate_service_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Suggestions::OutdateService do + describe '#execute' do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.target_project } + let(:user) { merge_request.author } + let(:file_path) { 'files/ruby/popen.rb' } + let(:branch_name) { project.default_branch } + let(:diff_file) { suggestion.diff_file } + let(:position) { build_position(file_path, comment_line) } + let(:note) do + create(:diff_note_on_merge_request, noteable: merge_request, + position: position, + project: project) + end + + def build_position(path, line) + Gitlab::Diff::Position.new(old_path: path, + new_path: path, + old_line: nil, + new_line: line, + diff_refs: merge_request.diff_refs) + end + + def commit_changes(file_path, new_content) + params = { + file_path: file_path, + commit_message: "Update File", + file_content: new_content, + start_project: project, + start_branch: project.default_branch, + branch_name: branch_name + } + + Files::UpdateService.new(project, user, params).execute + end + + def update_file_line(diff_file, change_line, content) + new_lines = diff_file.new_blob.data.lines + new_lines[change_line..change_line] = content + result = commit_changes(diff_file.file_path, new_lines.join) + newrev = result[:result] + + expect(result[:status]).to eq(:success) + expect(newrev).to be_present + + # Ensure all memoized data is cleared in order + # to generate the new merge_request_diff. + MergeRequest.find(merge_request.id).reload_diff(user) + + note.reload + end + + before do + project.add_maintainer(user) + end + + subject { described_class.new.execute(merge_request) } + + context 'when there is a change within multi-line suggestion range' do + let(:comment_line) { 9 } + let(:lines_above) { 8 } # suggesting to change lines 1..9 + let(:change_line) { 2 } # line 2 is within the range + let!(:suggestion) do + create(:suggestion, :content_from_repo, note: note, lines_above: lines_above) + end + + it 'updates the outdatable suggestion record' do + update_file_line(diff_file, change_line, "# foo\nbar\n") + + # Make sure note is still active + expect(note.active?).to be(true) + + expect { subject }.to change { suggestion.reload.outdated } + .from(false).to(true) + end + end + + context 'when there is no change within multi-line suggestion range' do + let(:comment_line) { 9 } + let(:lines_above) { 3 } # suggesting to change lines 6..9 + let(:change_line) { 2 } # line 2 is not within the range + let!(:suggestion) do + create(:suggestion, :content_from_repo, note: note, lines_above: lines_above) + end + + subject { described_class.new.execute(merge_request) } + + it 'does not outdates suggestion record' do + update_file_line(diff_file, change_line, "# foo\nbar\n") + + # Make sure note is still active + expect(note.active?).to be(true) + + expect { subject }.not_to change { suggestion.reload.outdated }.from(false) + end + end + end +end diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb index 0cbe57352be..e112cdc8881 100644 --- a/spec/services/tags/create_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -41,7 +41,7 @@ describe Tags::CreateService do it 'returns an error' do expect(repository).to receive(:add_tag) .with(user, 'v1.1.0', 'master', 'Foo') - .and_raise(Gitlab::Git::PreReceiveError, 'something went wrong') + .and_raise(Gitlab::Git::PreReceiveError, 'GitLab: something went wrong') response = service.execute('v1.1.0', 'master', 'Foo') diff --git a/spec/services/tags/destroy_service_spec.rb b/spec/services/tags/destroy_service_spec.rb index 7c8c1dd0d3a..a541d300595 100644 --- a/spec/services/tags/destroy_service_spec.rb +++ b/spec/services/tags/destroy_service_spec.rb @@ -7,11 +7,27 @@ describe Tags::DestroyService do let(:service) { described_class.new(project, user) } describe '#execute' do + subject { service.execute(tag_name) } + it 'removes the tag' do expect(repository).to receive(:before_remove_tag) expect(service).to receive(:success) service.execute('v1.1.0') end + + context 'when there is an associated release on the tag' do + let(:tag) { repository.tags.first } + let(:tag_name) { tag.name } + + before do + project.add_maintainer(user) + create(:release, tag: tag_name, project: project) + end + + it 'destroys the release' do + expect { subject }.to change { project.releases.count }.by(-1) + end + end end end diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb index d974cc0226f..ddf9d2b4917 100644 --- a/spec/services/verify_pages_domain_service_spec.rb +++ b/spec/services/verify_pages_domain_service_spec.rb @@ -9,88 +9,130 @@ describe VerifyPagesDomainService do subject(:service) { described_class.new(domain) } describe '#execute' do - context 'verification code recognition (verified domain)' do - where(:domain_sym, :code_sym) do - :domain | :verification_code - :domain | :keyed_verification_code + where(:domain_sym, :code_sym) do + :domain | :verification_code + :domain | :keyed_verification_code - :verification_domain | :verification_code - :verification_domain | :keyed_verification_code - end - - with_them do - set(:domain) { create(:pages_domain) } + :verification_domain | :verification_code + :verification_domain | :keyed_verification_code + end - let(:domain_name) { domain.send(domain_sym) } - let(:verification_code) { domain.send(code_sym) } + with_them do + let(:domain_name) { domain.send(domain_sym) } + let(:verification_code) { domain.send(code_sym) } + shared_examples 'verifies and enables the domain' do it 'verifies and enables the domain' do - stub_resolver(domain_name => ['something else', verification_code]) - expect(service.execute).to eq(status: :success) + expect(domain).to be_verified expect(domain).to be_enabled end + end - it 'verifies and enables when the code is contained partway through a TXT record' do - stub_resolver(domain_name => "something #{verification_code} else") + shared_examples 'successful enablement and verification' do + context 'when txt record contains verification code' do + before do + stub_resolver(domain_name => ['something else', verification_code]) + end - expect(service.execute).to eq(status: :success) - expect(domain).to be_verified - expect(domain).to be_enabled + include_examples 'verifies and enables the domain' end - it 'does not verify when the code is not present' do - stub_resolver(domain_name => 'something else') - - expect(service.execute).to eq(error_status) + context 'when txt record contains verification code with other text' do + before do + stub_resolver(domain_name => "something #{verification_code} else") + end - expect(domain).not_to be_verified - expect(domain).to be_enabled + include_examples 'verifies and enables the domain' end end - context 'verified domain' do - set(:domain) { create(:pages_domain) } + context 'when domain is disabled(or new)' do + let(:domain) { create(:pages_domain, :disabled) } - it 'unverifies (but does not disable) when the right code is not present' do - stub_resolver(domain.domain => 'something else') + include_examples 'successful enablement and verification' - expect(service.execute).to eq(error_status) - expect(domain).not_to be_verified - expect(domain).to be_enabled + shared_examples 'unverifies and disables domain' do + it 'unverifies and disables domain' do + expect(service.execute).to eq(error_status) + + expect(domain).not_to be_verified + expect(domain).not_to be_enabled + end end - it 'unverifies (but does not disable) when no records are present' do - stub_resolver + context 'when txt record does not contain verification code' do + before do + stub_resolver(domain_name => 'something else') + end - expect(service.execute).to eq(error_status) - expect(domain).not_to be_verified - expect(domain).to be_enabled + include_examples 'unverifies and disables domain' + end + + context 'when no txt records are present' do + before do + stub_resolver + end + + include_examples 'unverifies and disables domain' end end - context 'expired domain' do - set(:domain) { create(:pages_domain, :expired) } + context 'when domain is verified' do + let(:domain) { create(:pages_domain) } - it 'verifies and enables when the right code is present' do - stub_resolver(domain.domain => domain.keyed_verification_code) + include_examples 'successful enablement and verification' - expect(service.execute).to eq(status: :success) + context 'when txt record does not contain verification code' do + before do + stub_resolver(domain_name => 'something else') + end - expect(domain).to be_verified - expect(domain).to be_enabled + it 'unverifies but does not disable domain' do + expect(service.execute).to eq(error_status) + expect(domain).not_to be_verified + expect(domain).to be_enabled + end end - it 'disables when the right code is not present' do - error_status[:message] += '. It is now disabled.' + context 'when no txt records are present' do + before do + stub_resolver + end - stub_resolver + it 'unverifies but does not disable domain' do + expect(service.execute).to eq(error_status) + expect(domain).not_to be_verified + expect(domain).to be_enabled + end + end + end - expect(service.execute).to eq(error_status) + context 'when domain is expired' do + let(:domain) { create(:pages_domain, :expired) } - expect(domain).not_to be_verified - expect(domain).not_to be_enabled + context 'when the right code is present' do + before do + stub_resolver(domain_name => domain.keyed_verification_code) + end + + include_examples 'verifies and enables the domain' + end + + context 'when the right code is not present' do + before do + stub_resolver + end + + it 'disables domain' do + error_status[:message] += '. It is now disabled.' + + expect(service.execute).to eq(error_status) + + expect(domain).not_to be_verified + expect(domain).not_to be_enabled + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e8d7b18bf04..60db3e1bc46 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -66,6 +66,7 @@ RSpec.configure do |config| metadata[:type] = match[1].singularize.to_sym if match end + config.include LicenseHelpers config.include ActiveJob::TestHelper config.include ActiveSupport::Testing::TimeHelpers config.include CycleAnalyticsHelpers @@ -96,6 +97,7 @@ RSpec.configure do |config| config.include MigrationsHelpers, :migration config.include RedisHelpers config.include Rails.application.routes.url_helpers, type: :routing + config.include PolicyHelpers, type: :policy if ENV['CI'] # This includes the first try, i.e. tests will be run 4 times before failing. diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb index 5f709831ce1..63b719be03e 100644 --- a/spec/support/api/milestones_shared_examples.rb +++ b/spec/support/api/milestones_shared_examples.rb @@ -72,6 +72,15 @@ shared_examples_for 'group and project milestones' do |route_definition| expect(json_response.first['id']).to eq closed_milestone.id end + it 'returns a milestone by title' do + get api(route, user), params: { title: 'version2' } + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to eq(1) + expect(json_response.first['title']).to eq milestone.title + expect(json_response.first['id']).to eq milestone.id + end + it 'returns a milestone by searching for title' do get api(route, user), params: { search: 'version2' } diff --git a/spec/support/features/issuable_quick_actions_shared_examples.rb b/spec/support/features/issuable_quick_actions_shared_examples.rb deleted file mode 100644 index 2a883ce1074..00000000000 --- a/spec/support/features/issuable_quick_actions_shared_examples.rb +++ /dev/null @@ -1,389 +0,0 @@ -# Specifications for behavior common to all objects with executable attributes. -# It takes a `issuable_type`, and expect an `issuable`. - -shared_examples 'issuable record that supports quick actions in its description and notes' do |issuable_type| - include Spec::Support::Helpers::Features::NotesHelpers - - let(:maintainer) { create(:user) } - let(:project) do - case issuable_type - when :merge_request - create(:project, :public, :repository) - when :issue - create(:project, :public) - end - end - let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } - let!(:label_bug) { create(:label, project: project, title: 'bug') } - let!(:label_feature) { create(:label, project: project, title: 'feature') } - let(:new_url_opts) { {} } - - before do - project.add_maintainer(maintainer) - - gitlab_sign_in(maintainer) - end - - after do - # Ensure all outstanding Ajax requests are complete to avoid database deadlocks - wait_for_requests - end - - describe "new #{issuable_type}", :js do - context 'with commands in the description' do - it "creates the #{issuable_type} and interpret commands accordingly" do - case issuable_type - when :merge_request - visit public_send("namespace_project_new_merge_request_path", project.namespace, project, new_url_opts) - when :issue - visit public_send("new_namespace_project_issue_path", project.namespace, project, new_url_opts) - end - fill_in "#{issuable_type}_title", with: 'bug 345' - fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\"" - click_button "Submit #{issuable_type}".humanize - - issuable = project.public_send(issuable_type.to_s.pluralize).first - - expect(issuable.description).to eq "bug description" - expect(issuable.labels).to eq [label_bug] - expect(issuable.milestone).to eq milestone - expect(page).to have_content 'bug 345' - expect(page).to have_content 'bug description' - end - end - end - - describe "note on #{issuable_type}", :js do - before do - visit public_send("project_#{issuable_type}_path", project, issuable) - end - - context 'with a note containing commands' do - it 'creates a note without the commands and interpret the commands accordingly' do - assignee = create(:user, username: 'bob') - add_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"") - - expect(page).to have_content 'Awesome!' - expect(page).not_to have_content '/assign @bob' - expect(page).not_to have_content '/label ~bug' - expect(page).not_to have_content '/milestone %"ASAP"' - - wait_for_requests - issuable.reload - note = issuable.notes.user.first - - expect(note.note).to eq "Awesome!" - expect(issuable.assignees).to eq [assignee] - expect(issuable.labels).to eq [label_bug] - expect(issuable.milestone).to eq milestone - end - - it 'removes the quick action from note and explains it in the preview' do - preview_note("Awesome!\n\n/close") - - expect(page).to have_content 'Awesome!' - expect(page).not_to have_content '/close' - issuable_name = issuable.is_a?(Issue) ? 'issue' : 'merge request' - expect(page).to have_content "Closes this #{issuable_name}." - end - end - - context 'with a note containing only commands' do - it 'does not create a note but interpret the commands accordingly' do - assignee = create(:user, username: 'bob') - add_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"") - - expect(page).not_to have_content '/assign @bob' - expect(page).not_to have_content '/label ~bug' - expect(page).not_to have_content '/milestone %"ASAP"' - expect(page).to have_content 'Commands applied' - - issuable.reload - - expect(issuable.notes.user).to be_empty - expect(issuable.assignees).to eq [assignee] - expect(issuable.labels).to eq [label_bug] - expect(issuable.milestone).to eq milestone - end - end - - context "with a note closing the #{issuable_type}" do - before do - expect(issuable).to be_open - end - - context "when current user can close #{issuable_type}" do - it "closes the #{issuable_type}" do - add_note("/close") - - expect(page).not_to have_content '/close' - expect(page).to have_content 'Commands applied' - - expect(issuable.reload).to be_closed - end - end - - context "when current user cannot close #{issuable_type}" do - before do - guest = create(:user) - project.add_guest(guest) - - gitlab_sign_out - gitlab_sign_in(guest) - visit public_send("project_#{issuable_type}_path", project, issuable) - end - - it "does not close the #{issuable_type}" do - add_note("/close") - - expect(page).not_to have_content 'Commands applied' - - expect(issuable).to be_open - end - end - end - - context "with a note reopening the #{issuable_type}" do - before do - issuable.close - expect(issuable).to be_closed - end - - context "when current user can reopen #{issuable_type}" do - it "reopens the #{issuable_type}" do - add_note("/reopen") - - expect(page).not_to have_content '/reopen' - expect(page).to have_content 'Commands applied' - - expect(issuable.reload).to be_open - end - end - - context "when current user cannot reopen #{issuable_type}" do - before do - guest = create(:user) - project.add_guest(guest) - - gitlab_sign_out - gitlab_sign_in(guest) - visit public_send("project_#{issuable_type}_path", project, issuable) - end - - it "does not reopen the #{issuable_type}" do - add_note("/reopen") - - expect(page).not_to have_content 'Commands applied' - - expect(issuable).to be_closed - end - end - end - - context "with a note changing the #{issuable_type}'s title" do - context "when current user can change title of #{issuable_type}" do - it "reopens the #{issuable_type}" do - add_note("/title Awesome new title") - - expect(page).not_to have_content '/title' - expect(page).to have_content 'Commands applied' - - expect(issuable.reload.title).to eq 'Awesome new title' - end - end - - context "when current user cannot change title of #{issuable_type}" do - before do - guest = create(:user) - project.add_guest(guest) - - gitlab_sign_out - gitlab_sign_in(guest) - visit public_send("project_#{issuable_type}_path", project, issuable) - end - - it "does not change the #{issuable_type} title" do - add_note("/title Awesome new title") - - expect(page).not_to have_content 'Commands applied' - - expect(issuable.reload.title).not_to eq 'Awesome new title' - end - end - end - - context "with a note marking the #{issuable_type} as todo" do - it "creates a new todo for the #{issuable_type}" do - add_note("/todo") - - expect(page).not_to have_content '/todo' - expect(page).to have_content 'Commands applied' - - todos = TodosFinder.new(maintainer).execute - todo = todos.first - - expect(todos.size).to eq 1 - expect(todo).to be_pending - expect(todo.target).to eq issuable - expect(todo.author).to eq maintainer - expect(todo.user).to eq maintainer - end - end - - context "with a note marking the #{issuable_type} as done" do - before do - TodoService.new.mark_todo(issuable, maintainer) - end - - it "creates a new todo for the #{issuable_type}" do - todos = TodosFinder.new(maintainer).execute - todo = todos.first - - expect(todos.size).to eq 1 - expect(todos.first).to be_pending - expect(todo.target).to eq issuable - expect(todo.author).to eq maintainer - expect(todo.user).to eq maintainer - - add_note("/done") - - expect(page).not_to have_content '/done' - expect(page).to have_content 'Commands applied' - - expect(todo.reload).to be_done - end - end - - context "with a note subscribing to the #{issuable_type}" do - it "creates a new todo for the #{issuable_type}" do - expect(issuable.subscribed?(maintainer, project)).to be_falsy - - add_note("/subscribe") - - expect(page).not_to have_content '/subscribe' - expect(page).to have_content 'Commands applied' - - expect(issuable.subscribed?(maintainer, project)).to be_truthy - end - end - - context "with a note unsubscribing to the #{issuable_type} as done" do - before do - issuable.subscribe(maintainer, project) - end - - it "creates a new todo for the #{issuable_type}" do - expect(issuable.subscribed?(maintainer, project)).to be_truthy - - add_note("/unsubscribe") - - expect(page).not_to have_content '/unsubscribe' - expect(page).to have_content 'Commands applied' - - expect(issuable.subscribed?(maintainer, project)).to be_falsy - end - end - - context "with a note assigning the #{issuable_type} to the current user" do - it "assigns the #{issuable_type} to the current user" do - add_note("/assign me") - - expect(page).not_to have_content '/assign me' - expect(page).to have_content 'Commands applied' - - expect(issuable.reload.assignees).to eq [maintainer] - end - end - - context "with a note locking the #{issuable_type} discussion" do - before do - issuable.update(discussion_locked: false) - expect(issuable).not_to be_discussion_locked - end - - context "when current user can lock #{issuable_type} discussion" do - it "locks the #{issuable_type} discussion" do - add_note("/lock") - - expect(page).not_to have_content '/lock' - expect(page).to have_content 'Commands applied' - - expect(issuable.reload).to be_discussion_locked - end - end - - context "when current user cannot lock #{issuable_type}" do - before do - guest = create(:user) - project.add_guest(guest) - - gitlab_sign_out - sign_in(guest) - visit public_send("project_#{issuable_type}_path", project, issuable) - end - - it "does not lock the #{issuable_type} discussion" do - add_note("/lock") - - expect(page).not_to have_content 'Commands applied' - - expect(issuable).not_to be_discussion_locked - end - end - end - - context "with a note unlocking the #{issuable_type} discussion" do - before do - issuable.update(discussion_locked: true) - expect(issuable).to be_discussion_locked - end - - context "when current user can unlock #{issuable_type} discussion" do - it "unlocks the #{issuable_type} discussion" do - add_note("/unlock") - - expect(page).not_to have_content '/unlock' - expect(page).to have_content 'Commands applied' - - expect(issuable.reload).not_to be_discussion_locked - end - end - - context "when current user cannot unlock #{issuable_type}" do - before do - guest = create(:user) - project.add_guest(guest) - - gitlab_sign_out - sign_in(guest) - visit public_send("project_#{issuable_type}_path", project, issuable) - end - - it "does not unlock the #{issuable_type} discussion" do - add_note("/unlock") - - expect(page).not_to have_content 'Commands applied' - - expect(issuable).to be_discussion_locked - end - end - end - end - - describe "preview of note on #{issuable_type}", :js do - it 'removes quick actions from note and explains them' do - create(:user, username: 'bob') - - visit public_send("project_#{issuable_type}_path", project, issuable) - - page.within('.js-main-target-form') do - fill_in 'note[note]', with: "Awesome!\n/assign @bob " - click_on 'Preview' - - expect(page).to have_content 'Awesome!' - expect(page).not_to have_content '/assign @bob' - expect(page).to have_content 'Assigns @bob.' - end - end - end -end diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index ecefdc23811..33648292037 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -23,7 +23,7 @@ module CycleAnalyticsHelpers return if skip_push_handler - GitPushService.new(project, + Git::BranchPushService.new(project, user, oldrev: oldrev, newrev: commit_shas.last, diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index f525b2f945e..9cae8f934db 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -5,7 +5,7 @@ module JavaScriptFixturesHelpers extend ActiveSupport::Concern include Gitlab::Popen - FIXTURE_PATHS = %w[spec/javascripts/fixtures ee/spec/javascripts/fixtures].freeze + extend self included do |base| base.around do |example| @@ -14,32 +14,32 @@ module JavaScriptFixturesHelpers end end + def fixture_root_path + 'spec/javascripts/fixtures' + end + # Public: Removes all fixture files from given directory # - # directory_name - directory of the fixtures (relative to FIXTURE_PATHS) + # directory_name - directory of the fixtures (relative to .fixture_root_path) # def clean_frontend_fixtures(directory_name) - FIXTURE_PATHS.each do |fixture_path| - directory_name = File.expand_path(directory_name, fixture_path) - Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name| - FileUtils.rm(file_name) - end + full_directory_name = File.expand_path(directory_name, fixture_root_path) + Dir[File.expand_path('*.html', full_directory_name)].each do |file_name| + FileUtils.rm(file_name) end end # Public: Store a response object as fixture file # # response - string or response object to store - # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATHS) + # fixture_file_name - file name to store the fixture in (relative to .fixture_root_path) # def store_frontend_fixture(response, fixture_file_name) - FIXTURE_PATHS.each do |fixture_path| - fixture_file_name = File.expand_path(fixture_file_name, fixture_path) - fixture = response.respond_to?(:body) ? parse_response(response) : response + full_fixture_path = File.expand_path(fixture_file_name, fixture_root_path) + fixture = response.respond_to?(:body) ? parse_response(response) : response - FileUtils.mkdir_p(File.dirname(fixture_file_name)) - File.write(fixture_file_name, fixture) - end + FileUtils.mkdir_p(File.dirname(full_fixture_path)) + File.write(full_fixture_path, fixture) end def remove_repository(project) diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index cca11e112c9..ac52acb6570 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -250,16 +250,19 @@ module KubernetesHelpers # This is a partial response, it will have many more elements in reality but # these are the ones we care about at the moment - def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil) + def kube_pod(name: "kube-pod", environment_slug: "production", project_slug: "project-path-slug", status: "Running", track: nil) { "metadata" => { "name" => name, "generate_name" => "generated-name-with-suffix", "creationTimestamp" => "2016-11-25T19:55:19Z", + "annotations" => { + "app.gitlab.com/env" => environment_slug, + "app.gitlab.com/app" => project_slug + }, "labels" => { - "app" => app, "track" => track - } + }.compact }, "spec" => { "containers" => [ @@ -293,13 +296,16 @@ module KubernetesHelpers } end - def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil) + def kube_deployment(name: "kube-deployment", environment_slug: "production", project_slug: "project-path-slug", track: nil) { "metadata" => { "name" => name, "generation" => 4, + "annotations" => { + "app.gitlab.com/env" => environment_slug, + "app.gitlab.com/app" => project_slug + }, "labels" => { - "app" => app, "track" => track }.compact }, diff --git a/spec/support/helpers/license_helper.rb b/spec/support/helpers/license_helper.rb new file mode 100644 index 00000000000..4aaad55a8ef --- /dev/null +++ b/spec/support/helpers/license_helper.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Placeholder module for EE implementation needed for CE specs to be run in EE codebase +module LicenseHelpers + def stub_licensed_features(features) + # do nothing + end +end diff --git a/spec/support/helpers/policy_helpers.rb b/spec/support/helpers/policy_helpers.rb new file mode 100644 index 00000000000..3d780eb5fb1 --- /dev/null +++ b/spec/support/helpers/policy_helpers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module PolicyHelpers + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end +end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index ff21bbe28ca..cfa9151b2d7 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -84,6 +84,10 @@ module StubConfiguration allow(Gitlab.config.kerberos).to receive_messages(to_settings(messages)) end + def stub_gitlab_shell_setting(messages) + allow(Gitlab.config.gitlab_shell).to receive_messages(to_settings(messages)) + end + private # Modifies stubbed messages to also stub possible predicate versions diff --git a/spec/support/helpers/stub_worker.rb b/spec/support/helpers/stub_worker.rb new file mode 100644 index 00000000000..58b7ee93dff --- /dev/null +++ b/spec/support/helpers/stub_worker.rb @@ -0,0 +1,9 @@ +# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb +module StubWorker + def stub_worker(queue:) + Class.new do + include Sidekiq::Worker + sidekiq_options queue: queue + end + end +end diff --git a/spec/support/matchers/issuable_matchers.rb b/spec/support/matchers/issuable_matchers.rb index f5d9a97051a..62f510b0fbd 100644 --- a/spec/support/matchers/issuable_matchers.rb +++ b/spec/support/matchers/issuable_matchers.rb @@ -1,4 +1,4 @@ -RSpec::Matchers.define :have_header_with_correct_id_and_link do |level, text, id, parent = ".wiki"| +RSpec::Matchers.define :have_header_with_correct_id_and_link do |level, text, id, parent = ".md"| match do |actual| node = find("#{parent} h#{level} a#user-content-#{id}") diff --git a/spec/support/shared_context/policies/project_policy_shared_context.rb b/spec/support/shared_context/policies/project_policy_shared_context.rb new file mode 100644 index 00000000000..8bcd26ec0cd --- /dev/null +++ b/spec/support/shared_context/policies/project_policy_shared_context.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +RSpec.shared_context 'ProjectPolicy context' do + set(:guest) { create(:user) } + set(:reporter) { create(:user) } + set(:developer) { create(:user) } + set(:maintainer) { create(:user) } + set(:owner) { create(:user) } + set(:admin) { create(:admin) } + let(:project) { create(:project, :public, namespace: owner.namespace) } + + let(:base_guest_permissions) do + %i[ + read_project read_board read_list read_wiki read_issue + read_project_for_iids read_issue_iid read_label + read_milestone read_project_snippet read_project_member read_note + create_project create_issue create_note upload_file create_merge_request_in + award_emoji read_release + ] + end + + let(:base_reporter_permissions) do + %i[ + download_code fork_project create_project_snippet update_issue + admin_issue admin_label admin_list read_commit_status read_build + read_container_image read_pipeline read_environment read_deployment + read_merge_request download_wiki_code read_sentry_issue + ] + end + + let(:team_member_reporter_permissions) do + %i[build_download_code build_read_container_image] + end + + let(:developer_permissions) do + %i[ + admin_milestone admin_merge_request update_merge_request create_commit_status + update_commit_status create_build update_build create_pipeline + update_pipeline create_merge_request_from create_wiki push_code + resolve_note create_container_image update_container_image + create_environment create_deployment create_release update_release + ] + end + + let(:base_maintainer_permissions) do + %i[ + push_to_delete_protected_branch update_project_snippet update_environment + update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project + admin_commit_status admin_build admin_container_image + admin_pipeline admin_environment admin_deployment destroy_release add_cluster + daily_statistics + ] + end + + let(:public_permissions) do + %i[ + download_code fork_project read_commit_status read_pipeline + read_container_image build_download_code build_read_container_image + download_wiki_code read_release + ] + end + + let(:base_owner_permissions) do + %i[ + change_namespace change_visibility_level rename_project remove_project + archive_project remove_fork_project destroy_merge_request destroy_issue + set_issue_iid set_issue_created_at set_note_created_at + ] + end + + # Used in EE specs + let(:additional_guest_permissions) { [] } + let(:additional_reporter_permissions) { [] } + let(:additional_maintainer_permissions) { [] } + let(:additional_owner_permissions) { [] } + + let(:guest_permissions) { base_guest_permissions + additional_guest_permissions } + let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions } + let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions } + let(:owner_permissions) { base_owner_permissions + additional_owner_permissions } + + before do + project.add_guest(guest) + project.add_maintainer(maintainer) + project.add_developer(developer) + project.add_reporter(reporter) + end +end diff --git a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb new file mode 100644 index 00000000000..a0d994c4d8d --- /dev/null +++ b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +RSpec.shared_context 'GroupProjectsFinder context' do + let(:group) { create(:group) } + let(:subgroup) { create(:group, parent: group) } + let(:current_user) { create(:user) } + let(:options) { {} } + + let(:finder) { described_class.new(group: group, current_user: current_user, options: options) } + + let!(:public_project) { create(:project, :public, group: group, path: '1') } + let!(:private_project) { create(:project, :private, group: group, path: '2') } + let!(:shared_project_1) { create(:project, :public, path: '3') } + let!(:shared_project_2) { create(:project, :private, path: '4') } + let!(:shared_project_3) { create(:project, :internal, path: '5') } + let!(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) } + let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) } + + before do + shared_project_1.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) + shared_project_2.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) + shared_project_3.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group) + end +end diff --git a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb new file mode 100644 index 00000000000..b8a9554f55f --- /dev/null +++ b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +RSpec.shared_context 'IssuesFinder context' do + set(:user) { create(:user) } + set(:user2) { create(:user) } + set(:group) { create(:group) } + set(:subgroup) { create(:group, parent: group) } + set(:project1) { create(:project, group: group) } + set(:project2) { create(:project) } + set(:project3) { create(:project, group: subgroup) } + set(:milestone) { create(:milestone, project: project1) } + set(:label) { create(:label, project: project2) } + set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) } + set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) } + set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) } + set(:issue4) { create(:issue, project: project3) } + set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) } + set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) } + set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) } +end + +RSpec.shared_context 'IssuesFinder#execute context' do + let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: issue2) } + let(:search_user) { user } + let(:params) { {} } + let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + + before(:context) do + project1.add_maintainer(user) + project2.add_developer(user) + project2.add_developer(user2) + project3.add_developer(user) + + issue1 + issue2 + issue3 + issue4 + + award_emoji1 + award_emoji2 + award_emoji3 + end +end diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb new file mode 100644 index 00000000000..4df80b4168a --- /dev/null +++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests context' do + include ProjectForksHelper + + # We need to explicitly permit Gitaly N+1s because of the specs that use + # :request_store. Gitaly N+1 detection is only enabled when :request_store is, + # but we don't care about potential N+1s when we're just creating several + # projects in the setup phase. + def allow_gitaly_n_plus_1 + Gitlab::GitalyClient.allow_n_plus_1_calls do + yield + end + end + + set(:user) { create(:user) } + set(:user2) { create(:user) } + + set(:group) { create(:group) } + set(:subgroup) { create(:group, parent: group) } + set(:project1) do + allow_gitaly_n_plus_1 { create(:project, :public, group: group) } + end + # We cannot use `set` here otherwise we get: + # Failure/Error: allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) + # The use of doubles or partial doubles from rspec-mocks outside of the per-test lifecycle is not supported. + let(:project2) do + allow_gitaly_n_plus_1 do + fork_project(project1, user) + end + end + let(:project3) do + allow_gitaly_n_plus_1 do + fork_project(project1, user).tap do |project| + project.update!(archived: true) + end + end + end + set(:project4) do + allow_gitaly_n_plus_1 { create(:project, :repository, group: subgroup) } + end + set(:project5) do + allow_gitaly_n_plus_1 { create(:project, group: subgroup) } + end + set(:project6) do + allow_gitaly_n_plus_1 { create(:project, group: subgroup) } + end + + let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') } + let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } + let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') } + let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') } + let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') } + + before do + project1.add_maintainer(user) + project2.add_developer(user) + project3.add_developer(user) + project4.add_developer(user) + project5.add_developer(user) + project6.add_developer(user) + + project2.add_developer(user2) + end +end diff --git a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb new file mode 100644 index 00000000000..9e1f89ee0ed --- /dev/null +++ b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +RSpec.shared_context 'UsersFinder#execute filter by project context' do + set(:normal_user) { create(:user, username: 'johndoe') } + set(:blocked_user) { create(:user, :blocked, username: 'notsorandom') } + set(:external_user) { create(:user, :external) } + set(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') } +end diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb new file mode 100644 index 00000000000..b4808ac0068 --- /dev/null +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +RSpec.shared_context 'GroupPolicy context' do + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:developer) { create(:user) } + let(:maintainer) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + let(:group) { create(:group, :private) } + + let(:guest_permissions) do + %i[ + read_label read_group upload_file read_namespace read_group_activity + read_group_issues read_group_boards read_group_labels read_group_milestones + read_group_merge_requests + ] + end + let(:reporter_permissions) { [:admin_label] } + let(:developer_permissions) { [:admin_milestone] } + let(:maintainer_permissions) do + %i[ + create_projects + read_cluster create_cluster update_cluster admin_cluster add_cluster + ] + end + let(:owner_permissions) do + [ + :admin_group, + :admin_namespace, + :admin_group_member, + :change_visibility_level, + :set_note_created_at, + (Gitlab::Database.postgresql? ? :create_subgroup : nil) + ].compact + end + + before do + group.add_guest(guest) + group.add_reporter(reporter) + group.add_developer(developer) + group.add_maintainer(maintainer) + group.add_owner(owner) + end + + subject { described_class.new(current_user, group) } +end diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb new file mode 100644 index 00000000000..e7ec24c5b7e --- /dev/null +++ b/spec/support/shared_examples/application_setting_examples.rb @@ -0,0 +1,252 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'application settings examples' do + context 'restricted signup domains' do + it 'sets single domain' do + setting.domain_whitelist_raw = 'example.com' + expect(setting.domain_whitelist).to eq(['example.com']) + end + + it 'sets multiple domains with spaces' do + setting.domain_whitelist_raw = 'example.com *.example.com' + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) + end + + it 'sets multiple domains with newlines and a space' do + setting.domain_whitelist_raw = "example.com\n *.example.com" + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) + end + + it 'sets multiple domains with commas' do + setting.domain_whitelist_raw = "example.com, *.example.com" + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) + end + end + + context 'blacklisted signup domains' do + it 'sets single domain' do + setting.domain_blacklist_raw = 'example.com' + expect(setting.domain_blacklist).to contain_exactly('example.com') + end + + it 'sets multiple domains with spaces' do + setting.domain_blacklist_raw = 'example.com *.example.com' + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'sets multiple domains with newlines and a space' do + setting.domain_blacklist_raw = "example.com\n *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'sets multiple domains with commas' do + setting.domain_blacklist_raw = "example.com, *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'sets multiple domains with semicolon' do + setting.domain_blacklist_raw = "example.com; *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'sets multiple domains with mixture of everything' do + setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com') + end + + it 'sets multiple domain with file' do + setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt')) + expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar') + end + end + + describe 'usage ping settings' do + context 'when the usage ping is disabled in gitlab.yml' do + before do + allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false) + end + + it 'does not allow the usage ping to be configured' do + expect(setting.usage_ping_can_be_configured?).to be_falsey + end + + context 'when the usage ping is disabled in the DB' do + before do + setting.usage_ping_enabled = false + end + + it 'returns false for usage_ping_enabled' do + expect(setting.usage_ping_enabled).to be_falsey + end + end + + context 'when the usage ping is enabled in the DB' do + before do + setting.usage_ping_enabled = true + end + + it 'returns false for usage_ping_enabled' do + expect(setting.usage_ping_enabled).to be_falsey + end + end + end + + context 'when the usage ping is enabled in gitlab.yml' do + before do + allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true) + end + + it 'allows the usage ping to be configured' do + expect(setting.usage_ping_can_be_configured?).to be_truthy + end + + context 'when the usage ping is disabled in the DB' do + before do + setting.usage_ping_enabled = false + end + + it 'returns false for usage_ping_enabled' do + expect(setting.usage_ping_enabled).to be_falsey + end + end + + context 'when the usage ping is enabled in the DB' do + before do + setting.usage_ping_enabled = true + end + + it 'returns true for usage_ping_enabled' do + expect(setting.usage_ping_enabled).to be_truthy + end + end + end + end + + describe '#allowed_key_types' do + it 'includes all key types by default' do + expect(setting.allowed_key_types).to contain_exactly(*described_class::SUPPORTED_KEY_TYPES) + end + + it 'excludes disabled key types' do + expect(setting.allowed_key_types).to include(:ed25519) + + setting.ed25519_key_restriction = described_class::FORBIDDEN_KEY_VALUE + + expect(setting.allowed_key_types).not_to include(:ed25519) + end + end + + describe '#key_restriction_for' do + it 'returns the restriction value for recognised types' do + setting.rsa_key_restriction = 1024 + + expect(setting.key_restriction_for(:rsa)).to eq(1024) + end + + it 'allows types to be passed as a string' do + setting.rsa_key_restriction = 1024 + + expect(setting.key_restriction_for('rsa')).to eq(1024) + end + + it 'returns forbidden for unrecognised type' do + expect(setting.key_restriction_for(:foo)).to eq(described_class::FORBIDDEN_KEY_VALUE) + end + end + + describe '#allow_signup?' do + it 'returns true' do + expect(setting.allow_signup?).to be_truthy + end + + it 'returns false if signup is disabled' do + allow(setting).to receive(:signup_enabled?).and_return(false) + + expect(setting.allow_signup?).to be_falsey + end + + it 'returns false if password authentication is disabled for the web interface' do + allow(setting).to receive(:password_authentication_enabled_for_web?).and_return(false) + + expect(setting.allow_signup?).to be_falsey + end + end + + describe '#pick_repository_storage' do + it 'uses Array#sample to pick a random storage' do + array = double('array', sample: 'random') + expect(setting).to receive(:repository_storages).and_return(array) + + expect(setting.pick_repository_storage).to eq('random') + end + end + + describe '#user_default_internal_regex_enabled?' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :user_default_internal_regex, :result) do + false | nil | false + false | '' | false + false | '^(?:(?!\.ext@).)*$\r?\n?' | false + true | '' | false + true | nil | false + true | '^(?:(?!\.ext@).)*$\r?\n?' | true + end + + with_them do + before do + setting.user_default_external = user_default_external + setting.user_default_internal_regex = user_default_internal_regex + end + + subject { setting.user_default_internal_regex_enabled? } + + it { is_expected.to eq(result) } + end + end + + describe '#archive_builds_older_than' do + subject { setting.archive_builds_older_than } + + context 'when the archive_builds_in_seconds is set' do + before do + setting.archive_builds_in_seconds = 3600 + end + + it { is_expected.to be_within(1.minute).of(1.hour.ago) } + end + + context 'when the archive_builds_in_seconds is set' do + before do + setting.archive_builds_in_seconds = nil + end + + it { is_expected.to be_nil } + end + end + + describe '#commit_email_hostname' do + context 'when the value is provided' do + before do + setting.commit_email_hostname = 'localhost' + end + + it 'returns the provided value' do + expect(setting.commit_email_hostname).to eq('localhost') + end + end + + context 'when the value is not provided' do + it 'returns the default from the class' do + expect(setting.commit_email_hostname) + .to eq(described_class.default_commit_email_hostname) + end + end + end + + it 'predicate method changes when value is updated' do + setting.password_authentication_enabled_for_web = false + + expect(setting.password_authentication_enabled_for_web?).to be_falsey + end +end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb new file mode 100644 index 00000000000..7a71e2ee370 --- /dev/null +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'archived project policies' do + let(:feature_write_abilities) do + described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature| + described_class.create_update_admin_destroy(feature) + end + additional_reporter_permissions + additional_maintainer_permissions + end + + let(:other_write_abilities) do + %i[ + create_merge_request_in + create_merge_request_from + push_to_delete_protected_branch + push_code + request_access + upload_file + resolve_note + award_emoji + ] + end + + context 'when the project is archived' do + before do + project.archived = true + end + + it 'disables write actions on all relevant project features' do + expect_disallowed(*feature_write_abilities) + end + + it 'disables some other important write actions' do + expect_disallowed(*other_write_abilities) + end + + it 'does not disable other abilities' do + expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities)) + end + end +end + +RSpec.shared_examples 'project policies as anonymous' do + context 'abilities for public projects' do + context 'when a project has pending invites' do + let(:group) { create(:group, :public) } + let(:project) { create(:project, :public, namespace: group) } + let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } + let(:anonymous_permissions) { guest_permissions - user_permissions } + + subject { described_class.new(nil, project) } + + before do + create(:group_member, :invited, group: group) + end + + it 'does not grant owner access' do + expect_allowed(*anonymous_permissions) + expect_disallowed(*user_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { anonymous_permissions } + end + end + end + + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(nil, project) } + + it { is_expected.to be_banned } + end +end + +RSpec.shared_examples 'project policies as guest' do + subject { described_class.new(guest, project) } + + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + let(:reporter_public_build_permissions) do + reporter_permissions - [:read_build, :read_pipeline] + end + + it do + expect_allowed(*guest_permissions) + expect_disallowed(*reporter_public_build_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { guest_permissions } + end + + context 'public builds enabled' do + it do + expect_allowed(*guest_permissions) + expect_allowed(:read_build, :read_pipeline) + end + end + + context 'when public builds disabled' do + before do + project.update(public_builds: false) + end + + it do + expect_allowed(*guest_permissions) + expect_disallowed(:read_build, :read_pipeline) + end + end + + context 'when builds are disabled' do + before do + project.project_feature.update(builds_access_level: ProjectFeature::DISABLED) + end + + it do + expect_disallowed(:read_build) + expect_allowed(:read_pipeline) + end + end + end +end + +RSpec.shared_examples 'project policies as reporter' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(reporter, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_disallowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { reporter_permissions } + end + end +end + +RSpec.shared_examples 'project policies as developer' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + subject { described_class.new(developer, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_disallowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { developer_permissions } + end + end +end + +RSpec.shared_examples 'project policies as maintainer' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(maintainer, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*maintainer_permissions) + expect_disallowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { maintainer_permissions } + end + end +end + +RSpec.shared_examples 'project policies as owner' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(owner, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_allowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*maintainer_permissions) + expect_allowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { owner_permissions } + end + end +end + +RSpec.shared_examples 'project policies as admin' do + context 'abilities for non-public projects' do + let(:project) { create(:project, namespace: owner.namespace) } + + subject { described_class.new(admin, project) } + + it do + expect_allowed(*guest_permissions) + expect_allowed(*reporter_permissions) + expect_disallowed(*team_member_reporter_permissions) + expect_allowed(*developer_permissions) + expect_allowed(*maintainer_permissions) + expect_allowed(*owner_permissions) + end + + it_behaves_like 'archived project policies' do + let(:regular_abilities) { owner_permissions } + end + end +end diff --git a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb new file mode 100644 index 00000000000..4604d867507 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'tag quick action' do +end diff --git a/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb new file mode 100644 index 00000000000..d97da6be192 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +shared_examples 'assign quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets assign quick action accordingly" do + assignee = create(:user, username: 'bob') + + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/assign @bob" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable.assignees).to eq [assignee] + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + end + + it "creates the #{issuable_type} and interprets assign quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/assign me" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable.assignees).to eq [maintainer] + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the assign quick action accordingly' do + assignee = create(:user, username: 'bob') + add_note("Awesome!\n\n/assign @bob") + + expect(page).to have_content 'Awesome!' + expect(page).not_to have_content '/assign @bob' + + wait_for_requests + issuable.reload + note = issuable.notes.user.first + + expect(note.note).to eq 'Awesome!' + expect(issuable.assignees).to eq [assignee] + end + + it "assigns the #{issuable_type} to the current user" do + add_note("/assign me") + + expect(page).not_to have_content '/assign me' + expect(page).to have_content 'Commands applied' + + expect(issuable.reload.assignees).to eq [maintainer] + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains assign quick action to bob' do + create(:user, username: 'bob') + + visit public_send("project_#{issuable_type}_path", project, issuable) + + page.within('.js-main-target-form') do + fill_in 'note[note]', with: "Awesome!\n/assign @bob " + click_on 'Preview' + + expect(page).not_to have_content '/assign @bob' + expect(page).to have_content 'Awesome!' + expect(page).to have_content 'Assigns @bob.' + end + end + + it 'explains assign quick action to me' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + page.within('.js-main-target-form') do + fill_in 'note[note]', with: "Awesome!\n/assign me" + click_on 'Preview' + + expect(page).not_to have_content '/assign me' + expect(page).to have_content 'Awesome!' + expect(page).to have_content "Assigns @#{maintainer.username}." + end + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb new file mode 100644 index 00000000000..74cbfa3f4b4 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +shared_examples 'award quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets award quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/award :100:" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.award_emoji).to eq [] + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + expect(issuable.award_emoji).to eq [] + end + + it 'creates the note and interprets the award quick action accordingly' do + add_note("/award :100:") + + wait_for_requests + expect(page).not_to have_content '/award' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.award_emoji.last.name).to eq('100') + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains label quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/award :100:') + + expect(page).not_to have_content '/award' + expect(page).to have_selector "gl-emoji[data-name='100']" + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb new file mode 100644 index 00000000000..e0d0b790a0e --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +shared_examples 'close quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets close quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/close" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + end + end + + context "post note to existing #{issuable_type}" do + before do + expect(issuable).to be_opened + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the close quick action accordingly' do + add_note("this is done, close\n\n/close") + + wait_for_requests + expect(page).not_to have_content '/close' + expect(page).to have_content 'this is done, close' + + issuable.reload + note = issuable.notes.user.first + + expect(note.note).to eq 'this is done, close' + expect(issuable).to be_closed + end + + context "when current user cannot close #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + end + + it "does not close the #{issuable_type}" do + add_note('/close') + + expect(page).not_to have_content 'Commands applied' + expect(issuable).to be_open + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains close quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + page.within('.js-main-target-form') do + fill_in 'note[note]', with: "this is done, close\n/close" + click_on 'Preview' + + expect(page).not_to have_content '/close' + expect(page).to have_content 'this is done, close' + expect(page).to have_content "Closes this #{issuable_type.to_s.humanize.downcase}." + end + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb new file mode 100644 index 00000000000..1e1e3c7bc95 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +shared_examples 'copy_metadata quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets copy_metadata quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/copy_metadata #{source_issuable.to_reference(project)}" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).last + + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + issuable.reload + expect(issuable.description).to eq 'bug description' + expect(issuable.milestone).to eq milestone + expect(issuable.labels).to match_array([label_bug, label_feature]) + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets copy_metadata quick action accordingly' do + add_note("/copy_metadata #{source_issuable.to_reference(project)}") + + wait_for_requests + expect(page).not_to have_content '/copy_metadata' + expect(page).to have_content 'Commands applied' + issuable.reload + expect(issuable.milestone).to eq milestone + expect(issuable.labels).to match_array([label_bug, label_feature]) + end + + context "when current user cannot copy_metadata" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not copy_metadata' do + add_note("/copy_metadata #{source_issuable.to_reference(project)}") + + wait_for_requests + expect(page).not_to have_content '/copy_metadata' + expect(page).not_to have_content 'Commands applied' + issuable.reload + expect(issuable.milestone).not_to eq milestone + expect(issuable.labels).to eq [] + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains copy_metadata quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note("/copy_metadata #{source_issuable.to_reference(project)}") + + expect(page).not_to have_content '/copy_metadata' + expect(page).to have_content "Copy labels and milestone from #{source_issuable.to_reference(project)}." + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb new file mode 100644 index 00000000000..8a72bbc13bf --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +shared_examples 'done quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets done quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/done" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + + todos = TodosFinder.new(maintainer).execute + expect(todos.size).to eq 0 + end + end + + context "post note to existing #{issuable_type}" do + before do + TodoService.new.mark_todo(issuable, maintainer) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the done quick action accordingly' do + todos = TodosFinder.new(maintainer).execute + todo = todos.first + expect(todo.reload).to be_pending + + expect(todos.size).to eq 1 + expect(todo.target).to eq issuable + expect(todo.author).to eq maintainer + expect(todo.user).to eq maintainer + + add_note('/done') + + wait_for_requests + expect(page).not_to have_content '/done' + expect(page).to have_content 'Commands applied' + expect(todo.reload).to be_done + end + + context "when current user cannot mark #{issuable_type} todo as done" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it "does not set the #{issuable_type} todo as done" do + todos = TodosFinder.new(maintainer).execute + todo = todos.first + expect(todo.reload).to be_pending + + expect(todos.size).to eq 1 + expect(todo.target).to eq issuable + expect(todo.author).to eq maintainer + expect(todo.user).to eq maintainer + + add_note('/done') + + expect(page).not_to have_content 'Commands applied' + expect(todo.reload).to be_pending + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains done quick action' do + TodoService.new.mark_todo(issuable, maintainer) + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/done') + + expect(page).not_to have_content '/done' + expect(page).to have_content "Marks todo as done." + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb new file mode 100644 index 00000000000..648755d7e55 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +shared_examples 'estimate quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets estimate quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/estimate 1d 2h 3m" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.time_estimate).to eq 36180 + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the estimate quick action accordingly' do + add_note("/estimate 1d 2h 3m") + + wait_for_requests + expect(page).not_to have_content '/estimate' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.time_estimate).to eq 36180 + end + + context "when current user cannot set estimate to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not set estimate' do + add_note("/estimate ~bug ~feature") + + wait_for_requests + expect(page).not_to have_content '/estimate' + expect(issuable.reload.time_estimate).to eq 0 + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains estimate quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/estimate 1d 2h 3m') + + expect(page).not_to have_content '/estimate' + expect(page).to have_content 'Sets time estimate to 1d 2h 3m.' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb new file mode 100644 index 00000000000..9066e382b70 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +shared_examples 'label quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets label quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug ~feature" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.labels).to match_array([label_bug, label_feature]) + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + expect(issuable.labels).to eq [] + end + + it 'creates the note and interprets the label quick action accordingly' do + add_note("/label ~bug ~feature") + + wait_for_requests + expect(page).not_to have_content '/label' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.labels).to match_array([label_bug, label_feature]) + end + + context "when current user cannot set label to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not set label' do + add_note("/label ~bug ~feature") + + wait_for_requests + expect(page).not_to have_content '/label' + expect(issuable.labels).to eq [] + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains label quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/label ~bug ~feature') + + expect(page).not_to have_content '/label' + expect(page).to have_content 'Adds bug feature labels.' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb new file mode 100644 index 00000000000..d3197f2a459 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +shared_examples 'lock quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets lock quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/lock" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable).not_to be_discussion_locked + end + end + + context "post note to existing #{issuable_type}" do + before do + issuable.update(discussion_locked: false) + expect(issuable).not_to be_discussion_locked + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the lock quick action accordingly' do + add_note('/lock') + + wait_for_requests + expect(page).not_to have_content '/lock' + expect(page).to have_content 'Commands applied' + expect(issuable.reload).to be_discussion_locked + end + + context "when current user cannot lock to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it "does not lock the #{issuable_type}" do + add_note('/lock') + + wait_for_requests + expect(page).not_to have_content '/lock' + expect(issuable).not_to be_discussion_locked + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains lock quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/lock') + + expect(page).not_to have_content '/lock' + expect(page).to have_content "Locks the discussion" + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb new file mode 100644 index 00000000000..7f16ce93b6a --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +shared_examples 'milestone quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets milestone quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/milestone %\"ASAP\"" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.milestone).to eq milestone + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + expect(issuable.milestone).to be_nil + end + + it 'creates the note and interprets the milestone quick action accordingly' do + add_note("/milestone %\"ASAP\"") + + wait_for_requests + expect(page).not_to have_content '/milestone' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.milestone).to eq milestone + end + + context "when current user cannot set milestone to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not set milestone' do + add_note('/milestone') + + wait_for_requests + expect(page).not_to have_content '/milestone' + expect(issuable.reload.milestone).to be_nil + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains milestone quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note("/milestone %\"ASAP\"") + + expect(page).not_to have_content '/milestone' + expect(page).to have_content 'Sets the milestone to %ASAP' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb new file mode 100644 index 00000000000..643ae77516a --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +shared_examples 'relabel quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets relabel quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug /relabel ~feature" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.labels).to eq [label_bug, label_feature] + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + issuable.update(labels: [label_bug]) + end + + it 'creates the note and interprets the relabel quick action accordingly' do + add_note('/relabel ~feature') + + wait_for_requests + expect(page).not_to have_content '/relabel' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.labels).to match_array([label_feature]) + end + + it 'creates the note and interprets the relabel quick action with empty param' do + add_note('/relabel') + + wait_for_requests + expect(page).not_to have_content '/relabel' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.labels).to match_array([label_bug]) + end + + context "when current user cannot relabel to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not relabel' do + add_note('/relabel ~feature') + + wait_for_requests + expect(page).not_to have_content '/relabel' + expect(issuable.labels).to match_array([label_bug]) + end + end + end + + context "preview of note on #{issuable_type}", :js do + before do + issuable.update(labels: [label_bug]) + visit public_send("project_#{issuable_type}_path", project, issuable) + end + + it 'explains relabel all quick action' do + preview_note('/relabel ~feature') + + expect(page).not_to have_content '/relabel' + expect(page).to have_content 'Replaces all labels with feature label.' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb new file mode 100644 index 00000000000..24f6f8d5bf4 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +shared_examples 'remove_estimate quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets estimate quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/remove_estimate" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.time_estimate).to eq 0 + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + issuable.update_attribute(:time_estimate, 36180) + end + + it 'creates the note and interprets the remove_estimate quick action accordingly' do + add_note("/remove_estimate") + + wait_for_requests + expect(page).not_to have_content '/remove_estimate' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.time_estimate).to eq 0 + end + + context "when current user cannot remove_estimate" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not remove_estimate' do + add_note('/remove_estimate') + + wait_for_requests + expect(page).not_to have_content '/remove_estimate' + expect(issuable.reload.time_estimate).to eq 36180 + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains remove_estimate quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/remove_estimate') + + expect(page).not_to have_content '/remove_estimate' + expect(page).to have_content 'Removes time estimate.' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb new file mode 100644 index 00000000000..edd92d5cdbc --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +shared_examples 'remove_milestone quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets remove_milestone quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/remove_milestone" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.milestone).to be_nil + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + issuable.update(milestone: milestone) + expect(issuable.milestone).to eq(milestone) + end + + it 'creates the note and interprets the remove_milestone quick action accordingly' do + add_note("/remove_milestone") + + wait_for_requests + expect(page).not_to have_content '/remove_milestone' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.milestone).to be_nil + end + + context "when current user cannot remove milestone to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not remove milestone' do + add_note('/remove_milestone') + + wait_for_requests + expect(page).not_to have_content '/remove_milestone' + expect(issuable.reload.milestone).to eq(milestone) + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains remove_milestone quick action' do + issuable.update(milestone: milestone) + expect(issuable.milestone).to eq(milestone) + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note("/remove_milestone") + + expect(page).not_to have_content '/remove_milestone' + expect(page).to have_content 'Removes %ASAP milestone.' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb new file mode 100644 index 00000000000..6d5894b2318 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +shared_examples 'remove_time_spent quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets remove_time_spent quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/remove_time_spent" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.total_time_spent).to eq 0 + end + end + + context "post note to existing #{issuable_type}" do + before do + issuable.update!(spend_time: { duration: 36180, user_id: maintainer.id }) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the remove_time_spent quick action accordingly' do + add_note("/remove_time_spent") + + wait_for_requests + expect(page).not_to have_content '/remove_time_spent' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.total_time_spent).to eq 0 + end + + context "when current user cannot set remove_time_spent time" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not set remove_time_spent time' do + add_note("/remove_time_spent") + + wait_for_requests + expect(page).not_to have_content '/remove_time_spent' + expect(issuable.reload.total_time_spent).to eq 36180 + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains remove_time_spent quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/remove_time_spent') + + expect(page).not_to have_content '/remove_time_spent' + expect(page).to have_content 'Removes spent time.' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb new file mode 100644 index 00000000000..af173e93bb5 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +shared_examples 'reopen quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets reopen quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/reopen" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + end + end + + context "post note to existing #{issuable_type}" do + before do + issuable.close + expect(issuable).to be_closed + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the reopen quick action accordingly' do + add_note('/reopen') + + wait_for_requests + expect(page).not_to have_content '/reopen' + expect(page).to have_content 'Commands applied' + + issuable.reload + expect(issuable).to be_opened + end + + context "when current user cannot reopen #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it "does not reopen the #{issuable_type}" do + add_note('/reopen') + + expect(page).not_to have_content 'Commands applied' + expect(issuable).to be_closed + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains reopen quick action' do + issuable.close + expect(issuable).to be_closed + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/reopen') + + expect(page).not_to have_content '/reopen' + expect(page).to have_content "Reopens this #{issuable_type.to_s.humanize.downcase}." + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb new file mode 100644 index 00000000000..0a526808585 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +shared_examples 'shrug quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets shrug quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/shrug oops" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq "bug description\noops ¯\\_(ツ)_/¯" + expect(page).to have_content 'bug 345' + expect(page).to have_content "bug description\noops ¯\\_(ツ)_/¯" + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets shrug quick action accordingly' do + add_note("/shrug oops") + + wait_for_requests + expect(page).not_to have_content '/shrug oops' + expect(page).to have_content "oops ¯\\_(ツ)_/¯" + expect(issuable.notes.last.note).to eq "oops ¯\\_(ツ)_/¯" + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains shrug quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/shrug oops') + + expect(page).not_to have_content '/shrug' + expect(page).to have_content "oops ¯\\_(ツ)_/¯" + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb new file mode 100644 index 00000000000..97b4885eba0 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +shared_examples 'spend quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets spend quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/spend 1d 2h 3m" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.total_time_spent).to eq 36180 + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the spend quick action accordingly' do + add_note("/spend 1d 2h 3m") + + wait_for_requests + expect(page).not_to have_content '/spend' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.total_time_spent).to eq 36180 + end + + context "when current user cannot set spend time" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not set spend time' do + add_note("/spend 1s 2h 3m") + + wait_for_requests + expect(page).not_to have_content '/spend' + expect(issuable.reload.total_time_spent).to eq 0 + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains spend quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/spend 1d 2h 3m') + + expect(page).not_to have_content '/spend' + expect(page).to have_content 'Adds 1d 2h 3m spent time.' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb new file mode 100644 index 00000000000..15aefd511a5 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +shared_examples 'subscribe quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets subscribe quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/subscribe" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.subscribed?(maintainer, project)).to be_truthy + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + expect(issuable.subscribed?(maintainer, project)).to be_falsy + end + + it 'creates the note and interprets the subscribe quick action accordingly' do + add_note('/subscribe') + + wait_for_requests + expect(page).not_to have_content '/subscribe' + expect(page).to have_content 'Commands applied' + expect(issuable.subscribed?(maintainer, project)).to be_truthy + end + + context "when current user cannot subscribe to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it "does not subscribe to the #{issuable_type}" do + add_note('/subscribe') + + wait_for_requests + expect(page).not_to have_content '/subscribe' + expect(page).to have_content 'Commands applied' + expect(issuable.subscribed?(maintainer, project)).to be_falsy + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains subscribe quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/subscribe') + + expect(page).not_to have_content '/subscribe' + expect(page).to have_content "Subscribes to this #{issuable_type.to_s.humanize.downcase}" + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb new file mode 100644 index 00000000000..ef831e39872 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +shared_examples 'tableflip quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets tableflip quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/tableflip oops" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq "bug description\noops (╯°□°)╯︵ ┻━┻" + expect(page).to have_content 'bug 345' + expect(page).to have_content "bug description\noops (╯°□°)╯︵ ┻━┻" + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets tableflip quick action accordingly' do + add_note("/tableflip oops") + + wait_for_requests + expect(page).not_to have_content '/tableflip oops' + expect(page).to have_content "oops (╯°□°)╯︵ ┻━┻" + expect(issuable.notes.last.note).to eq "oops (╯°□°)╯︵ ┻━┻" + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains tableflip quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/tableflip oops') + + expect(page).not_to have_content '/tableflip' + expect(page).to have_content "oops (╯°□°)╯︵ ┻━┻" + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb new file mode 100644 index 00000000000..ed904c8d539 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +shared_examples 'issuable time tracker' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + after do + wait_for_requests + end + + it 'renders the sidebar component empty state' do + page.within '.time-tracking-no-tracking-pane' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'updates the sidebar component when estimate is added' do + submit_time('/estimate 3w 1d 1h') + + wait_for_requests + page.within '.time-tracking-estimate-only-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'updates the sidebar component when spent is added' do + submit_time('/spend 3w 1d 1h') + + wait_for_requests + page.within '.time-tracking-spend-only-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'shows the comparison when estimate and spent are added' do + submit_time('/estimate 3w 1d 1h') + submit_time('/spend 3w 1d 1h') + + wait_for_requests + page.within '.time-tracking-comparison-pane' do + expect(page).to have_content '3w 1d 1h' + end + end + + it 'updates the sidebar component when estimate is removed' do + submit_time('/estimate 3w 1d 1h') + submit_time('/remove_estimate') + + page.within '.time-tracking-component-wrap' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'updates the sidebar component when spent is removed' do + submit_time('/spend 3w 1d 1h') + submit_time('/remove_time_spent') + + page.within '.time-tracking-component-wrap' do + expect(page).to have_content 'No estimate or time spent' + end + end + + it 'shows the help state when icon is clicked' do + page.within '.time-tracking-component-wrap' do + find('.help-button').click + expect(page).to have_content 'Track time with quick actions' + expect(page).to have_content 'Learn more' + end + end + + it 'hides the help state when close icon is clicked' do + page.within '.time-tracking-component-wrap' do + find('.help-button').click + find('.close-help-button').click + + expect(page).not_to have_content 'Track time with quick actions' + expect(page).not_to have_content 'Learn more' + end + end + + it 'displays the correct help url' do + page.within '.time-tracking-component-wrap' do + find('.help-button').click + + expect(find_link('Learn more')[:href]).to have_content('/help/workflow/time_tracking.md') + end + end +end + +def submit_time(quick_action) + fill_in 'note[note]', with: quick_action + find('.js-comment-submit-button').click + wait_for_requests +end diff --git a/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb new file mode 100644 index 00000000000..93a69093dde --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +shared_examples 'title quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets title quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/title new title" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(issuable.title).to eq 'bug 345' + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the title quick action accordingly' do + add_note('/title New title') + + wait_for_requests + expect(page).not_to have_content '/title new title' + expect(page).to have_content 'Commands applied' + expect(page).to have_content 'New title' + + issuable.reload + expect(issuable.title).to eq 'New title' + end + + context "when current user cannot set title #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it "does not set title to the #{issuable_type}" do + add_note('/title New title') + + expect(page).not_to have_content 'Commands applied' + expect(issuable.title).not_to eq 'New title' + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains title quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/title New title') + wait_for_requests + + expect(page).not_to have_content '/title New title' + expect(page).to have_content 'Changes the title to "New title".' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb new file mode 100644 index 00000000000..cccc28127ce --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +shared_examples 'todo quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets todo quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/todo" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + + todos = TodosFinder.new(maintainer).execute + expect(todos.size).to eq 0 + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the todo quick action accordingly' do + add_note('/todo') + + wait_for_requests + expect(page).not_to have_content '/todo' + expect(page).to have_content 'Commands applied' + + todos = TodosFinder.new(maintainer).execute + todo = todos.first + + expect(todos.size).to eq 1 + expect(todo).to be_pending + expect(todo.target).to eq issuable + expect(todo.author).to eq maintainer + expect(todo.user).to eq maintainer + end + + context "when current user cannot add todo #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it "does not add todo the #{issuable_type}" do + add_note('/todo') + + expect(page).not_to have_content 'Commands applied' + todos = TodosFinder.new(maintainer).execute + expect(todos.size).to eq 0 + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains todo quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/todo') + + expect(page).not_to have_content '/todo' + expect(page).to have_content "Adds a todo." + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb new file mode 100644 index 00000000000..0b1a52bc860 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +shared_examples 'unassign quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets unassign quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/unassign @bob" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable.assignees).to eq [] + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + end + + it "creates the #{issuable_type} and interprets unassign quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/unassign me" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable.assignees).to eq [] + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the unassign quick action accordingly' do + assignee = create(:user, username: 'bob') + issuable.update(assignee_ids: [assignee.id]) + expect(issuable.assignees).to eq [assignee] + + add_note("Awesome!\n\n/unassign @bob") + + expect(page).to have_content 'Awesome!' + expect(page).not_to have_content '/unassign @bob' + + wait_for_requests + issuable.reload + note = issuable.notes.user.first + + expect(note.note).to eq 'Awesome!' + expect(issuable.assignees).to eq [] + end + + it "unassigns the #{issuable_type} from current user" do + issuable.update(assignee_ids: [maintainer.id]) + expect(issuable.reload.assignees).to eq [maintainer] + expect(issuable.assignees).to eq [maintainer] + + add_note("/unassign me") + + expect(page).not_to have_content '/unassign me' + expect(page).to have_content 'Commands applied' + + expect(issuable.reload.assignees).to eq [] + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains unassign quick action: from bob' do + assignee = create(:user, username: 'bob') + issuable.update(assignee_ids: [assignee.id]) + expect(issuable.assignees).to eq [assignee] + + visit public_send("project_#{issuable_type}_path", project, issuable) + + page.within('.js-main-target-form') do + fill_in 'note[note]', with: "Awesome!\n/unassign @bob " + click_on 'Preview' + + expect(page).not_to have_content '/unassign @bob' + expect(page).to have_content 'Awesome!' + expect(page).to have_content 'Removes assignee @bob.' + end + end + + it 'explains unassign quick action: from me' do + issuable.update(assignee_ids: [maintainer.id]) + expect(issuable.assignees).to eq [maintainer] + + visit public_send("project_#{issuable_type}_path", project, issuable) + + page.within('.js-main-target-form') do + fill_in 'note[note]', with: "Awesome!\n/unassign me" + click_on 'Preview' + + expect(page).not_to have_content '/unassign me' + expect(page).to have_content 'Awesome!' + expect(page).to have_content "Removes assignee @#{maintainer.username}." + end + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb new file mode 100644 index 00000000000..1a1ee05841f --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +shared_examples 'unlabel quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets unlabel quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug /unlabel" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.labels).to eq [label_bug] + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + issuable.update(labels: [label_bug, label_feature]) + end + + it 'creates the note and interprets the unlabel all quick action accordingly' do + add_note("/unlabel") + + wait_for_requests + expect(page).not_to have_content '/unlabel' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.labels).to eq [] + end + + it 'creates the note and interprets the unlabel some quick action accordingly' do + add_note("/unlabel ~bug") + + wait_for_requests + expect(page).not_to have_content '/unlabel' + expect(page).to have_content 'Commands applied' + expect(issuable.reload.labels).to match_array([label_feature]) + end + + context "when current user cannot unlabel to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'does not unlabel' do + add_note("/unlabel") + + wait_for_requests + expect(page).not_to have_content '/unlabel' + expect(issuable.labels).to match_array([label_bug, label_feature]) + end + end + end + + context "preview of note on #{issuable_type}", :js do + before do + issuable.update(labels: [label_bug, label_feature]) + visit public_send("project_#{issuable_type}_path", project, issuable) + end + + it 'explains unlabel all quick action' do + preview_note('/unlabel') + + expect(page).not_to have_content '/unlabel' + expect(page).to have_content 'Removes all labels.' + end + + it 'explains unlabel some quick action' do + preview_note('/unlabel ~bug') + + expect(page).not_to have_content '/unlabel' + expect(page).to have_content 'Removes bug label.' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb new file mode 100644 index 00000000000..998ff99b32e --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +shared_examples 'unlock quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets unlock quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/unlock" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable).not_to be_discussion_locked + end + end + + context "post note to existing #{issuable_type}" do + before do + issuable.update(discussion_locked: true) + expect(issuable).to be_discussion_locked + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it 'creates the note and interprets the unlock quick action accordingly' do + add_note('/unlock') + + wait_for_requests + expect(page).not_to have_content '/unlock' + expect(page).to have_content 'Commands applied' + expect(issuable.reload).not_to be_discussion_locked + end + + context "when current user cannot unlock to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it "does not lock the #{issuable_type}" do + add_note('/unlock') + + wait_for_requests + expect(page).not_to have_content '/unlock' + expect(issuable).to be_discussion_locked + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains unlock quick action' do + issuable.update(discussion_locked: true) + expect(issuable).to be_discussion_locked + + visit public_send("project_#{issuable_type}_path", project, issuable) + + preview_note('/unlock') + + expect(page).not_to have_content '/unlock' + expect(page).to have_content 'Unlocks the discussion' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb new file mode 100644 index 00000000000..bd92f133889 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +shared_examples 'unsubscribe quick action' do |issuable_type| + before do + project.add_maintainer(maintainer) + gitlab_sign_in(maintainer) + end + + context "new #{issuable_type}", :js do + before do + case issuable_type + when :merge_request + visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts) + wait_for_all_requests + when :issue + visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts) + wait_for_all_requests + end + end + + it "creates the #{issuable_type} and interprets unsubscribe quick action accordingly" do + fill_in "#{issuable_type}_title", with: 'bug 345' + fill_in "#{issuable_type}_description", with: "bug description\n/unsubscribe" + click_button "Submit #{issuable_type}".humanize + + issuable = project.public_send(issuable_type.to_s.pluralize).first + + expect(issuable.description).to eq 'bug description' + expect(issuable).to be_opened + expect(page).to have_content 'bug 345' + expect(page).to have_content 'bug description' + expect(issuable.subscribed?(maintainer, project)).to be_truthy + end + end + + context "post note to existing #{issuable_type}" do + before do + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + issuable.subscribe(maintainer, project) + expect(issuable.subscribed?(maintainer, project)).to be_truthy + end + + it 'creates the note and interprets the unsubscribe quick action accordingly' do + add_note('/unsubscribe') + + wait_for_requests + expect(page).not_to have_content '/unsubscribe' + expect(page).to have_content 'Commands applied' + expect(issuable.subscribed?(maintainer, project)).to be_falsey + end + + context "when current user cannot unsubscribe to #{issuable_type}" do + before do + guest = create(:user) + project.add_guest(guest) + + gitlab_sign_out + gitlab_sign_in(guest) + visit public_send("project_#{issuable_type}_path", project, issuable) + wait_for_all_requests + end + + it "does not unsubscribe to the #{issuable_type}" do + add_note('/unsubscribe') + + wait_for_requests + expect(page).not_to have_content '/unsubscribe' + expect(issuable.subscribed?(maintainer, project)).to be_truthy + end + end + end + + context "preview of note on #{issuable_type}", :js do + it 'explains unsubscribe quick action' do + visit public_send("project_#{issuable_type}_path", project, issuable) + issuable.subscribe(maintainer, project) + expect(issuable.subscribed?(maintainer, project)).to be_truthy + + preview_note('/unsubscribe') + + expect(page).not_to have_content '/unsubscribe' + expect(page).to have_content "Unsubscribes from this #{issuable_type.to_s.humanize.downcase}." + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb new file mode 100644 index 00000000000..6edd20bb024 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'board_move quick action' do +end diff --git a/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb new file mode 100644 index 00000000000..c68e5aee842 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'confidential quick action' do +end diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb new file mode 100644 index 00000000000..5bfc3bb222f --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'create_merge_request quick action' do +end diff --git a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb new file mode 100644 index 00000000000..db3ecccc339 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +shared_examples 'due quick action not available' do + it 'does not set the due date' do + add_note('/due 2016-08-28') + + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content '/due 2016-08-28' + end +end + +shared_examples 'due quick action available and date can be added' do + it 'sets the due date accordingly' do + add_note('/due 2016-08-28') + + expect(page).not_to have_content '/due 2016-08-28' + expect(page).to have_content 'Commands applied' + + visit project_issue_path(project, issue) + + page.within '.due_date' do + expect(page).to have_content 'Aug 28, 2016' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb new file mode 100644 index 00000000000..24576fe0021 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'duplicate quick action' do +end diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb new file mode 100644 index 00000000000..953e67b0423 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'move quick action' do +end diff --git a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb new file mode 100644 index 00000000000..5904164fcfc --- /dev/null +++ b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +shared_examples 'remove_due_date action not available' do + it 'does not remove the due date' do + add_note("/remove_due_date") + + expect(page).not_to have_content 'Commands applied' + expect(page).not_to have_content '/remove_due_date' + end +end + +shared_examples 'remove_due_date action available and due date can be removed' do + it 'removes the due date accordingly' do + add_note('/remove_due_date') + + expect(page).not_to have_content '/remove_due_date' + expect(page).to have_content 'Commands applied' + + visit project_issue_path(project, issue) + + page.within '.due_date' do + expect(page).to have_content 'No due date' + end + end +end diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb new file mode 100644 index 00000000000..31d88183f0d --- /dev/null +++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'merge quick action' do +end diff --git a/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb new file mode 100644 index 00000000000..ccb4a85325b --- /dev/null +++ b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'target_branch quick action' do +end diff --git a/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb new file mode 100644 index 00000000000..6abb12b41b2 --- /dev/null +++ b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +shared_examples 'wip quick action' do +end diff --git a/spec/support/shared_examples/snippet_visibility.rb b/spec/support/shared_examples/snippet_visibility.rb deleted file mode 100644 index 3a7c69b7877..00000000000 --- a/spec/support/shared_examples/snippet_visibility.rb +++ /dev/null @@ -1,322 +0,0 @@ -RSpec.shared_examples 'snippet visibility' do - let!(:author) { create(:user) } - let!(:member) { create(:user) } - let!(:external) { create(:user, :external) } - - let!(:snippet_type_visibilities) do - { - public: Snippet::PUBLIC, - internal: Snippet::INTERNAL, - private: Snippet::PRIVATE - } - end - - context "For project snippets" do - let!(:users) do - { - unauthenticated: nil, - external: external, - non_member: create(:user), - member: member, - author: author - } - end - - let!(:project_type_visibilities) do - { - public: Gitlab::VisibilityLevel::PUBLIC, - internal: Gitlab::VisibilityLevel::INTERNAL, - private: Gitlab::VisibilityLevel::PRIVATE - } - end - - let(:project_feature_visibilities) do - { - enabled: ProjectFeature::ENABLED, - private: ProjectFeature::PRIVATE, - disabled: ProjectFeature::DISABLED - } - end - - where(:project_type, :feature_visibility, :user_type, :snippet_type, :outcome) do - [ - # Public projects - [:public, :enabled, :unauthenticated, :public, true], - [:public, :enabled, :unauthenticated, :internal, false], - [:public, :enabled, :unauthenticated, :private, false], - - [:public, :enabled, :external, :public, true], - [:public, :enabled, :external, :internal, false], - [:public, :enabled, :external, :private, false], - - [:public, :enabled, :non_member, :public, true], - [:public, :enabled, :non_member, :internal, true], - [:public, :enabled, :non_member, :private, false], - - [:public, :enabled, :member, :public, true], - [:public, :enabled, :member, :internal, true], - [:public, :enabled, :member, :private, true], - - [:public, :enabled, :author, :public, true], - [:public, :enabled, :author, :internal, true], - [:public, :enabled, :author, :private, true], - - [:public, :private, :unauthenticated, :public, false], - [:public, :private, :unauthenticated, :internal, false], - [:public, :private, :unauthenticated, :private, false], - - [:public, :private, :external, :public, false], - [:public, :private, :external, :internal, false], - [:public, :private, :external, :private, false], - - [:public, :private, :non_member, :public, false], - [:public, :private, :non_member, :internal, false], - [:public, :private, :non_member, :private, false], - - [:public, :private, :member, :public, true], - [:public, :private, :member, :internal, true], - [:public, :private, :member, :private, true], - - [:public, :private, :author, :public, true], - [:public, :private, :author, :internal, true], - [:public, :private, :author, :private, true], - - [:public, :disabled, :unauthenticated, :public, false], - [:public, :disabled, :unauthenticated, :internal, false], - [:public, :disabled, :unauthenticated, :private, false], - - [:public, :disabled, :external, :public, false], - [:public, :disabled, :external, :internal, false], - [:public, :disabled, :external, :private, false], - - [:public, :disabled, :non_member, :public, false], - [:public, :disabled, :non_member, :internal, false], - [:public, :disabled, :non_member, :private, false], - - [:public, :disabled, :member, :public, false], - [:public, :disabled, :member, :internal, false], - [:public, :disabled, :member, :private, false], - - [:public, :disabled, :author, :public, false], - [:public, :disabled, :author, :internal, false], - [:public, :disabled, :author, :private, false], - - # Internal projects - [:internal, :enabled, :unauthenticated, :public, false], - [:internal, :enabled, :unauthenticated, :internal, false], - [:internal, :enabled, :unauthenticated, :private, false], - - [:internal, :enabled, :external, :public, false], - [:internal, :enabled, :external, :internal, false], - [:internal, :enabled, :external, :private, false], - - [:internal, :enabled, :non_member, :public, true], - [:internal, :enabled, :non_member, :internal, true], - [:internal, :enabled, :non_member, :private, false], - - [:internal, :enabled, :member, :public, true], - [:internal, :enabled, :member, :internal, true], - [:internal, :enabled, :member, :private, true], - - [:internal, :enabled, :author, :public, true], - [:internal, :enabled, :author, :internal, true], - [:internal, :enabled, :author, :private, true], - - [:internal, :private, :unauthenticated, :public, false], - [:internal, :private, :unauthenticated, :internal, false], - [:internal, :private, :unauthenticated, :private, false], - - [:internal, :private, :external, :public, false], - [:internal, :private, :external, :internal, false], - [:internal, :private, :external, :private, false], - - [:internal, :private, :non_member, :public, false], - [:internal, :private, :non_member, :internal, false], - [:internal, :private, :non_member, :private, false], - - [:internal, :private, :member, :public, true], - [:internal, :private, :member, :internal, true], - [:internal, :private, :member, :private, true], - - [:internal, :private, :author, :public, true], - [:internal, :private, :author, :internal, true], - [:internal, :private, :author, :private, true], - - [:internal, :disabled, :unauthenticated, :public, false], - [:internal, :disabled, :unauthenticated, :internal, false], - [:internal, :disabled, :unauthenticated, :private, false], - - [:internal, :disabled, :external, :public, false], - [:internal, :disabled, :external, :internal, false], - [:internal, :disabled, :external, :private, false], - - [:internal, :disabled, :non_member, :public, false], - [:internal, :disabled, :non_member, :internal, false], - [:internal, :disabled, :non_member, :private, false], - - [:internal, :disabled, :member, :public, false], - [:internal, :disabled, :member, :internal, false], - [:internal, :disabled, :member, :private, false], - - [:internal, :disabled, :author, :public, false], - [:internal, :disabled, :author, :internal, false], - [:internal, :disabled, :author, :private, false], - - # Private projects - [:private, :enabled, :unauthenticated, :public, false], - [:private, :enabled, :unauthenticated, :internal, false], - [:private, :enabled, :unauthenticated, :private, false], - - [:private, :enabled, :external, :public, true], - [:private, :enabled, :external, :internal, true], - [:private, :enabled, :external, :private, true], - - [:private, :enabled, :non_member, :public, false], - [:private, :enabled, :non_member, :internal, false], - [:private, :enabled, :non_member, :private, false], - - [:private, :enabled, :member, :public, true], - [:private, :enabled, :member, :internal, true], - [:private, :enabled, :member, :private, true], - - [:private, :enabled, :author, :public, true], - [:private, :enabled, :author, :internal, true], - [:private, :enabled, :author, :private, true], - - [:private, :private, :unauthenticated, :public, false], - [:private, :private, :unauthenticated, :internal, false], - [:private, :private, :unauthenticated, :private, false], - - [:private, :private, :external, :public, true], - [:private, :private, :external, :internal, true], - [:private, :private, :external, :private, true], - - [:private, :private, :non_member, :public, false], - [:private, :private, :non_member, :internal, false], - [:private, :private, :non_member, :private, false], - - [:private, :private, :member, :public, true], - [:private, :private, :member, :internal, true], - [:private, :private, :member, :private, true], - - [:private, :private, :author, :public, true], - [:private, :private, :author, :internal, true], - [:private, :private, :author, :private, true], - - [:private, :disabled, :unauthenticated, :public, false], - [:private, :disabled, :unauthenticated, :internal, false], - [:private, :disabled, :unauthenticated, :private, false], - - [:private, :disabled, :external, :public, false], - [:private, :disabled, :external, :internal, false], - [:private, :disabled, :external, :private, false], - - [:private, :disabled, :non_member, :public, false], - [:private, :disabled, :non_member, :internal, false], - [:private, :disabled, :non_member, :private, false], - - [:private, :disabled, :member, :public, false], - [:private, :disabled, :member, :internal, false], - [:private, :disabled, :member, :private, false], - - [:private, :disabled, :author, :public, false], - [:private, :disabled, :author, :internal, false], - [:private, :disabled, :author, :private, false] - ] - end - - with_them do - let!(:project) { create(:project, visibility_level: project_type_visibilities[project_type]) } - let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, project_feature_visibilities[feature_visibility]) } - let!(:user) { users[user_type] } - let!(:snippet) { create(:project_snippet, visibility_level: snippet_type_visibilities[snippet_type], project: project, author: author) } - let!(:members) do - project.add_developer(author) - project.add_developer(member) - project.add_developer(external) if project.private? - end - - context "For #{params[:project_type]} project and #{params[:user_type]} users" do - it 'should agree with the read_project_snippet policy' do - expect(can?(user, :read_project_snippet, snippet)).to eq(outcome) - end - - it 'should return proper outcome' do - results = described_class.new(user, project: project).execute - expect(results.include?(snippet)).to eq(outcome) - end - end - - context "Without a given project and #{params[:user_type]} users" do - it 'should return proper outcome' do - results = described_class.new(user).execute - expect(results.include?(snippet)).to eq(outcome) - end - - it 'returns no snippets when the user cannot read cross project' do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } - - snippets = described_class.new(user).execute - - expect(snippets).to be_empty - end - end - end - end - - context 'For personal snippets' do - let!(:users) do - { - unauthenticated: nil, - external: external, - non_member: create(:user), - author: author - } - end - - where(:snippet_visibility, :user_type, :outcome) do - [ - [:public, :unauthenticated, true], - [:public, :external, true], - [:public, :non_member, true], - [:public, :author, true], - - [:internal, :unauthenticated, false], - [:internal, :external, false], - [:internal, :non_member, true], - [:internal, :author, true], - - [:private, :unauthenticated, false], - [:private, :external, false], - [:private, :non_member, false], - [:private, :author, true] - ] - end - - with_them do - let!(:user) { users[user_type] } - let!(:snippet) { create(:personal_snippet, visibility_level: snippet_type_visibilities[snippet_visibility], author: author) } - - context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do - it 'should agree with read_personal_snippet policy' do - expect(can?(user, :read_personal_snippet, snippet)).to eq(outcome) - end - - it 'should return proper outcome' do - results = described_class.new(user).execute - expect(results.include?(snippet)).to eq(outcome) - end - - it 'should return personal snippets when the user cannot read cross project' do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } - - results = described_class.new(user).execute - - expect(results.include?(snippet)).to eq(outcome) - end - end - end - end -end diff --git a/spec/support/shared_examples/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/snippet_visibility_shared_examples.rb new file mode 100644 index 00000000000..4f662db2120 --- /dev/null +++ b/spec/support/shared_examples/snippet_visibility_shared_examples.rb @@ -0,0 +1,306 @@ +RSpec.shared_examples 'snippet visibility' do + using RSpec::Parameterized::TableSyntax + + # Make sure no snippets exist prior to running the test matrix + before(:context) do + DatabaseCleaner.clean_with(:truncation) + end + + set(:author) { create(:user) } + set(:member) { create(:user) } + set(:external) { create(:user, :external) } + + context "For project snippets" do + let!(:users) do + { + unauthenticated: nil, + external: external, + non_member: create(:user), + member: member, + author: author + } + end + + where(:project_type, :feature_visibility, :user_type, :snippet_type, :outcome) do + [ + # Public projects + [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, true], + [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], + [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + + [:public, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, true], + [:public, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, false], + [:public, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, false], + + [:public, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, true], + [:public, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, true], + [:public, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + + [:public, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], + [:public, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], + [:public, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + + [:public, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], + [:public, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], + [:public, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + + [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], + [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], + [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + + [:public, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, false], + [:public, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, false], + [:public, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, false], + + [:public, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], + [:public, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], + [:public, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + + [:public, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], + [:public, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], + [:public, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + + [:public, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], + [:public, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], + [:public, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + + [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], + [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], + [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + + [:public, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], + [:public, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], + [:public, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + + [:public, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], + [:public, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], + [:public, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + + [:public, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], + [:public, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], + [:public, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + + [:public, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], + [:public, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], + [:public, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false], + + # Internal projects + [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, false], + [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], + [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, false], + [:internal, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, false], + [:internal, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, true], + [:internal, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, true], + [:internal, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], + [:internal, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], + [:internal, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + + [:internal, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], + [:internal, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], + [:internal, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + + [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], + [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], + [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, false], + [:internal, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, false], + [:internal, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], + [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], + [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], + [:internal, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], + [:internal, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + + [:internal, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], + [:internal, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], + [:internal, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + + [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], + [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], + [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], + [:internal, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], + [:internal, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], + [:internal, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], + [:internal, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], + [:internal, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], + [:internal, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + + [:internal, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], + [:internal, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], + [:internal, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false], + + # Private projects + [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, false], + [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false], + [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false], + + [:private, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, true], + [:private, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, true], + [:private, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, true], + + [:private, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, false], + [:private, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, false], + [:private, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false], + + [:private, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true], + [:private, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true], + [:private, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true], + + [:private, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true], + [:private, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true], + [:private, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true], + + [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false], + [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false], + [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false], + + [:private, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, true], + [:private, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, true], + [:private, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, true], + + [:private, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false], + [:private, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false], + [:private, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false], + + [:private, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true], + [:private, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true], + [:private, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true], + + [:private, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true], + [:private, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true], + [:private, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true], + + [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false], + [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false], + [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false], + + [:private, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false], + [:private, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false], + [:private, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false], + + [:private, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false], + [:private, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false], + [:private, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false], + + [:private, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false], + [:private, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false], + [:private, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false], + + [:private, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false], + [:private, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false], + [:private, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false] + ] + end + + with_them do + let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel.level_value(project_type.to_s)) } + let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, feature_visibility) } + let!(:user) { users[user_type] } + let!(:snippet) { create(:project_snippet, visibility_level: snippet_type, project: project, author: author) } + let!(:members) do + project.add_developer(author) + project.add_developer(member) + project.add_developer(external) if project.private? + end + + context "For #{params[:project_type]} project and #{params[:user_type]} users" do + it 'should agree with the read_project_snippet policy' do + expect(can?(user, :read_project_snippet, snippet)).to eq(outcome) + end + + it 'should return proper outcome' do + results = described_class.new(user, project: project).execute + + expect(results.include?(snippet)).to eq(outcome) + end + end + + context "Without a given project and #{params[:user_type]} users" do + it 'should return proper outcome' do + results = described_class.new(user).execute + expect(results.include?(snippet)).to eq(outcome) + end + + it 'returns no snippets when the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + snippets = described_class.new(user).execute + + expect(snippets).to be_empty + end + end + end + end + + context 'For personal snippets' do + let!(:users) do + { + unauthenticated: nil, + external: external, + non_member: create(:user), + author: author + } + end + + where(:snippet_visibility, :user_type, :outcome) do + [ + [Snippet::PUBLIC, :unauthenticated, true], + [Snippet::PUBLIC, :external, true], + [Snippet::PUBLIC, :non_member, true], + [Snippet::PUBLIC, :author, true], + + [Snippet::INTERNAL, :unauthenticated, false], + [Snippet::INTERNAL, :external, false], + [Snippet::INTERNAL, :non_member, true], + [Snippet::INTERNAL, :author, true], + + [Snippet::PRIVATE, :unauthenticated, false], + [Snippet::PRIVATE, :external, false], + [Snippet::PRIVATE, :non_member, false], + [Snippet::PRIVATE, :author, true] + ] + end + + with_them do + let!(:user) { users[user_type] } + let!(:snippet) { create(:personal_snippet, visibility_level: snippet_visibility, author: author) } + + context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do + it 'should agree with read_personal_snippet policy' do + expect(can?(user, :read_personal_snippet, snippet)).to eq(outcome) + end + + it 'should return proper outcome' do + results = described_class.new(user).execute + expect(results.include?(snippet)).to eq(outcome) + end + + it 'should return personal snippets when the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + results = described_class.new(user).execute + + expect(results.include?(snippet)).to eq(outcome) + end + end + end + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index a8fae4a88a3..bdbd39475b9 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -21,9 +21,6 @@ describe 'gitlab:app namespace rake task' do # empty task as env is already loaded Rake::Task.define_task :environment - - # We need this directory to run `gitlab:backup:create` task - FileUtils.mkdir_p('public/uploads') end before do @@ -38,6 +35,7 @@ describe 'gitlab:app namespace rake task' do end def run_rake_task(task_name) + FileUtils.mkdir_p('tmp/tests/public/uploads') Rake::Task[task_name].reenable Rake.application.invoke_task task_name end diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb index 736809eee5b..4b04d9cec39 100644 --- a/spec/tasks/gitlab/storage_rake_spec.rb +++ b/spec/tasks/gitlab/storage_rake_spec.rb @@ -89,9 +89,9 @@ describe 'rake gitlab:storage:*', :sidekiq do describe 'gitlab:storage:migrate_to_hashed' do let(:task) { 'gitlab:storage:migrate_to_hashed' } - context 'with rollback already scheduled' do + context 'with rollback already scheduled', :redis do it 'does nothing' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::RollbackerWorker.perform_async(1, 5) expect(Project).not_to receive(:with_unmigrated_storage) @@ -146,9 +146,9 @@ describe 'rake gitlab:storage:*', :sidekiq do it_behaves_like 'make sure database is writable' - context 'with migration already scheduled' do + context 'with migration already scheduled', :redis do it 'does nothing' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::MigratorWorker.perform_async(1, 5) expect(Project).not_to receive(:with_unmigrated_storage) diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 533e9d87ea6..9ce9a353913 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -375,7 +375,7 @@ describe ObjectStorage do describe '#fog_public' do subject { uploader.fog_public } - it { is_expected.to eq(false) } + it { is_expected.to eq(nil) } end describe '.workhorse_authorize' do diff --git a/spec/workers/ci/build_prepare_worker_spec.rb b/spec/workers/ci/build_prepare_worker_spec.rb new file mode 100644 index 00000000000..9f76696ee66 --- /dev/null +++ b/spec/workers/ci/build_prepare_worker_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Ci::BuildPrepareWorker do + subject { described_class.new.perform(build_id) } + + context 'build exists' do + let(:build) { create(:ci_build) } + let(:build_id) { build.id } + let(:service) { double(execute: true) } + + it 'calls the prepare build service' do + expect(Ci::PrepareBuildService).to receive(:new).with(build).and_return(service) + expect(service).to receive(:execute).once + + subject + end + end + + context 'build does not exist' do + let(:build_id) { -1 } + + it 'does not attempt to prepare the build' do + expect(Ci::PrepareBuildService).not_to receive(:new) + + subject + end + end +end diff --git a/spec/workers/cluster_configure_worker_spec.rb b/spec/workers/cluster_configure_worker_spec.rb index 6918ee3d7d8..83f76809435 100644 --- a/spec/workers/cluster_configure_worker_spec.rb +++ b/spec/workers/cluster_configure_worker_spec.rb @@ -4,6 +4,11 @@ require 'spec_helper' describe ClusterConfigureWorker, '#perform' do let(:worker) { described_class.new } + let(:ci_preparing_state_enabled) { false } + + before do + stub_feature_flags(ci_preparing_state: ci_preparing_state_enabled) + end context 'when group cluster' do let(:cluster) { create(:cluster, :group, :provided_by_gcp) } @@ -66,4 +71,15 @@ describe ClusterConfigureWorker, '#perform' do described_class.new.perform(123) end end + + context 'ci_preparing_state feature is enabled' do + let(:cluster) { create(:cluster) } + let(:ci_preparing_state_enabled) { true } + + it 'does not configure the cluster' do + expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster) + + described_class.new.perform(cluster.id) + end + end end diff --git a/spec/workers/cluster_project_configure_worker_spec.rb b/spec/workers/cluster_project_configure_worker_spec.rb new file mode 100644 index 00000000000..afdea55adf4 --- /dev/null +++ b/spec/workers/cluster_project_configure_worker_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ClusterProjectConfigureWorker, '#perform' do + let(:worker) { described_class.new } + + context 'ci_preparing_state feature is enabled' do + let(:cluster) { create(:cluster) } + + before do + stub_feature_flags(ci_preparing_state: true) + end + + it 'does not configure the cluster' do + expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_project) + + described_class.new.perform(cluster.id) + end + end +end diff --git a/spec/workers/migrate_external_diffs_worker_spec.rb b/spec/workers/migrate_external_diffs_worker_spec.rb new file mode 100644 index 00000000000..88d48cad14b --- /dev/null +++ b/spec/workers/migrate_external_diffs_worker_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe MigrateExternalDiffsWorker do + let(:worker) { described_class.new } + let(:diff) { create(:merge_request).merge_request_diff } + + describe '#perform' do + it 'migrates the listed diff' do + expect_next_instance_of(MergeRequests::MigrateExternalDiffsService) do |instance| + expect(instance.diff).to eq(diff) + expect(instance).to receive(:execute) + end + + worker.perform(diff.id) + end + + it 'does nothing if the diff is missing' do + diff.destroy + + worker.perform(diff.id) + end + end +end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index caae46a3175..9cddad71a51 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -33,8 +33,8 @@ describe PostReceive do describe "#process_project_changes" do context 'empty changes' do it "does not call any PushService but runs after project hooks" do - expect(GitPushService).not_to receive(:new) - expect(GitTagPushService).not_to receive(:new) + expect(Git::BranchPushService).not_to receive(:new) + expect(Git::TagPushService).not_to receive(:new) expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) } described_class.new.perform(gl_repository, key_id, "") @@ -45,8 +45,8 @@ describe PostReceive do let!(:key_id) { "" } it 'returns false' do - expect(GitPushService).not_to receive(:new) - expect(GitTagPushService).not_to receive(:new) + expect(Git::BranchPushService).not_to receive(:new) + expect(Git::TagPushService).not_to receive(:new) expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false end @@ -60,9 +60,9 @@ describe PostReceive do context "branches" do let(:changes) { "123456 789012 refs/heads/tést" } - it "calls GitPushService" do - expect_any_instance_of(GitPushService).to receive(:execute).and_return(true) - expect_any_instance_of(GitTagPushService).not_to receive(:execute) + it "calls Git::BranchPushService" do + expect_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true) + expect_any_instance_of(Git::TagPushService).not_to receive(:execute) described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -70,9 +70,9 @@ describe PostReceive do context "tags" do let(:changes) { "123456 789012 refs/tags/tag" } - it "calls GitTagPushService" do - expect_any_instance_of(GitPushService).not_to receive(:execute) - expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true) + it "calls Git::TagPushService" do + expect_any_instance_of(Git::BranchPushService).not_to receive(:execute) + expect_any_instance_of(Git::TagPushService).to receive(:execute).and_return(true) described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -81,8 +81,8 @@ describe PostReceive do let(:changes) { "123456 789012 refs/merge-requests/123" } it "does not call any of the services" do - expect_any_instance_of(GitPushService).not_to receive(:execute) - expect_any_instance_of(GitTagPushService).not_to receive(:execute) + expect_any_instance_of(Git::BranchPushService).not_to receive(:execute) + expect_any_instance_of(Git::TagPushService).not_to receive(:execute) described_class.new.perform(gl_repository, key_id, base64_changes) end end @@ -125,7 +125,7 @@ describe PostReceive do allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) # silence hooks so we can isolate allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) - allow_any_instance_of(GitPushService).to receive(:execute).and_return(true) + allow_any_instance_of(Git::BranchPushService).to receive(:execute).and_return(true) end it 'calls SystemHooksService' do diff --git a/spec/workers/schedule_migrate_external_diffs_worker_spec.rb b/spec/workers/schedule_migrate_external_diffs_worker_spec.rb new file mode 100644 index 00000000000..9d6fecc9f4e --- /dev/null +++ b/spec/workers/schedule_migrate_external_diffs_worker_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ScheduleMigrateExternalDiffsWorker do + include ExclusiveLeaseHelpers + + let(:worker) { described_class.new } + + describe '#perform' do + it 'triggers a scan for diffs to migrate' do + expect(MergeRequests::MigrateExternalDiffsService).to receive(:enqueue!) + + worker.perform + end + + it 'will not run if the lease is already taken' do + stub_exclusive_lease_taken('schedule_migrate_external_diffs_worker', timeout: 2.hours) + + expect(MergeRequests::MigrateExternalDiffsService).not_to receive(:enqueue!) + + worker.perform + end + end +end |