diff options
Diffstat (limited to 'spec')
221 files changed, 5384 insertions, 1456 deletions
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb index c29b2fe8946..ddf38967dd7 100644 --- a/spec/controllers/admin/groups_controller_spec.rb +++ b/spec/controllers/admin/groups_controller_spec.rb @@ -36,6 +36,15 @@ describe Admin::GroupsController do expect(group.users).to include group_user end + it 'can add unlimited members' do + put :members_update, id: group, + user_ids: 1.upto(1000).to_a.join(','), + access_level: Gitlab::Access::GUEST + + expect(response).to set_flash.to 'Users were successfully added.' + expect(response).to redirect_to(admin_group_path(group)) + end + it 'adds no user to members' do put :members_update, id: group, user_ids: '', diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 2c9d1ffc9c2..4c3a5ec49ef 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -170,22 +170,32 @@ describe AutocompleteController do end context 'author of issuable included' do - before do - sign_in(user) - end - let(:body) { JSON.parse(response.body) } - it 'includes the author' do - get(:users, author_id: non_member.id) + context 'authenticated' do + before do + sign_in(user) + end + + it 'includes the author' do + get(:users, author_id: non_member.id) + + expect(body.first["username"]).to eq non_member.username + end + + it 'rejects non existent user ids' do + get(:users, author_id: 99999) - expect(body.first["username"]).to eq non_member.username + expect(body.collect { |u| u['id'] }).not_to include(99999) + end end - it 'rejects non existent user ids' do - get(:users, author_id: 99999) + context 'without authenticating' do + it 'returns empty result' do + get(:users, author_id: non_member.id) - expect(body.collect { |u| u['id'] }).not_to include(99999) + expect(body).to be_empty + end end end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb new file mode 100644 index 00000000000..424f39fd3b8 --- /dev/null +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Dashboard::MilestonesController do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:project_milestone) { create(:milestone, project: project) } + let(:milestone) do + DashboardMilestone.build( + [project], + project_milestone.title + ) + end + let(:issue) { create(:issue, project: project, milestone: project_milestone) } + let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: project_milestone) } + let(:milestone_path) { dashboard_milestone_path(milestone.safe_title, title: milestone.title) } + + before do + sign_in(user) + project.team << [user, :master] + end + + it_behaves_like 'milestone tabs' + + describe "#show" do + render_views + + def view_milestone + get :show, id: milestone.safe_title, title: milestone.title + end + + it 'shows milestone page' do + view_milestone + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 60db0192dfd..ed4ad7b600e 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -124,6 +124,13 @@ describe Groups::GroupMembersController do expect(response).to redirect_to(dashboard_groups_path) expect(group.users).not_to include user end + + it 'supports json request' do + delete :leave, group_id: group, format: :json + + expect(response).to have_http_status(200) + expect(json_response['notice']).to eq "You left the \"#{group.name}\" group." + end end context 'and is an owner' do diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index b8b6e0c3a88..e7c19b47a6a 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -54,43 +54,4 @@ describe HealthController do end end end - - describe '#metrics' do - context 'authorization token provided' do - before do - request.headers['TOKEN'] = token - end - - it 'returns DB ping metrics' do - get :metrics - expect(response.body).to match(/^db_ping_timeout 0$/) - expect(response.body).to match(/^db_ping_success 1$/) - expect(response.body).to match(/^db_ping_latency [0-9\.]+$/) - end - - it 'returns Redis ping metrics' do - get :metrics - expect(response.body).to match(/^redis_ping_timeout 0$/) - expect(response.body).to match(/^redis_ping_success 1$/) - expect(response.body).to match(/^redis_ping_latency [0-9\.]+$/) - end - - it 'returns file system check metrics' do - get :metrics - expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) - expect(response.body).to match(/^filesystem_write_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/) - expect(response.body).to match(/^filesystem_read_latency{shard="default"} [0-9\.]+$/) - expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/) - end - end - - context 'without authorization token' do - it 'returns proper response' do - get :metrics - expect(response.status).to eq(404) - end - end - end end diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb new file mode 100644 index 00000000000..044c9f179ed --- /dev/null +++ b/spec/controllers/metrics_controller_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe MetricsController do + include StubENV + + let(:token) { current_application_settings.health_check_access_token } + let(:json_response) { JSON.parse(response.body) } + let(:metrics_multiproc_dir) { Dir.mktmpdir } + + before do + stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') + stub_env('prometheus_multiproc_dir', metrics_multiproc_dir) + allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(true) + end + + describe '#index' do + context 'authorization token provided' do + before do + request.headers['TOKEN'] = token + end + + it 'returns DB ping metrics' do + get :index + + expect(response.body).to match(/^db_ping_timeout 0$/) + expect(response.body).to match(/^db_ping_success 1$/) + expect(response.body).to match(/^db_ping_latency [0-9\.]+$/) + end + + it 'returns Redis ping metrics' do + get :index + + expect(response.body).to match(/^redis_ping_timeout 0$/) + expect(response.body).to match(/^redis_ping_success 1$/) + expect(response.body).to match(/^redis_ping_latency [0-9\.]+$/) + end + + it 'returns file system check metrics' do + get :index + + expect(response.body).to match(/^filesystem_access_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_accessible{shard="default"} 1$/) + expect(response.body).to match(/^filesystem_write_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_writable{shard="default"} 1$/) + expect(response.body).to match(/^filesystem_read_latency{shard="default"} [0-9\.]+$/) + expect(response.body).to match(/^filesystem_readable{shard="default"} 1$/) + end + + context 'prometheus metrics are disabled' do + before do + allow(Gitlab::Metrics).to receive(:prometheus_metrics_enabled?).and_return(false) + end + + it 'returns proper response' do + get :index + + expect(response.status).to eq(404) + end + end + end + + context 'without authorization token' do + it 'returns proper response' do + get :index + + expect(response.status).to eq(404) + end + end + end +end diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb new file mode 100644 index 00000000000..9d60dab12d1 --- /dev/null +++ b/spec/controllers/profiles_controller_spec.rb @@ -0,0 +1,31 @@ +require('spec_helper') + +describe ProfilesController do + describe "PUT update" do + it "allows an email update from a user without an external email address" do + user = create(:user) + sign_in(user) + + put :update, + user: { email: "john@gmail.com", name: "John" } + + user.reload + + expect(response.status).to eq(302) + expect(user.unconfirmed_email).to eq('john@gmail.com') + end + + it "ignores an email update from a user with an external email address" do + ldap_user = create(:omniauth_user, external_email: true) + sign_in(ldap_user) + + put :update, + user: { email: "john@gmail.com", name: "John" } + + ldap_user.reload + + expect(response.status).to eq(302) + expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com') + end + end +end diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 432f3c53c90..0f2664262e8 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do parsed_response = JSON.parse(response.body) expect(response).to match_response_schema('lists') - expect(parsed_response.length).to eq 2 + expect(parsed_response.length).to eq 3 end context 'with unauthorized user' do diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index f285e5333d6..f9e21f9d8f6 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -367,19 +367,5 @@ describe Projects::BranchesController do expect(parsed_response.first).to eq 'master' end end - - context 'show_all = true' do - it 'returns all the branches name' do - get :index, - namespace_id: project.namespace, - project_id: project, - format: :json, - show_all: true - - parsed_response = JSON.parse(response.body) - - expect(parsed_response.length).to eq(project.repository.branches.count) - end - end end end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index a38ae2eb990..b65e9e0dfc0 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -260,6 +260,7 @@ describe Projects::IssuesController do before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) } it 'rejects an issue recognized as a spam' do + expect(Gitlab::Recaptcha).to receive(:load_configurations!).and_return(true) expect { update_spam_issue }.not_to change{ issue.reload.title } end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 519232770a2..472e5fc51a0 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -69,18 +69,11 @@ describe Projects::JobsController do Ci::Build::AVAILABLE_STATUSES.each do |status| create_job(status, status) end - - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! end - it "verifies number of queries" do + it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { get_index } - expect(recorded.count).to be_within(5).of(8) + expect(recorded.count).to be_within(5).of(7) end def create_job(name, status) diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 130b0b744b5..bf1776eb320 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -117,7 +117,7 @@ describe Projects::LabelsController do let!(:promoted_label_name) { "Promoted Label" } let!(:label_1) { create(:label, title: promoted_label_name, project: project) } - context 'not group owner' do + context 'not group reporters' do it 'denies access' do post :promote, namespace_id: project.namespace.to_param, project_id: project, id: label_1.to_param @@ -125,9 +125,9 @@ describe Projects::LabelsController do end end - context 'group owner' do + context 'group reporter' do before do - GroupMember.add_users(group, [user], :owner) + group.add_reporter(user) end it 'gives access' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index a25db7a65fb..6e1c91738db 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -119,14 +119,14 @@ describe Projects::MergeRequestsController do end end - context 'number of queries' do + context 'number of queries', :request_store do it 'verifies number of queries' do # pre-create objects merge_request recorded = ActiveRecord::QueryRecorder.new { go(format: :json) } - expect(recorded.count).to be_within(5).of(59) + expect(recorded.count).to be_within(5).of(30) expect(recorded.cached_count).to eq(0) end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index c880da1e36a..954f89e3854 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -49,21 +49,14 @@ describe Projects::PipelinesController do expect(json_response['details']).to have_key 'stages' end - context 'when the pipeline has multiple stages and groups' do + context 'when the pipeline has multiple stages and groups', :request_store do before do - RequestStore.begin! - create_build('build', 0, 'build') create_build('test', 1, 'rspec 0') create_build('deploy', 2, 'production') create_build('post deploy', 3, 'pages 0') end - after do - RequestStore.end! - RequestStore.clear! - end - let(:project) { create(:project) } let(:pipeline) do create(:ci_empty_pipeline, project: project, user: user, sha: project.commit.id) diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index a4b4392d7cc..2294d5df581 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -36,7 +36,7 @@ describe Projects::ProjectMembersController do before { project.team << [user, :master] } it 'adds user to members' do - expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(true) + expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(status: :success) post :create, namespace_id: project.namespace, project_id: project, @@ -48,14 +48,14 @@ describe Projects::ProjectMembersController do end it 'adds no user to members' do - expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(false) + expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(status: :failure, message: 'Message') post :create, namespace_id: project.namespace, project_id: project, user_ids: '', access_level: Gitlab::Access::GUEST - expect(response).to set_flash.to 'No users specified.' + expect(response).to set_flash.to 'Message' expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project)) end end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 24a59caff4e..8c23c46798e 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -78,8 +78,18 @@ describe Projects::SnippetsController do post :create, { namespace_id: project.namespace.to_param, project_id: project, - project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + project_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params) }.merge(additional_params) + + Snippet.last + end + + it 'creates the snippet correctly' do + snippet = create_snippet(project, visibility_level: Snippet::PRIVATE) + + expect(snippet.title).to eq('Title') + expect(snippet.content).to eq('Content') + expect(snippet.description).to eq('Description') end context 'when the snippet is spam' do diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 930415a4778..9073c39f562 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -171,12 +171,50 @@ describe SnippetsController do sign_in(user) post :create, { - personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + personal_snippet: { title: 'Title', content: 'Content', description: 'Description' }.merge(snippet_params) }.merge(additional_params) Snippet.last end + it 'creates the snippet correctly' do + snippet = create_snippet(visibility_level: Snippet::PRIVATE) + + expect(snippet.title).to eq('Title') + expect(snippet.content).to eq('Content') + expect(snippet.description).to eq('Description') + end + + context 'when the snippet description contains a file' do + let(:picture_file) { '/temp/secret56/picture.jpg' } + let(:text_file) { '/temp/secret78/text.txt' } + let(:description) do + "Description with picture: ![picture](/uploads#{picture_file}) and "\ + "text: [text.txt](/uploads#{text_file})" + end + + before do + allow(FileUtils).to receive(:mkdir_p) + allow(FileUtils).to receive(:move) + end + + subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) } + + it 'creates the snippet' do + expect { subject }.to change { Snippet.count }.by(1) + end + + it 'stores the snippet description correctly' do + snippet = subject + + expected_description = "Description with picture: "\ + "![picture](/uploads/personal_snippet/#{snippet.id}/secret56/picture.jpg) and "\ + "text: [text.txt](/uploads/personal_snippet/#{snippet.id}/secret78/text.txt)" + + expect(snippet.description).to eq(expected_description) + end + end + context 'when the snippet is spam' do before do allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index 8000c9dec61..01a0659479b 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -92,6 +92,40 @@ describe UploadsController do end end end + + context 'temporal with valid image' do + subject do + post :create, model: 'personal_snippet', file: jpg, format: :json + end + + it 'returns a content with original filename, new link, and correct type.' do + subject + + expect(response.body).to match '\"alt\":\"rails_sample\"' + expect(response.body).to match "\"url\":\"/uploads/temp" + end + + it 'does not create an Upload record' do + expect { subject }.not_to change { Upload.count } + end + end + + context 'temporal with valid non-image file' do + subject do + post :create, model: 'personal_snippet', file: txt, format: :json + end + + it 'returns a content with original filename, new link, and correct type.' do + subject + + expect(response.body).to match '\"alt\":\"doc_sample.txt\"' + expect(response.body).to match "\"url\":\"/uploads/temp" + end + + it 'does not create an Upload record' do + expect { subject }.not_to change { Upload.count } + end + end end end diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb deleted file mode 100644 index 3cbb173c4cc..00000000000 --- a/spec/db/production/settings.rb +++ /dev/null @@ -1,16 +0,0 @@ -require 'spec_helper' - -describe 'seed production settings', lib: true do - include StubENV - - context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do - before do - stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') - end - - it 'writes the token to the database' do - load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb')) - expect(ApplicationSetting.current.runners_registration_token).to eq('013456789') - end - end -end diff --git a/spec/db/production/settings_spec.rb b/spec/db/production/settings_spec.rb new file mode 100644 index 00000000000..a9d015e0666 --- /dev/null +++ b/spec/db/production/settings_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' +require 'rainbow/ext/string' + +describe 'seed production settings', lib: true do + include StubENV + let(:settings_file) { Rails.root.join('db/fixtures/production/010_settings.rb') } + let(:settings) { Gitlab::CurrentSettings.current_application_settings } + + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do + before do + stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') + end + + it 'writes the token to the database' do + load(settings_file) + + expect(settings.runners_registration_token).to eq('013456789') + end + end + + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is set in the environment' do + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is true' do + before do + stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', 'true') + end + + it 'prometheus_metrics_enabled is set to true ' do + load(settings_file) + + expect(settings.prometheus_metrics_enabled).to eq(true) + end + end + + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is false' do + before do + stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', 'false') + end + + it 'prometheus_metrics_enabled is set to false' do + load(settings_file) + + expect(settings.prometheus_metrics_enabled).to eq(false) + end + end + + context 'GITLAB_PROMETHEUS_METRICS_ENABLED is false' do + before do + stub_env('GITLAB_PROMETHEUS_METRICS_ENABLED', '') + end + + it 'prometheus_metrics_enabled is set to false' do + load(settings_file) + + expect(settings.prometheus_metrics_enabled).to eq(false) + end + end + end +end diff --git a/spec/factories/ci/stages.rb b/spec/factories/ci/stages.rb index d37eabb3e8c..d3c8bf9d54f 100644 --- a/spec/factories/ci/stages.rb +++ b/spec/factories/ci/stages.rb @@ -1,5 +1,5 @@ FactoryGirl.define do - factory :ci_stage, class: Ci::Stage do + factory :ci_stage, class: Ci::LegacyStage do skip_create transient do @@ -10,7 +10,9 @@ FactoryGirl.define do end initialize_with do - Ci::Stage.new(pipeline, name: name, status: status, warnings: warnings) + Ci::LegacyStage.new(pipeline, name: name, + status: status, + warnings: warnings) end end end diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index f6a78811cbe..48142d3c49b 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -6,6 +6,12 @@ FactoryGirl.define do sequence(:position) end + factory :backlog_list, parent: :list do + list_type :backlog + label nil + position nil + end + factory :closed_list, parent: :list do list_type :closed label nil diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb index 18cb0f5de26..388f662e6e5 100644 --- a/spec/factories/snippets.rb +++ b/spec/factories/snippets.rb @@ -3,6 +3,7 @@ FactoryGirl.define do author title { generate(:title) } content { generate(:title) } + description { generate(:title) } file_name { generate(:filename) } trait :public do diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb new file mode 100644 index 00000000000..1383420fb44 --- /dev/null +++ b/spec/factories/uploads.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :upload do + model { build(:project) } + path { "uploads/system/project/avatar/avatar.jpg" } + size 100.kilobytes + uploader "AvatarUploader" + end +end diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb index 96d715ef383..595366ce352 100644 --- a/spec/features/admin/admin_appearance_spec.rb +++ b/spec/features/admin/admin_appearance_spec.rb @@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do end def logo_selector - '//img[@src^="/uploads/appearance/logo"]' + '//img[@src^="/uploads/system/appearance/logo"]' end def header_logo_selector - '//img[@src^="/uploads/appearance/header_logo"]' + '//img[@src^="/uploads/system/appearance/header_logo"]' end def logo_fixture diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb index c0b6995a84a..5f5fa4e932a 100644 --- a/spec/features/admin/admin_deploy_keys_spec.rb +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -11,40 +11,67 @@ RSpec.describe 'admin deploy keys', type: :feature do it 'show all public deploy keys' do visit admin_deploy_keys_path - expect(page).to have_content(deploy_key.title) - expect(page).to have_content(another_deploy_key.title) + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content(deploy_key.title) + expect(page).to have_content(another_deploy_key.title) + end end - describe 'create new deploy key' do + describe 'create a new deploy key' do + let(:new_ssh_key) { attributes_for(:key)[:key] } + before do visit admin_deploy_keys_path click_link 'New deploy key' end - it 'creates new deploy key' do - fill_deploy_key + it 'creates a new deploy key' do + fill_in 'deploy_key_title', with: 'laptop' + fill_in 'deploy_key_key', with: new_ssh_key + check 'deploy_key_can_push' click_button 'Create' - expect_renders_new_key - end + expect(current_path).to eq admin_deploy_keys_path - it 'creates new deploy key with write access' do - fill_deploy_key - check "deploy_key_can_push" - click_button "Create" + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content('laptop') + expect(page).to have_content('Yes') + end + end + end - expect_renders_new_key - expect(page).to have_content('Yes') + describe 'update an existing deploy key' do + before do + visit admin_deploy_keys_path + find('tr', text: deploy_key.title).click_link('Edit') end - def expect_renders_new_key + it 'updates an existing deploy key' do + fill_in 'deploy_key_title', with: 'new-title' + check 'deploy_key_can_push' + click_button 'Save changes' + expect(current_path).to eq admin_deploy_keys_path - expect(page).to have_content('laptop') + + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).to have_content('new-title') + expect(page).to have_content('Yes') + end end + end - def fill_deploy_key - fill_in 'deploy_key_title', with: 'laptop' - fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop' + describe 'remove an existing deploy key' do + before do + visit admin_deploy_keys_path + end + + it 'removes an existing deploy key' do + find('tr', text: deploy_key.title).click_link('Remove') + + expect(current_path).to eq admin_deploy_keys_path + page.within(find('.deploy-keys-list', match: :first)) do + expect(page).not_to have_content(deploy_key.title) + end end end end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index d5f595894d6..cf9d7bca255 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -24,7 +24,9 @@ feature 'Admin Groups', feature: true do it 'creates new group' do visit admin_groups_path - click_link "New group" + page.within '#content-body' do + click_link "New group" + end path_component = 'gitlab' group_name = 'GitLab group name' group_description = 'Description of group for GitLab' diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 32ac265814f..2b8edac4f10 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -231,7 +231,7 @@ describe 'Issue Boards add issue modal', :feature, :js do click_button 'Add 1 issue' end - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.card') end end @@ -247,7 +247,7 @@ describe 'Issue Boards add issue modal', :feature, :js do click_button 'Add 1 issue' end - page.within(find('.board:nth-child(2)')) do + page.within(find('.board:nth-child(3)')) do expect(page).to have_selector('.card') end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index ba27db23ced..c80453b8227 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -19,7 +19,7 @@ describe 'Issue Boards', feature: true, js: true do before do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'shows blank state' do @@ -36,18 +36,18 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.board-blank-state')) do click_button("Nevermind, I'll use my own") end - expect(page).to have_selector('.board', count: 1) + expect(page).to have_selector('.board', count: 2) end it 'creates default lists' do - lists = ['To Do', 'Doing', 'Closed'] + lists = ['Backlog', 'To Do', 'Doing', 'Closed'] page.within(find('.board-blank-state')) do click_button('Add default lists') end wait_for_requests - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 4) page.all('.board').each_with_index do |list, i| expect(list.find('.board-title')).to have_content(lists[i]) @@ -85,29 +85,25 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(page).to have_selector('.board', count: 3) - expect(find('.board:nth-child(1)')).to have_selector('.card') + expect(page).to have_selector('.board', count: 4) expect(find('.board:nth-child(2)')).to have_selector('.card') expect(find('.board:nth-child(3)')).to have_selector('.card') - end - - it 'shows lists' do - expect(page).to have_selector('.board', count: 3) + expect(find('.board:nth-child(4)')).to have_selector('.card') end it 'shows description tooltip on list title' do - page.within('.board:nth-child(1)') do + page.within('.board:nth-child(2)') do expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') end end it 'shows issues in lists' do - wait_for_board_cards(1, 8) - wait_for_board_cards(2, 2) + wait_for_board_cards(2, 8) + wait_for_board_cards(3, 2) end it 'shows confidential issues with icon' do - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.confidential-icon', count: 1) end end @@ -118,9 +114,9 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) end it 'search list' do @@ -129,32 +125,32 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) end it 'allows user to delete board' do - page.within(find('.board:nth-child(1)')) do + page.within(find('.board:nth-child(2)')) do find('.board-delete').click end wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'removes checkmark in new list dropdown after deleting' do click_button 'Add list' wait_for_requests - page.within(find('.board:nth-child(1)')) do + page.within(find('.board:nth-child(2)')) do find('.board-delete').click end wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'infinite scrolls list' do @@ -165,18 +161,18 @@ describe 'Issue Boards', feature: true, js: true do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('58') expect(page).to have_selector('.card', count: 20) expect(page).to have_content('Showing 20 of 58 issues') - evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") wait_for_requests expect(page).to have_selector('.card', count: 40) expect(page).to have_content('Showing 40 of 58 issues') - evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") wait_for_requests expect(page).to have_selector('.card', count: 58) @@ -186,83 +182,83 @@ describe 'Issue Boards', feature: true, js: true do context 'closed' do it 'shows list of closed issues' do - wait_for_board_cards(3, 1) + wait_for_board_cards(4, 1) wait_for_requests end it 'moves issue to closed' do - drag(list_from_index: 0, list_to_index: 2) + drag(list_from_index: 1, list_to_index: 3) - wait_for_board_cards(1, 7) - wait_for_board_cards(2, 2) + wait_for_board_cards(2, 7) wait_for_board_cards(3, 2) + wait_for_board_cards(4, 2) - expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2) - expect(find('.board:nth-child(3)')).to have_content(issue9.title) - expect(find('.board:nth-child(3)')).not_to have_content(planning.title) + expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) + expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) + expect(find('.board:nth-child(4)')).to have_content(issue9.title) + expect(find('.board:nth-child(4)')).not_to have_content(planning.title) end it 'removes all of the same issue to closed' do - drag(list_from_index: 0, list_to_index: 2) + drag(list_from_index: 1, list_to_index: 3) - wait_for_board_cards(1, 7) - wait_for_board_cards(2, 2) + wait_for_board_cards(2, 7) wait_for_board_cards(3, 2) + wait_for_board_cards(4, 2) - expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(3)')).to have_content(issue9.title) - expect(find('.board:nth-child(3)')).not_to have_content(planning.title) + expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) + expect(find('.board:nth-child(4)')).to have_content(issue9.title) + expect(find('.board:nth-child(4)')).not_to have_content(planning.title) end end context 'lists' do it 'changes position of list' do - drag(list_from_index: 1, list_to_index: 0, selector: '.board-header') + drag(list_from_index: 2, list_to_index: 1, selector: '.board-header') - wait_for_board_cards(1, 2) - wait_for_board_cards(2, 8) - wait_for_board_cards(3, 1) + wait_for_board_cards(2, 2) + wait_for_board_cards(3, 8) + wait_for_board_cards(4, 1) - expect(find('.board:nth-child(1)')).to have_content(development.title) - expect(find('.board:nth-child(1)')).to have_content(planning.title) + expect(find('.board:nth-child(2)')).to have_content(development.title) + expect(find('.board:nth-child(2)')).to have_content(planning.title) end it 'issue moves between lists' do - drag(list_from_index: 0, from_index: 1, list_to_index: 1) + drag(list_from_index: 1, from_index: 1, list_to_index: 2) - wait_for_board_cards(1, 7) - wait_for_board_cards(2, 2) - wait_for_board_cards(3, 1) + wait_for_board_cards(2, 7) + wait_for_board_cards(3, 2) + wait_for_board_cards(4, 1) - expect(find('.board:nth-child(2)')).to have_content(issue6.title) - expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title) + expect(find('.board:nth-child(3)')).to have_content(issue6.title) + expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title) end it 'issue moves between lists' do - drag(list_from_index: 1, list_to_index: 0) + drag(list_from_index: 2, list_to_index: 1) - wait_for_board_cards(1, 9) - wait_for_board_cards(2, 1) + wait_for_board_cards(2, 9) wait_for_board_cards(3, 1) + wait_for_board_cards(4, 1) - expect(find('.board:nth-child(1)')).to have_content(issue7.title) - expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title) + expect(find('.board:nth-child(2)')).to have_content(issue7.title) + expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title) end it 'issue moves from closed' do - drag(list_from_index: 2, list_to_index: 1) + drag(list_from_index: 3, list_to_index: 2) - expect(find('.board:nth-child(2)')).to have_content(issue8.title) + expect(find('.board:nth-child(3)')).to have_content(issue8.title) - wait_for_board_cards(1, 8) - wait_for_board_cards(2, 3) - wait_for_board_cards(3, 0) + wait_for_board_cards(2, 8) + wait_for_board_cards(3, 3) + wait_for_board_cards(4, 0) end context 'issue card' do it 'shows assignee' do - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.avatar', count: 1) end end @@ -290,7 +286,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 5) end it 'creates new list for Backlog label' do @@ -303,7 +299,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 5) end it 'creates new list for Closed label' do @@ -316,7 +312,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 5) end it 'keeps dropdown open after adding new list' do @@ -348,7 +344,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests wait_for_requests - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 5) end end end @@ -360,8 +356,8 @@ describe 'Issue Boards', feature: true, js: true do submit_filter wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'filters by assignee' do @@ -371,8 +367,8 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'filters by milestone' do @@ -381,9 +377,9 @@ describe 'Issue Boards', feature: true, js: true do submit_filter wait_for_requests - wait_for_board_cards(1, 1) - wait_for_board_cards(2, 0) + wait_for_board_cards(2, 1) wait_for_board_cards(3, 0) + wait_for_board_cards(4, 0) end it 'filters by label' do @@ -392,8 +388,8 @@ describe 'Issue Boards', feature: true, js: true do submit_filter wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'filters by label with space after reload' do @@ -403,17 +399,17 @@ describe 'Issue Boards', feature: true, js: true do # Test after reload page.evaluate_script 'window.location.reload()' - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) wait_for_requests - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('1') expect(page).to have_selector('.card', count: 1) end - page.within(find('.board:nth-child(2)')) do + page.within(find('.board:nth-child(3)')) do expect(page.find('.board-header')).to have_content('0') expect(page).to have_selector('.card', count: 0) end @@ -424,12 +420,12 @@ describe 'Issue Boards', feature: true, js: true do click_filter_link(testing.title) submit_filter - wait_for_board_cards(1, 1) + wait_for_board_cards(2, 1) find('.clear-search').click submit_filter - wait_for_board_cards(1, 8) + wait_for_board_cards(2, 8) end it 'infinite scrolls list with label filter' do @@ -443,17 +439,17 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page.find('.board-header')).to have_content('51') expect(page).to have_selector('.card', count: 20) expect(page).to have_content('Showing 20 of 51 issues') - evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") expect(page).to have_selector('.card', count: 40) expect(page).to have_content('Showing 40 of 51 issues') - evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") expect(page).to have_selector('.card', count: 51) expect(page).to have_content('Showing all issues') @@ -471,12 +467,12 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'filters by clicking label button on issue' do - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.card', count: 8) expect(find('.card', match: :first)).to have_content(bug.title) click_button(bug.title) @@ -489,12 +485,12 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..3)) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end it 'removes label filter by clicking label button on issue' do - page.within(find('.board', match: :first)) do + page.within(find('.board:nth-child(2)')) do page.within(find('.card', match: :first)) do click_button(bug.title) end diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 6c40cb2c9eb..1c289993e28 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -25,11 +25,11 @@ describe 'Issue Boards', :feature, :js do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'has un-ordered issue as last issue' do - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do expect(all('.card').last).to have_content(issue4.title) end end @@ -39,7 +39,7 @@ describe 'Issue Boards', :feature, :js do wait_for_requests - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do expect(first('.card')).to have_content(issue4.title) end end @@ -50,7 +50,7 @@ describe 'Issue Boards', :feature, :js do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'moves from middle to top' do @@ -113,50 +113,50 @@ describe 'Issue Boards', :feature, :js do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 4) end it 'moves to top of another list' do - drag(list_from_index: 0, list_to_index: 1) + drag(list_from_index: 1, list_to_index: 2) wait_for_requests - expect(first('.board')).to have_selector('.card', count: 2) - expect(all('.board')[1]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) + expect(all('.board')[2]).to have_selector('.card', count: 4) - page.within(all('.board')[1]) do + page.within(all('.board')[2]) do expect(first('.card')).to have_content(issue3.title) end end it 'moves to bottom of another list' do - drag(list_from_index: 0, list_to_index: 1, to_index: 2) + drag(list_from_index: 1, list_to_index: 2, to_index: 2) wait_for_requests - expect(first('.board')).to have_selector('.card', count: 2) - expect(all('.board')[1]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) + expect(all('.board')[2]).to have_selector('.card', count: 4) - page.within(all('.board')[1]) do + page.within(all('.board')[2]) do expect(all('.card').last).to have_content(issue3.title) end end it 'moves to index of another list' do - drag(list_from_index: 0, list_to_index: 1, to_index: 1) + drag(list_from_index: 1, list_to_index: 2, to_index: 1) wait_for_requests - expect(first('.board')).to have_selector('.card', count: 2) - expect(all('.board')[1]).to have_selector('.card', count: 4) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 2) + expect(all('.board')[2]).to have_selector('.card', count: 4) - page.within(all('.board')[1]) do + page.within(all('.board')[2]) do expect(all('.card')[1]).to have_content(issue3.title) end end end - def drag(selector: '.board-list', list_from_index: 0, from_index: 0, to_index: 0, list_to_index: 0) + def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1) drag_to(selector: selector, scrollable: '#board-app', list_from_index: list_from_index, diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 0e98f994018..056224dc436 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -15,15 +15,15 @@ describe 'Issue Boards new issue', feature: true, js: true do visit namespace_project_board_path(project.namespace, project, board) wait_for_requests - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 3) end it 'displays new issue button' do - expect(page).to have_selector('.board-issue-count-holder .btn', count: 1) + expect(first('.board')).to have_selector('.board-issue-count-holder .btn', count: 1) end it 'does not display new issue button in closed list' do - page.within('.board:nth-child(2)') do + page.within('.board:nth-child(3)') do expect(page).not_to have_selector('.board-issue-count-holder .btn') end end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 34f4d765117..235e4899707 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -13,7 +13,7 @@ describe 'Issue Boards', feature: true, js: true do let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch], relative_position: 1) } let(:board) { create(:board, project: project) } let!(:list) { create(:list, board: board, label: development, position: 0) } - let(:card) { first('.board').first('.card') } + let(:card) { find('.board:nth-child(2)').first('.card') } before do Timecop.freeze @@ -74,7 +74,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_requests - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do expect(page).to have_selector('.card', count: 1) end end @@ -101,7 +101,7 @@ describe 'Issue Boards', feature: true, js: true do end it 'removes the assignee' do - card_two = first('.board').find('.card:nth-child(2)') + card_two = find('.board:nth-child(2)').find('.card:nth-child(2)') click_card(card_two) page.within('.assignee') do @@ -154,7 +154,7 @@ describe 'Issue Boards', feature: true, js: true do expect(page).to have_content(user.name) end - page.within(first('.board')) do + page.within(find('.board:nth-child(2)')) do find('.card:nth-child(2)').trigger('click') end diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index b0e2953dda2..7eb254f8451 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -6,40 +6,124 @@ describe 'Dashboard Groups page', js: true, feature: true do let!(:nested_group) { create(:group, :nested) } let!(:another_group) { create(:group) } - before do + it 'shows groups user is member of' do group.add_owner(user) nested_group.add_owner(user) login_as(user) - visit dashboard_groups_path - end - it 'shows groups user is member of' do expect(page).to have_content(group.full_name) expect(page).to have_content(nested_group.full_name) expect(page).not_to have_content(another_group.full_name) end - it 'filters groups' do - fill_in 'filter_groups', with: group.name - wait_for_requests + describe 'when filtering groups' do + before do + group.add_owner(user) + nested_group.add_owner(user) - expect(page).to have_content(group.full_name) - expect(page).not_to have_content(nested_group.full_name) - expect(page).not_to have_content(another_group.full_name) + login_as(user) + + visit dashboard_groups_path + end + + it 'filters groups' do + fill_in 'filter_groups', with: group.name + wait_for_requests + + expect(page).to have_content(group.full_name) + expect(page).not_to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + end + + it 'resets search when user cleans the input' do + fill_in 'filter_groups', with: group.name + wait_for_requests + + fill_in 'filter_groups', with: "" + wait_for_requests + + expect(page).to have_content(group.full_name) + expect(page).to have_content(nested_group.full_name) + expect(page).not_to have_content(another_group.full_name) + expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 + end end - it 'resets search when user cleans the input' do - fill_in 'filter_groups', with: group.name - wait_for_requests + describe 'group with subgroups' do + let!(:subgroup) { create(:group, :public, parent: group) } - fill_in 'filter_groups', with: "" - wait_for_requests + before do + group.add_owner(user) + subgroup.add_owner(user) - expect(page).to have_content(group.full_name) - expect(page).to have_content(nested_group.full_name) - expect(page).not_to have_content(another_group.full_name) - expect(page.all('.js-groups-list-holder .content-list li').length).to eq 2 + login_as(user) + + visit dashboard_groups_path + end + + it 'shows subgroups inside of its parent group' do + expect(page).to have_selector('.groups-list-tree-container .group-list-tree', count: 2) + expect(page).to have_selector(".groups-list-tree-container #group-#{group.id} #group-#{subgroup.id}", count: 1) + end + + it 'can toggle parent group' do + # Expanded by default + expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1) + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right") + + # Collapse + find("#group-#{group.id}").trigger('click') + + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down") + expect(page).to have_selector("#group-#{group.id} .fa-caret-right", count: 1) + expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}") + + # Expand + find("#group-#{group.id}").trigger('click') + + expect(page).to have_selector("#group-#{group.id} .fa-caret-down", count: 1) + expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right") + expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}") + end + end + + describe 'when using pagination' do + let(:group2) { create(:group) } + + before do + group.add_owner(user) + group2.add_owner(user) + + allow(Kaminari.config).to receive(:default_per_page).and_return(1) + + login_as(user) + visit dashboard_groups_path + end + + it 'shows pagination' do + expect(page).to have_selector('.gl-pagination') + expect(page).to have_selector('.gl-pagination .page', count: 2) + end + + it 'loads results for next page' do + # Check first page + expect(page).to have_content(group2.full_name) + expect(page).to have_selector("#group-#{group2.id}") + expect(page).not_to have_content(group.full_name) + expect(page).not_to have_selector("#group-#{group.id}") + + # Go to next page + find(".gl-pagination .page:not(.active) a").trigger('click') + + wait_for_requests + + # Check second page + expect(page).to have_content(group.full_name) + expect(page).to have_selector("#group-#{group.id}") + expect(page).not_to have_content(group2.full_name) + expect(page).not_to have_selector("#group-#{group2.id}") + end end end diff --git a/spec/features/dashboard/milestone_tabs_spec.rb b/spec/features/dashboard/milestone_tabs_spec.rb new file mode 100644 index 00000000000..0c7b992c500 --- /dev/null +++ b/spec/features/dashboard/milestone_tabs_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'Dashboard milestone tabs', :js, :feature do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let!(:label) { create(:label, project: project) } + let(:project_milestone) { create(:milestone, project: project) } + let(:milestone) do + DashboardMilestone.build( + [project], + project_milestone.title + ) + end + let!(:merge_request) { create(:labeled_merge_request, source_project: project, target_project: project, milestone: project_milestone, labels: [label]) } + + before do + project.add_master(user) + login_as(user) + + visit dashboard_milestone_path(milestone.safe_title, title: milestone.title) + end + + it 'loads merge requests async' do + click_link 'Merge Requests' + + expect(page).to have_selector('.merge_requests-sortable-list') + end + + it 'loads participants async' do + click_link 'Participants' + + expect(page).to have_selector('#tab-participants .bordered-list') + end + + it 'loads labels async' do + click_link 'Labels' + + expect(page).to have_selector('#tab-labels .bordered-list') + end +end diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb new file mode 100644 index 00000000000..15a6354211b --- /dev/null +++ b/spec/features/explore/new_menu_spec.rb @@ -0,0 +1,172 @@ +require 'spec_helper' + +feature 'Top Plus Menu', feature: true, js: true do + let(:user) { create :user } + let(:guest_user) { create :user} + let(:group) { create(:group) } + let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } + let(:public_project) { create(:project, :public) } + + before do + group.add_owner(user) + group.add_guest(guest_user) + + project.add_guest(guest_user) + end + + context 'used by full user' do + before do + login_as(user) + end + + scenario 'click on New project shows new project page' do + visit root_dashboard_path + + click_topmenuitem("New project") + + expect(page).to have_content('Project path') + expect(page).to have_content('Project name') + end + + scenario 'click on New group shows new group page' do + visit root_dashboard_path + + click_topmenuitem("New group") + + expect(page).to have_content('Group path') + expect(page).to have_content('Group name') + end + + scenario 'click on New snippet shows new snippet page' do + visit root_dashboard_path + + click_topmenuitem("New snippet") + + expect(page).to have_content('New Snippet') + expect(page).to have_content('Title') + end + + scenario 'click on New issue shows new issue page' do + visit namespace_project_path(project.namespace, project) + + click_topmenuitem("New issue") + + expect(page).to have_content('New Issue') + expect(page).to have_content('Title') + end + + scenario 'click on New merge request shows new merge request page' do + visit namespace_project_path(project.namespace, project) + + click_topmenuitem("New merge request") + + expect(page).to have_content('New Merge Request') + expect(page).to have_content('Source branch') + expect(page).to have_content('Target branch') + end + + scenario 'click on New project snippet shows new snippet page' do + visit namespace_project_path(project.namespace, project) + + page.within '.header-content' do + find('.header-new-dropdown-toggle').trigger('click') + expect(page).to have_selector('.header-new.dropdown.open', count: 1) + find('.header-new-project-snippet a').trigger('click') + end + + expect(page).to have_content('New Snippet') + expect(page).to have_content('Title') + end + + scenario 'Click on New subgroup shows new group page' do + visit group_path(group) + + click_topmenuitem("New subgroup") + + expect(page).to have_content('Group path') + expect(page).to have_content('Group name') + end + + scenario 'Click on New project in group shows new project page' do + visit group_path(group) + + page.within '.header-content' do + find('.header-new-dropdown-toggle').trigger('click') + expect(page).to have_selector('.header-new.dropdown.open', count: 1) + find('.header-new-group-project a').trigger('click') + end + + expect(page).to have_content('Project path') + expect(page).to have_content('Project name') + end + end + + context 'used by guest user' do + before do + login_as(guest_user) + end + + scenario 'click on New issue shows new issue page' do + visit namespace_project_path(project.namespace, project) + + click_topmenuitem("New issue") + + expect(page).to have_content('New Issue') + expect(page).to have_content('Title') + end + + scenario 'has no New merge request menu item' do + visit namespace_project_path(project.namespace, project) + + hasnot_topmenuitem("New merge request") + end + + scenario 'has no New project snippet menu item' do + visit namespace_project_path(project.namespace, project) + + expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet') + end + + scenario 'public project has no New Issue Button' do + visit namespace_project_path(public_project.namespace, public_project) + + hasnot_topmenuitem("New issue") + end + + scenario 'public project has no New merge request menu item' do + visit namespace_project_path(public_project.namespace, public_project) + + hasnot_topmenuitem("New merge request") + end + + scenario 'public project has no New project snippet menu item' do + visit namespace_project_path(public_project.namespace, public_project) + + expect(find('.header-new.dropdown')).not_to have_selector('.header-new-project-snippet') + end + + scenario 'has no New subgroup menu item' do + visit group_path(group) + + hasnot_topmenuitem("New subgroup") + end + + scenario 'has no New project for group menu item' do + visit group_path(group) + + expect(find('.header-new.dropdown')).not_to have_selector('.header-new-group-project') + end + end + + def click_topmenuitem(item_name) + page.within '.header-content' do + find('.header-new-dropdown-toggle').trigger('click') + expect(page).to have_selector('.header-new.dropdown.open', count: 1) + click_link item_name + end + end + + def hasnot_topmenuitem(item_name) + expect(find('.header-new.dropdown')).not_to have_content(item_name) + end +end diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb index e0b2404e60a..31014f5cad2 100644 --- a/spec/features/help_pages_spec.rb +++ b/spec/features/help_pages_spec.rb @@ -34,7 +34,7 @@ describe 'Help Pages', feature: true do end end - context 'in a production environment with version check enabled', js: true do + context 'in a production environment with version check enabled', :js do before do allow(Rails.env).to receive(:production?) { true } allow(current_application_settings).to receive(:version_check_enabled) { true } @@ -44,18 +44,12 @@ describe 'Help Pages', feature: true do visit help_path end - it 'should display a version check image' do - expect(find('.js-version-status-badge')).to be_visible + it 'has a version check image' do + expect(find('.js-version-status-badge', visible: false)['src']).to end_with('/version-check-url') end - it 'should have a src url' do - expect(find('.js-version-status-badge')['src']).to match(/\/version-check-url/) - end - - it 'should hide the version check image if the image request fails' do - # We use '--load-images=no' with poltergeist so we must trigger manually - execute_script("$('.js-version-status-badge').trigger('error');") - + it 'hides the version check image if the image request fails' do + # We use '--load-images=yes' with poltergeist so the image fails to load expect(find('.js-version-status-badge', visible: false)).not_to be_visible end end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index 0a6f645b27e..95b4930cd32 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -18,13 +18,13 @@ feature 'Issues > Labels bulk assignment', feature: true do context 'can bulk assign' do before do - visit namespace_project_issues_path(project.namespace, project) + enable_bulk_update end context 'a label' do context 'to all issues' do before do - check 'check_all_issues' + check 'check-all-issues' open_labels_dropdown ['bug'] update_issues end @@ -52,7 +52,7 @@ feature 'Issues > Labels bulk assignment', feature: true do context 'multiple labels' do context 'to all issues' do before do - check 'check_all_issues' + check 'check-all-issues' open_labels_dropdown %w(bug feature) update_issues end @@ -86,9 +86,10 @@ feature 'Issues > Labels bulk assignment', feature: true do before do issue2.labels << bug issue2.labels << feature - visit namespace_project_issues_path(project.namespace, project) - check 'check_all_issues' + enable_bulk_update + check 'check-all-issues' + open_labels_dropdown ['bug'] update_issues end @@ -107,9 +108,8 @@ feature 'Issues > Labels bulk assignment', feature: true do issue2.labels << bug issue2.labels << feature - visit namespace_project_issues_path(project.namespace, project) - - check 'check_all_issues' + enable_bulk_update + check 'check-all-issues' unmark_labels_in_dropdown %w(bug feature) update_issues end @@ -127,8 +127,7 @@ feature 'Issues > Labels bulk assignment', feature: true do issue1.labels << bug issue2.labels << feature - visit namespace_project_issues_path(project.namespace, project) - + enable_bulk_update check_issue issue1 unmark_labels_in_dropdown ['bug'] update_issues @@ -147,8 +146,7 @@ feature 'Issues > Labels bulk assignment', feature: true do issue2.labels << bug issue2.labels << feature - visit namespace_project_issues_path(project.namespace, project) - + enable_bulk_update check_issue issue1 check_issue issue2 unmark_labels_in_dropdown ['bug'] @@ -171,14 +169,15 @@ feature 'Issues > Labels bulk assignment', feature: true do before do issue1.labels << bug issue2.labels << feature - visit namespace_project_issues_path(project.namespace, project) + enable_bulk_update end it 'keeps labels' do expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue2.id}")).to have_content 'feature' - check 'check_all_issues' + check 'check-all-issues' + open_milestone_dropdown(['First Release']) update_issues @@ -192,14 +191,13 @@ feature 'Issues > Labels bulk assignment', feature: true do context 'setting a milestone and adding another label' do before do issue1.labels << bug - - visit namespace_project_issues_path(project.namespace, project) + enable_bulk_update end it 'keeps existing label and new label is present' do expect(find("#issue_#{issue1.id}")).to have_content 'bug' - check 'check_all_issues' + check 'check-all-issues' open_milestone_dropdown ['First Release'] open_labels_dropdown ['feature'] update_issues @@ -218,7 +216,7 @@ feature 'Issues > Labels bulk assignment', feature: true do issue1.labels << feature issue2.labels << feature - visit namespace_project_issues_path(project.namespace, project) + enable_bulk_update end it 'keeps existing label and new label is present' do @@ -226,7 +224,8 @@ feature 'Issues > Labels bulk assignment', feature: true do expect(find("#issue_#{issue1.id}")).to have_content 'bug' expect(find("#issue_#{issue2.id}")).to have_content 'feature' - check 'check_all_issues' + check 'check-all-issues' + open_milestone_dropdown ['First Release'] unmark_labels_in_dropdown ['feature'] update_issues @@ -248,7 +247,7 @@ feature 'Issues > Labels bulk assignment', feature: true do issue1.labels << bug issue2.labels << feature - visit namespace_project_issues_path(project.namespace, project) + enable_bulk_update end it 'keeps labels' do @@ -257,7 +256,7 @@ feature 'Issues > Labels bulk assignment', feature: true do expect(find("#issue_#{issue2.id}")).to have_content 'feature' expect(find("#issue_#{issue2.id}")).to have_content 'First Release' - check 'check_all_issues' + check 'check-all-issues' open_milestone_dropdown(['No Milestone']) update_issues @@ -272,8 +271,7 @@ feature 'Issues > Labels bulk assignment', feature: true do context 'toggling checked issues' do before do issue1.labels << bug - - visit namespace_project_issues_path(project.namespace, project) + enable_bulk_update end it do @@ -298,14 +296,14 @@ feature 'Issues > Labels bulk assignment', feature: true do issue1.labels << feature issue2.labels << bug - visit namespace_project_issues_path(project.namespace, project) + enable_bulk_update end it 'applies label from filtered results' do - check 'check_all_issues' + check 'check-all-issues' - page.within('.issues_bulk_update') do - click_button 'Labels' + page.within('.issues-bulk-update') do + click_button 'Select labels' wait_for_requests expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active') @@ -340,15 +338,16 @@ feature 'Issues > Labels bulk assignment', feature: true do context 'cannot bulk assign labels' do it do - expect(page).not_to have_css '.check_all_issues' + expect(page).not_to have_button 'Edit Issues' + expect(page).not_to have_css '.check-all-issues' expect(page).not_to have_css '.issue-check' end end end def open_milestone_dropdown(items = []) - page.within('.issues_bulk_update') do - click_button 'Milestone' + page.within('.issues-bulk-update') do + click_button 'Select milestone' wait_for_requests items.map do |item| click_link item @@ -357,8 +356,8 @@ feature 'Issues > Labels bulk assignment', feature: true do end def open_labels_dropdown(items = [], unmark = false) - page.within('.issues_bulk_update') do - click_button 'Labels' + page.within('.issues-bulk-update') do + click_button 'Select labels' wait_for_requests items.map do |item| click_link item @@ -391,7 +390,12 @@ feature 'Issues > Labels bulk assignment', feature: true do end def update_issues - click_button 'Update issues' + click_button 'Update all' wait_for_requests end + + def enable_bulk_update + visit namespace_project_issues_path(project.namespace, project) + click_button 'Edit Issues' + end end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index e5e4ba06b5a..863f8f75cd8 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -777,17 +777,17 @@ describe 'Filter issues', js: true, feature: true do end it 'open state' do - find('.issues-state-filters a', text: 'Closed').click + find('.issues-state-filters [data-state="closed"]').click wait_for_requests - find('.issues-state-filters a', text: 'Open').click + find('.issues-state-filters [data-state="opened"]').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 4) end it 'closed state' do - find('.issues-state-filters a', text: 'Closed').click + find('.issues-state-filters [data-state="closed"]').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 1) @@ -795,7 +795,7 @@ describe 'Filter issues', js: true, feature: true do end it 'all state' do - find('.issues-state-filters a', text: 'All').click + find('.issues-state-filters [data-state="all"]').click wait_for_requests expect(page).to have_selector('.issues-list .issue', count: 5) diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index dbbafc9e004..ff32b0c7d11 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -34,7 +34,7 @@ describe 'Visual tokens', js: true, feature: true do describe 'editing author token' do before do input_filtered_search('author:@root assignee:none', submit: false) - first('.tokens-container .filtered-search-token').double_click + first('.tokens-container .filtered-search-token').click end it 'opens author dropdown' do @@ -331,7 +331,7 @@ describe 'Visual tokens', js: true, feature: true do it 'does not tokenize incomplete token' do filtered_search.send_keys('author:') - find('#content-body').click + find('body').click token = page.all('.tokens-container .js-visual-token')[1] expect_filtered_search_input_empty diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 8949dbcb663..96d37e33f3d 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -24,37 +24,17 @@ describe 'New/edit issue', :feature, :js do visit new_namespace_project_issue_path(project.namespace, project) end - describe 'shorten users API pagination limit' do + describe 'shorten users API pagination limit (CE)' do before do + # Using `allow_any_instance_of`/`and_wrap_original`, `original` would + # somehow refer to the very block we defined to _wrap_ that method, instead of + # the original method, resulting in infinite recurison when called. + # This is likely a bug with helper modules included into dynamically generated view classes. + # To work around this, we have to hold on to and call to the original implementation manually. + original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options) allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args| - has_multiple_assignees = *args[1] - - options = { - toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', - title: 'Select assignee', - filter: true, - dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee', - placeholder: 'Search users', - data: { - per_page: 1, - null_user: true, - current_user: true, - project_id: project.try(:id), - field_name: "issue[assignee_ids][]", - default_label: 'Assignee', - 'max-select': 1, - 'dropdown-header': 'Assignee', - multi_select: true, - 'input-meta': 'name', - 'always-show-selectbox': true - } - } - - if has_multiple_assignees - options[:title] = 'Select assignee(s)' - options[:data][:'dropdown-header'] = 'Assignee(s)' - options[:data].delete(:'max-select') - end + options = original_issue_dropdown_options.bind(original.receiver).call(*args) + options[:data][:per_page] = 2 options end @@ -74,6 +54,7 @@ describe 'New/edit issue', :feature, :js do click_link user2.name end + find('.js-assignee-search').click find('.js-dropdown-input-clear').click page.within '.dropdown-menu-user' do @@ -83,7 +64,7 @@ describe 'New/edit issue', :feature, :js do end end - describe 'single assignee' do + describe 'single assignee (CE)' do before do click_button 'Unassigned' diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb index 80f57906506..2c0a6ffd3cb 100644 --- a/spec/features/issues/note_polling_spec.rb +++ b/spec/features/issues/note_polling_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Issue notes polling', :feature, :js do + include NoteInteractionHelpers + let(:project) { create(:empty_project, :public) } let(:issue) { create(:issue, project: project) } @@ -48,7 +50,7 @@ feature 'Issue notes polling', :feature, :js do end it 'when editing but have not changed anything, and an update comes in, show the updated content in the textarea' do - find("#note_#{existing_note.id} .js-note-edit").click + click_edit_action(existing_note) expect(page).to have_field("note[note]", with: note_text) @@ -58,19 +60,18 @@ feature 'Issue notes polling', :feature, :js do end it 'when editing but you changed some things, and an update comes in, show a warning' do - find("#note_#{existing_note.id} .js-note-edit").click + click_edit_action(existing_note) expect(page).to have_field("note[note]", with: note_text) find("#note_#{existing_note.id} .js-note-text").set('something random') - update_note(existing_note, updated_text) expect(page).to have_selector(".alert") end it 'when editing but you changed some things, an update comes in, and you press cancel, show the updated content' do - find("#note_#{existing_note.id} .js-note-edit").click + click_edit_action(existing_note) expect(page).to have_field("note[note]", with: note_text) @@ -128,4 +129,12 @@ feature 'Issue notes polling', :feature, :js do note.update(note: new_text) page.execute_script('notes.refresh();') end + + def click_edit_action(note) + note_element = find("#note_#{note.id}") + + open_more_actions_dropdown(note) + + note_element.find('.js-note-edit').click + end end diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index 0911f1db9ba..8595847d313 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -14,7 +14,8 @@ feature 'Multiple issue updating from issues#index', feature: true do it 'sets to closed' do visit namespace_project_issues_path(project.namespace, project) - find('#check_all_issues').click + click_button 'Edit Issues' + find('#check-all-issues').click find('.js-issue-status').click find('.dropdown-menu-status a', text: 'Closed').click @@ -26,7 +27,8 @@ feature 'Multiple issue updating from issues#index', feature: true do create_closed visit namespace_project_issues_path(project.namespace, project, state: 'closed') - find('#check_all_issues').click + click_button 'Edit Issues' + find('#check-all-issues').click find('.js-issue-status').click find('.dropdown-menu-status a', text: 'Open').click @@ -39,7 +41,8 @@ feature 'Multiple issue updating from issues#index', feature: true do it 'updates to current user' do visit namespace_project_issues_path(project.namespace, project) - find('#check_all_issues').click + click_button 'Edit Issues' + find('#check-all-issues').click click_update_assignee_button find('.dropdown-menu-user-link', text: user.username).click @@ -54,7 +57,8 @@ feature 'Multiple issue updating from issues#index', feature: true do create_assigned visit namespace_project_issues_path(project.namespace, project) - find('#check_all_issues').click + click_button 'Edit Issues' + find('#check-all-issues').click click_update_assignee_button click_link 'Unassigned' @@ -69,8 +73,9 @@ feature 'Multiple issue updating from issues#index', feature: true do it 'updates milestone' do visit namespace_project_issues_path(project.namespace, project) - find('#check_all_issues').click - find('.issues_bulk_update .js-milestone-select').click + click_button 'Edit Issues' + find('#check-all-issues').click + find('.issues-bulk-update .js-milestone-select').click find('.dropdown-menu-milestone a', text: milestone.title).click click_update_issues_button @@ -84,8 +89,9 @@ feature 'Multiple issue updating from issues#index', feature: true do expect(first('.issue')).to have_content milestone.title - find('#check_all_issues').click - find('.issues_bulk_update .js-milestone-select').click + click_button 'Edit Issues' + find('#check-all-issues').click + find('.issues-bulk-update .js-milestone-select').click find('.dropdown-menu-milestone a', text: "No Milestone").click click_update_issues_button @@ -112,7 +118,7 @@ feature 'Multiple issue updating from issues#index', feature: true do end def click_update_issues_button - find('.update_selected_issues').click + find('.update-selected-issues').click wait_for_requests end end diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 854e2d1758f..e23dc2cd940 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Diff note avatars', feature: true, js: true do + include NoteInteractionHelpers + let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } @@ -110,6 +112,8 @@ feature 'Diff note avatars', feature: true, js: true do end it 'removes avatar when note is deleted' do + open_more_actions_dropdown(note) + page.within find(".note-row-#{note.id}") do find('.js-note-delete').click end diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 1e26b3d601e..d086be70d69 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -40,13 +40,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters [data-state="closed"]').click expect_assignee_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters [data-state="all"]').click expect_assignee_visual_tokens() end @@ -73,13 +73,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters [data-state="closed"]').click expect_milestone_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters [data-state="all"]').click expect_milestone_visual_tokens() end @@ -142,11 +142,9 @@ describe 'Filter merge requests', feature: true do expect_tokens([{ name: 'assignee', value: "@#{user.username}" }]) expect_filtered_search_input_empty - input_filtered_search_keys("label:~#{label.title} ") + input_filtered_search_keys("label:~#{label.title}") expect_mr_list_count(1) - - find("#state-opened[href=\"#{URI.parse(current_url).path}?assignee_username=#{user.username}&label_name%5B%5D=#{label.title}&scope=all&state=opened\"]") end context 'assignee and label', js: true do @@ -163,13 +161,13 @@ describe 'Filter merge requests', feature: true do end it 'does not change when closed link is clicked' do - find('.issues-state-filters a', text: "Closed").click + find('.issues-state-filters [data-state="closed"]').click expect_assignee_label_visual_tokens() end it 'does not change when all link is clicked' do - find('.issues-state-filters a', text: "All").click + find('.issues-state-filters [data-state="all"]').click expect_assignee_label_visual_tokens() end diff --git a/spec/features/merge_requests/mini_pipeline_graph_spec.rb b/spec/features/merge_requests/mini_pipeline_graph_spec.rb index 3ceb91d951d..3a11ea3c8b2 100644 --- a/spec/features/merge_requests/mini_pipeline_graph_spec.rb +++ b/spec/features/merge_requests/mini_pipeline_graph_spec.rb @@ -12,13 +12,39 @@ feature 'Mini Pipeline Graph', :js, :feature do build.run login_as(user) - visit namespace_project_merge_request_path(project.namespace, project, merge_request) + visit_merge_request + end + + def visit_merge_request(format = :html) + visit namespace_project_merge_request_path(project.namespace, project, merge_request, format: format) end it 'should display a mini pipeline graph' do expect(page).to have_selector('.mr-widget-pipeline-graph') end + context 'as json' do + let(:artifacts_file1) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } + let(:artifacts_file2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/png') } + + before do + create(:ci_build, pipeline: pipeline, artifacts_file: artifacts_file1) + create(:ci_build, pipeline: pipeline, when: 'manual') + end + + it 'avoids repeated database queries' do + before = ActiveRecord::QueryRecorder.new { visit_merge_request(:json) } + + create(:ci_build, pipeline: pipeline, artifacts_file: artifacts_file2) + create(:ci_build, pipeline: pipeline, when: 'manual') + + after = ActiveRecord::QueryRecorder.new { visit_merge_request(:json) } + + expect(before.count).to eq(after.count) + expect(before.cached_count).to eq(after.cached_count) + end + end + describe 'build list toggle' do let(:toggle) do find('.mini-pipeline-graph-dropdown-toggle') diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb index 4ef59a8aeb8..bcdfdf78a44 100644 --- a/spec/features/merge_requests/update_merge_requests_spec.rb +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -98,14 +98,16 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t end def change_status(text) - find('#check_all_issues').click + click_button 'Edit Merge Requests' + find('#check-all-issues').click find('.js-issue-status').click find('.dropdown-menu-status a', text: text).click click_update_merge_requests_button end def change_assignee(text) - find('#check_all_issues').click + click_button 'Edit Merge Requests' + find('#check-all-issues').click find('.js-update-assignee').click wait_for_requests @@ -117,14 +119,15 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t end def change_milestone(text) - find('#check_all_issues').click - find('.issues_bulk_update .js-milestone-select').click + click_button 'Edit Merge Requests' + find('#check-all-issues').click + find('.issues-bulk-update .js-milestone-select').click find('.dropdown-menu-milestone a', text: text).click click_update_merge_requests_button end def click_update_merge_requests_button - find('.update_selected_issues').click + find('.update-selected-issues').click wait_for_requests end end diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb index 06de072257a..22552529b9e 100644 --- a/spec/features/merge_requests/user_posts_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_notes_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Merge requests > User posts notes', :js do + include NoteInteractionHelpers + let(:project) { create(:project) } let(:merge_request) do create(:merge_request, source_project: project, target_project: project) @@ -73,6 +75,8 @@ describe 'Merge requests > User posts notes', :js do describe 'editing the note' do before do find('.note').hover + open_more_actions_dropdown(note) + find('.js-note-edit').click end @@ -100,6 +104,8 @@ describe 'Merge requests > User posts notes', :js do wait_for_requests find('.note').hover + open_more_actions_dropdown(note) + find('.js-note-edit').click page.within('.current-note-edit-form') do @@ -126,6 +132,8 @@ describe 'Merge requests > User posts notes', :js do describe 'deleting an attachment' do before do find('.note').hover + open_more_actions_dropdown(note) + find('.js-note-edit').click end diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 27a20e78a43..7e2e685df26 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -17,6 +17,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do def disallow_personal_access_token_saves! allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false) + errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") } allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors) end @@ -91,8 +92,11 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do context "when revocation fails" do it "displays an error message" do - disallow_personal_access_token_saves! visit profile_personal_access_tokens_path + allow_any_instance_of(PersonalAccessToken).to receive(:update!).and_return(false) + + errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") } + allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors) click_on "Revoke" expect(active_personal_access_tokens).to have_text(personal_access_token.name) diff --git a/spec/features/projects/artifacts/file_spec.rb b/spec/features/projects/artifacts/file_spec.rb index 25c4f3c87a2..860373e531b 100644 --- a/spec/features/projects/artifacts/file_spec.rb +++ b/spec/features/projects/artifacts/file_spec.rb @@ -39,6 +39,7 @@ feature 'Artifact file', :js, feature: true do context 'JPG file' do before do + page.driver.browser.url_blacklist = [] visit_file('rails_sample.jpg') wait_for_requests diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 82cfbfda157..45fdb36e506 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' feature 'File blob', :js, feature: true do let(:project) { create(:project, :public) } - def visit_blob(path, fragment = nil) - visit namespace_project_blob_path(project.namespace, project, File.join('master', path), anchor: fragment) + def visit_blob(path, anchor: nil, ref: 'master') + visit namespace_project_blob_path(project.namespace, project, File.join(ref, path), anchor: anchor) wait_for_requests end @@ -101,7 +101,7 @@ feature 'File blob', :js, feature: true do context 'visiting with a line number anchor' do before do - visit_blob('files/markdown/ruby-style-guide.md', 'L1') + visit_blob('files/markdown/ruby-style-guide.md', anchor: 'L1') end it 'displays the blob using the simple viewer' do @@ -352,6 +352,37 @@ feature 'File blob', :js, feature: true do end end + context 'binary file that appears to be text in the first 1024 bytes' do + before do + visit_blob('encoding/binary-1.bin', ref: 'binary-encoding') + end + + it 'displays the blob' do + aggregate_failures do + # shows a download link + expect(page).to have_link('Download (23.8 KB)') + + # does not show a viewer switcher + expect(page).not_to have_selector('.js-blob-viewer-switcher') + + # The specs below verify an arguably incorrect result, but since we only + # learn that the file is not actually text once the text viewer content + # is loaded asynchronously, there is no straightforward way to get these + # synchronously loaded elements to display correctly. + # + # Clicking the copy button will result in nothing being copied. + # Clicking the raw button will result in the binary file being downloaded, + # as expected. + + # shows an enabled copy button, incorrectly + expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button, incorrectly + expect(page).to have_link('Open raw') + end + end + end + context '.gitlab-ci.yml' do before do project.add_master(project.creator) diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 1a38997450d..d04c3248ead 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -102,7 +102,7 @@ feature 'Editing file blob', feature: true, js: true do it 'shows blob editor with same branch' do expect(page).to have_current_path(namespace_project_edit_blob_path(project.namespace, project, tree_join(branch, file_path))) - expect(find('.js-target-branch .dropdown-toggle-text').text).to eq(branch) + expect(find('.js-branch-name').value).to eq(branch) end end @@ -112,7 +112,7 @@ feature 'Editing file blob', feature: true, js: true do end it 'shows blob editor with patch branch' do - expect(find('.js-target-branch .dropdown-toggle-text').text).to eq('patch-1') + expect(find('.js-branch-name').value).to eq('patch-1') end end end @@ -128,7 +128,7 @@ feature 'Editing file blob', feature: true, js: true do it 'shows blob editor with same branch' do expect(page).to have_current_path(namespace_project_edit_blob_path(project.namespace, project, tree_join(branch, file_path))) - expect(find('.js-target-branch .dropdown-toggle-text').text).to eq(branch) + expect(find('.js-branch-name').value).to eq(branch) end end end diff --git a/spec/features/projects/blobs/user_create_spec.rb b/spec/features/projects/blobs/user_create_spec.rb deleted file mode 100644 index 4b6c55f5f44..00000000000 --- a/spec/features/projects/blobs/user_create_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -require 'spec_helper' - -feature 'New blob creation', feature: true, js: true do - include TargetBranchHelpers - - given(:user) { create(:user) } - given(:role) { :developer } - given(:project) { create(:project) } - given(:content) { 'class NextFeature\nend\n' } - - background do - login_as(user) - project.team << [user, role] - visit namespace_project_new_blob_path(project.namespace, project, 'master') - end - - def edit_file - wait_for_requests - fill_in 'file_name', with: 'feature.rb' - execute_script("ace.edit('editor').setValue('#{content}')") - end - - def commit_file - click_button 'Commit changes' - end - - context 'with default target branch' do - background do - edit_file - commit_file - end - - scenario 'creates the blob in the default branch' do - expect(page).to have_content 'master' - expect(page).to have_content 'successfully created' - expect(page).to have_content 'NextFeature' - end - end - - context 'with different target branch' do - background do - edit_file - select_branch('feature') - commit_file - end - - scenario 'creates the blob in the different branch' do - expect(page).to have_content 'feature' - expect(page).to have_content 'successfully created' - end - end - - context 'with a new target branch' do - given(:new_branch_name) { 'new-feature' } - - background do - edit_file - create_new_branch(new_branch_name) - commit_file - end - - scenario 'creates the blob in the new branch' do - expect(page).to have_content new_branch_name - expect(page).to have_content 'successfully created' - end - scenario 'returns you to the mr' do - expect(page).to have_content 'New Merge Request' - expect(page).to have_content "From #{new_branch_name} into master" - expect(page).to have_content 'Add new file' - end - end - - context 'the file already exist in the source branch' do - background do - Files::CreateService.new( - project, - user, - start_branch: 'master', - branch_name: 'master', - commit_message: 'Create file', - file_path: 'feature.rb', - file_content: content - ).execute - edit_file - commit_file - end - - scenario 'shows error message' do - expect(page).to have_content('A file with this name already exists') - expect(page).to have_content('New file') - expect(page).to have_content('NextFeature') - end - end -end diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb index 4e5682c8636..1b680a56492 100644 --- a/spec/features/projects/group_links_spec.rb +++ b/spec/features/projects/group_links_spec.rb @@ -16,15 +16,17 @@ feature 'Project group links', :feature, :js do before do visit namespace_project_settings_members_path(project.namespace, project) + click_on 'share-with-group-tab' + select2 group.id, from: '#link_group_id' fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') page.find('body').click - click_on 'Share' + find('.btn-create').trigger('click') end it 'shows the expiration time with a warning class' do - page.within('.enabled-groups') do - expect(page).to have_content('expires in 4 days') + page.within('.project-members-groups') do + expect(page).to have_content('Expires in 4 days') expect(page).to have_selector('.text-warning') end end @@ -43,6 +45,7 @@ feature 'Project group links', :feature, :js do it 'does not show ancestors', :nested_groups do visit namespace_project_settings_members_path(project.namespace, project) + click_on 'share-with-group-tab' click_link 'Search for a group' page.within '.select2-drop' do diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 77114266e7c..31237d40821 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -5,6 +5,7 @@ feature 'Jobs', :feature do let(:user) { create(:user) } let(:user_access_level) { :developer } let(:project) { create(:project) } + let(:namespace) { project.namespace } let(:pipeline) { create(:ci_pipeline, project: project) } let(:job) { create(:ci_build, :trace, pipeline: pipeline) } @@ -113,10 +114,16 @@ feature 'Jobs', :feature do describe "GET /:project/jobs/:id" do context "Job from project" do + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + before do visit namespace_project_job_path(project.namespace, project, job) end + it 'shows status name', :js do + expect(page).to have_css('.ci-status.ci-success', text: 'passed') + end + it 'shows commit`s data' do expect(page.status_code).to eq(200) expect(page).to have_content pipeline.sha[0..7] @@ -129,6 +136,48 @@ feature 'Jobs', :feature do end end + context 'when job is not running', :js do + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + + before do + visit namespace_project_job_path(project.namespace, project, build) + end + + it 'shows retry button' do + expect(page).to have_link('Retry') + end + + context 'if build passed' do + it 'does not show New issue button' do + expect(page).not_to have_link('New issue') + end + end + + context 'if build failed' do + let(:build) { create(:ci_build, :failed, pipeline: pipeline) } + + before do + visit namespace_project_job_path(namespace, project, build) + end + + it 'shows New issue button' do + expect(page).to have_link('New issue') + end + + it 'links to issues/new with the title and description filled in' do + button_title = "Build Failed ##{build.id}" + build_path = namespace_project_job_path(namespace, project, build) + options = { issue: { title: button_title, description: build_path } } + + href = new_namespace_project_issue_path(namespace, project, options) + + page.within('.header-action-buttons') do + expect(find('.js-new-issue')['href']).to include(href) + end + end + end + end + context "Job from other project" do before do visit namespace_project_job_path(project.namespace, project, job2) @@ -305,63 +354,38 @@ feature 'Jobs', :feature do end end - describe "POST /:project/jobs/:id/cancel" do + describe "POST /:project/jobs/:id/cancel", :js do context "Job from project" do before do job.run! visit namespace_project_job_path(project.namespace, project, job) - click_link "Cancel" + find('.js-cancel-job').click() end it 'loads the page and shows all needed controls' do expect(page.status_code).to eq(200) - expect(page).to have_content 'canceled' expect(page).to have_content 'Retry' end end - - context "Job from other project" do - before do - job.run! - visit namespace_project_job_path(project.namespace, project, job) - page.driver.post(cancel_namespace_project_job_path(project.namespace, project, job2)) - end - - it { expect(page.status_code).to eq(404) } - end end describe "POST /:project/jobs/:id/retry" do - context "Job from project" do + context "Job from project", :js do before do job.run! visit namespace_project_job_path(project.namespace, project, job) - click_link 'Cancel' - page.within('.build-header') do - click_link 'Retry job' - end + find('.js-cancel-job').click() + find('.js-retry-button').trigger('click') end - it 'shows the right status and buttons' do + it 'shows the right status and buttons', :js do expect(page).to have_http_status(200) - expect(page).to have_content 'pending' page.within('aside.right-sidebar') do expect(page).to have_content 'Cancel' end end end - context "Job from other project" do - before do - job.run! - visit namespace_project_job_path(project.namespace, project, job) - click_link 'Cancel' - page.driver.post(retry_namespace_project_job_path(project.namespace, project, job2)) - end - - it { expect(page).to have_http_status(404) } - end - context "Job that current user is not allowed to retry" do before do job.run! @@ -435,20 +459,17 @@ feature 'Jobs', :feature do Capybara.current_session.driver.headers = { 'X-Sendfile-Type' => 'X-Sendfile' } job.run! - - allow_any_instance_of(Gitlab::Ci::Trace).to receive(:paths) - .and_return(paths) - - visit namespace_project_job_path(project.namespace, project, job) end context 'when job has trace in file', :js do - let(:paths) do - [existing_file] - end - before do - find('.js-raw-link-controller').click() + allow_any_instance_of(Gitlab::Ci::Trace) + .to receive(:paths) + .and_return([existing_file]) + + visit namespace_project_job_path(namespace, project, build) + + find('.js-raw-link-controller').click end it 'sends the right headers' do @@ -458,11 +479,17 @@ feature 'Jobs', :feature do end end - context 'when job has trace in DB' do - let(:paths) { [] } + context 'when job has trace in the database', :js do + before do + allow_any_instance_of(Gitlab::Ci::Trace) + .to receive(:paths) + .and_return([]) + + visit namespace_project_job_path(namespace, project, build) + end it 'sends the right headers' do - expect(page.status_code).not_to have_selector('.js-raw-link-controller') + expect(page).not_to have_selector('.js-raw-link-controller') end end end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index c66b9a34b86..b1f9eb15667 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -17,10 +17,10 @@ feature "New project", feature: true do expect(find_field("project_visibility_level_#{level}")).to be_checked end - it 'saves visibility level on validation error' do + it "saves visibility level #{level} on validation error" do visit new_project_path - choose(key) + choose(s_(key)) click_button('Create project') expect(find_field("project_visibility_level_#{level}")).to be_checked diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb new file mode 100644 index 00000000000..4cc38c5286e --- /dev/null +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +feature 'Repository settings', feature: true do + let(:project) { create(:project_empty_repo) } + let(:user) { create(:user) } + let(:role) { :developer } + + background do + project.team << [user, role] + login_as(user) + end + + context 'for developer' do + given(:role) { :developer } + + scenario 'is not allowed to view' do + visit namespace_project_settings_repository_path(project.namespace, project) + + expect(page.status_code).to eq(404) + end + end + + context 'for master' do + given(:role) { :master } + + context 'Deploy Keys', js: true do + let(:private_deploy_key) { create(:deploy_key, title: 'private_deploy_key', public: false) } + let(:public_deploy_key) { create(:another_deploy_key, title: 'public_deploy_key', public: true) } + let(:new_ssh_key) { attributes_for(:key)[:key] } + + scenario 'get list of keys' do + project.deploy_keys << private_deploy_key + project.deploy_keys << public_deploy_key + + visit namespace_project_settings_repository_path(project.namespace, project) + + expect(page.status_code).to eq(200) + expect(page).to have_content('private_deploy_key') + expect(page).to have_content('public_deploy_key') + end + + scenario 'add a new deploy key' do + visit namespace_project_settings_repository_path(project.namespace, project) + + fill_in 'deploy_key_title', with: 'new_deploy_key' + fill_in 'deploy_key_key', with: new_ssh_key + check 'deploy_key_can_push' + click_button 'Add key' + + expect(page).to have_content('new_deploy_key') + expect(page).to have_content('Write access allowed') + end + + scenario 'edit an existing deploy key' do + project.deploy_keys << private_deploy_key + visit namespace_project_settings_repository_path(project.namespace, project) + + find('li', text: private_deploy_key.title).click_link('Edit') + + fill_in 'deploy_key_title', with: 'updated_deploy_key' + check 'deploy_key_can_push' + click_button 'Save changes' + + expect(page).to have_content('updated_deploy_key') + expect(page).to have_content('Write access allowed') + end + + scenario 'remove an existing deploy key' do + project.deploy_keys << private_deploy_key + visit namespace_project_settings_repository_path(project.namespace, project) + + find('li', text: private_deploy_key.title).click_button('Remove') + + expect(page).not_to have_content(private_deploy_key.title) + end + end + end +end diff --git a/spec/features/projects/snippets/create_snippet_spec.rb b/spec/features/projects/snippets/create_snippet_spec.rb new file mode 100644 index 00000000000..5ac1ca45c74 --- /dev/null +++ b/spec/features/projects/snippets/create_snippet_spec.rb @@ -0,0 +1,86 @@ +require 'rails_helper' + +feature 'Create Snippet', :js, feature: true do + include DropzoneHelper + + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public) } + + def fill_form + fill_in 'project_snippet_title', with: 'My Snippet Title' + fill_in 'project_snippet_description', with: 'My Snippet **Description**' + page.within('.file-editor') do + find('.ace_editor').native.send_keys('Hello World!') + end + end + + context 'when a user is authenticated' do + before do + project.team << [user, :master] + login_as(user) + + visit namespace_project_snippets_path(project.namespace, project) + + click_on('New snippet') + end + + it 'creates a new snippet' do + fill_form + click_button('Create snippet') + wait_for_requests + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('Hello World!') + page.within('.snippet-header .description') do + expect(page).to have_content('My Snippet Description') + expect(page).to have_selector('strong') + end + end + + it 'uploads a file when dragging into textarea' do + fill_form + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + expect(page.find_field("project_snippet_description").value).to have_content('banana_sample') + + click_button('Create snippet') + wait_for_requests + + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/#{Regexp.escape(project.full_path) }/uploads/\h{32}/banana_sample\.gif\z}) + end + + it 'creates a snippet when all reuiqred fields are filled in after validation failing' do + fill_in 'project_snippet_title', with: 'My Snippet Title' + click_button('Create snippet') + + expect(page).to have_selector('#error_explanation') + + fill_form + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + click_button('Create snippet') + wait_for_requests + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('Hello World!') + page.within('.snippet-header .description') do + expect(page).to have_content('My Snippet Description') + expect(page).to have_selector('strong') + end + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/#{Regexp.escape(project.full_path) }/uploads/\h{32}/banana_sample\.gif\z}) + end + end + + context 'when a user is not authenticated' do + it 'shows a public snippet on the index page but not the New snippet button' do + snippet = create(:project_snippet, :public, project: project) + + visit namespace_project_snippets_path(project.namespace, project) + + expect(page).to have_content(snippet.title) + expect(page).not_to have_content('New snippet') + end + end +end diff --git a/spec/features/projects/user_create_dir_spec.rb b/spec/features/projects/user_create_dir_spec.rb index 5dfdc465d7d..aeb7e0b7c33 100644 --- a/spec/features/projects/user_create_dir_spec.rb +++ b/spec/features/projects/user_create_dir_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'New directory creation', feature: true, js: true do - include TargetBranchHelpers - given(:user) { create(:user) } given(:role) { :developer } given(:project) { create(:project) } @@ -36,23 +34,11 @@ feature 'New directory creation', feature: true, js: true do end end - context 'with different target branch' do - background do - select_branch('feature') - create_directory - end - - scenario 'creates the directory in the different branch' do - expect(page).to have_content 'feature' - expect(page).to have_content 'The directory has been successfully created' - end - end - context 'with a new target branch' do given(:new_branch_name) { 'new-feature' } background do - create_new_branch(new_branch_name) + fill_in :branch_name, with: new_branch_name create_directory end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 49d7ef09e64..94f6bb16730 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -14,11 +14,12 @@ feature 'Projects > Wiki > User previews markdown changes', feature: true, js: t background do project.team << [user, :master] + WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute + login_as(user) visit namespace_project_path(project.namespace, project) find('.shortcuts-wiki').trigger('click') - WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute end context "while creating a new wiki page" do diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index 884d1bbb10c..667895bffa5 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projected Branches', feature: true, js: true do +feature 'Protected Branches', feature: true, js: true do let(:user) { create(:user, :admin) } let(:project) { create(:project, :repository) } diff --git a/spec/features/reportable_note/commit_spec.rb b/spec/features/reportable_note/commit_spec.rb new file mode 100644 index 00000000000..39b1c4acf52 --- /dev/null +++ b/spec/features/reportable_note/commit_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe 'Reportable note on commit', :feature, :js do + include RepoHelpers + + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.add_master(user) + login_as user + end + + context 'a normal note' do + let!(:note) { create(:note_on_commit, commit_id: sample_commit.id, project: project) } + + before do + visit namespace_project_commit_path(project.namespace, project, sample_commit.id) + end + + it_behaves_like 'reportable note' + end + + context 'a diff note' do + let!(:note) { create(:diff_note_on_commit, commit_id: sample_commit.id, project: project) } + + before do + visit namespace_project_commit_path(project.namespace, project, sample_commit.id) + end + + it_behaves_like 'reportable note' + end +end diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb new file mode 100644 index 00000000000..5f526818994 --- /dev/null +++ b/spec/features/reportable_note/issue_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'Reportable note on issue', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let!(:note) { create(:note_on_issue, noteable: issue, project: project) } + + before do + project.add_master(user) + login_as user + + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it_behaves_like 'reportable note' +end diff --git a/spec/features/reportable_note/merge_request_spec.rb b/spec/features/reportable_note/merge_request_spec.rb new file mode 100644 index 00000000000..6d053d26626 --- /dev/null +++ b/spec/features/reportable_note/merge_request_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe 'Reportable note on merge request', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + project.add_master(user) + login_as user + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'a normal note' do + let!(:note) { create(:note_on_merge_request, noteable: merge_request, project: project) } + + it_behaves_like 'reportable note' + end + + context 'a diff note' do + let!(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } + + it_behaves_like 'reportable note' + end +end diff --git a/spec/features/reportable_note/snippets_spec.rb b/spec/features/reportable_note/snippets_spec.rb new file mode 100644 index 00000000000..3f1e0cf9097 --- /dev/null +++ b/spec/features/reportable_note/snippets_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' + +describe 'Reportable note on snippets', :feature, :js do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + + before do + project.add_master(user) + login_as user + end + + describe 'on project snippet' do + let(:snippet) { create(:project_snippet, :public, project: project, author: user) } + let!(:note) { create(:note_on_project_snippet, noteable: snippet, project: project) } + + before do + visit namespace_project_snippet_path(project.namespace, project, snippet) + end + + it_behaves_like 'reportable note' + end + + describe 'on personal snippet' do + let(:snippet) { create(:personal_snippet, :public, author: user) } + let!(:note) { create(:note_on_personal_snippet, noteable: snippet, author: user) } + + before do + visit snippet_path(snippet) + end + + it_behaves_like 'reportable note' + end +end diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb index 31a2d4ae984..ddd31ede064 100644 --- a/spec/features/snippets/create_snippet_spec.rb +++ b/spec/features/snippets/create_snippet_spec.rb @@ -1,24 +1,93 @@ require 'rails_helper' feature 'Create Snippet', :js, feature: true do + include DropzoneHelper + before do login_as :user visit new_snippet_path end - scenario 'Authenticated user creates a snippet' do + def fill_form fill_in 'personal_snippet_title', with: 'My Snippet Title' + fill_in 'personal_snippet_description', with: 'My Snippet **Description**' page.within('.file-editor') do find('.ace_editor').native.send_keys 'Hello World!' end + end - click_button 'Create snippet' + scenario 'Authenticated user creates a snippet' do + fill_form + + click_button('Create snippet') wait_for_requests expect(page).to have_content('My Snippet Title') + page.within('.snippet-header .description') do + expect(page).to have_content('My Snippet Description') + expect(page).to have_selector('strong') + end expect(page).to have_content('Hello World!') end + scenario 'previews a snippet with file' do + fill_in 'personal_snippet_description', with: 'My Snippet' + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + find('.js-md-preview-button').click + + page.within('#new_personal_snippet .md-preview') do + expect(page).to have_content('My Snippet') + + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/uploads/temp/\h{32}/banana_sample\.gif\z}) + + visit(link) + expect(page.status_code).to eq(200) + end + end + + scenario 'uploads a file when dragging into textarea' do + fill_form + + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + expect(page.find_field("personal_snippet_description").value).to have_content('banana_sample') + + click_button('Create snippet') + wait_for_requests + + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/uploads/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) + + visit(link) + expect(page.status_code).to eq(200) + end + + scenario 'validation fails for the first time' do + fill_in 'personal_snippet_title', with: 'My Snippet Title' + click_button('Create snippet') + + expect(page).to have_selector('#error_explanation') + + fill_form + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + + click_button('Create snippet') + wait_for_requests + + expect(page).to have_content('My Snippet Title') + page.within('.snippet-header .description') do + expect(page).to have_content('My Snippet Description') + expect(page).to have_selector('strong') + end + expect(page).to have_content('Hello World!') + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/uploads/personal_snippet/#{Snippet.last.id}/\h{32}/banana_sample\.gif\z}) + + visit(link) + expect(page.status_code).to eq(200) + end + scenario 'Authenticated user creates a snippet with + in filename' do fill_in 'personal_snippet_title', with: 'My Snippet Title' page.within('.file-editor') do diff --git a/spec/features/snippets/edit_snippet_spec.rb b/spec/features/snippets/edit_snippet_spec.rb new file mode 100644 index 00000000000..89ae593db88 --- /dev/null +++ b/spec/features/snippets/edit_snippet_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +feature 'Edit Snippet', :js, feature: true do + include DropzoneHelper + + let(:file_name) { 'test.rb' } + let(:content) { 'puts "test"' } + + let(:user) { create(:user) } + let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, author: user) } + + before do + login_as(user) + + visit edit_snippet_path(snippet) + wait_for_requests + end + + it 'updates the snippet' do + fill_in 'personal_snippet_title', with: 'New Snippet Title' + + click_button('Save changes') + wait_for_requests + + expect(page).to have_content('New Snippet Title') + end + + it 'updates the snippet with files attached' do + dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif') + expect(page.find_field("personal_snippet_description").value).to have_content('banana_sample') + + click_button('Save changes') + wait_for_requests + + link = find('a.no-attachment-icon img[alt="banana_sample"]')['src'] + expect(link).to match(%r{/uploads/personal_snippet/#{snippet.id}/\h{32}/banana_sample\.gif\z}) + 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 f7afc174019..44b0c89fac7 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe 'Comments on personal snippets', :js, feature: true do + include NoteInteractionHelpers + let!(:user) { create(:user) } let!(:snippet) { create(:personal_snippet, :public) } let!(:snippet_notes) do @@ -22,6 +24,8 @@ describe 'Comments on personal snippets', :js, feature: true do it 'contains notes for a snippet with correct action icons' do expect(page).to have_selector('#notes-list li', count: 2) + open_more_actions_dropdown(snippet_notes[0]) + # comment authored by current user page.within("#notes-list li#note_#{snippet_notes[0].id}") do expect(page).to have_content(snippet_notes[0].note) @@ -29,6 +33,8 @@ describe 'Comments on personal snippets', :js, feature: true do expect(page).to have_selector('.note-emoji-button') end + open_more_actions_dropdown(snippet_notes[1]) + page.within("#notes-list li#note_#{snippet_notes[1].id}") do expect(page).to have_content(snippet_notes[1].note) expect(page).not_to have_selector('.js-note-delete') @@ -68,6 +74,8 @@ describe 'Comments on personal snippets', :js, feature: true do context 'when editing a note' do it 'changes the text' do + open_more_actions_dropdown(snippet_notes[0]) + page.within("#notes-list li#note_#{snippet_notes[0].id}") do click_on 'Edit comment' end @@ -89,8 +97,10 @@ describe 'Comments on personal snippets', :js, feature: true do context 'when deleting a note' do it 'removes the note from the snippet detail page' do + open_more_actions_dropdown(snippet_notes[0]) + page.within("#notes-list li#note_#{snippet_notes[0].id}") do - click_on 'Remove comment' + click_on 'Delete comment' end wait_for_requests diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb index bb4b2aed0e3..feb2fe8a7d1 100644 --- a/spec/features/todos/todos_spec.rb +++ b/spec/features/todos/todos_spec.rb @@ -333,29 +333,6 @@ describe 'Dashboard Todos', feature: true do end end - context 'User have large number of todos' do - before do - create_list(:todo, 101, :mentioned, user: user, project: project, target: issue, author: author) - - login_as(user) - visit dashboard_todos_path - end - - it 'shows 99+ for count >= 100 in notification' do - expect(page).to have_selector('.todos-count', text: '99+') - end - - it 'shows exact number in To do tab' do - expect(page).to have_selector('.todos-pending .badge', text: '101') - end - - it 'shows exact number for count < 100' do - 3.times { first('.js-done-todo').click } - - expect(page).to have_selector('.todos-count', text: '98') - end - end - context 'User has a Build Failed todo' do let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: author) } diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb index f88a515f7fc..d9d6f2e2382 100644 --- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb @@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do visit group_path(group) - expect(page).to have_selector(%Q(img[src$="/uploads/group/avatar/#{group.id}/dk.png"])) + expect(page).to have_selector(%Q(img[src$="/uploads/system/group/avatar/#{group.id}/dk.png"])) # Cheating here to verify something that isn't user-facing, but is important expect(group.reload.avatar.file).to exist diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb index 0dfd29045e5..eb8dbd76aab 100644 --- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb +++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb @@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do visit user_path(user) - expect(page).to have_selector(%Q(img[src$="/uploads/user/avatar/#{user.id}/dk.png"])) + expect(page).to have_selector(%Q(img[src$="/uploads/system/user/avatar/#{user.id}/dk.png"])) # Cheating here to verify something that isn't user-facing, but is important expect(user.reload.avatar.file).to exist diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb new file mode 100644 index 00000000000..c2842255b86 --- /dev/null +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +describe 'User can display performacne bar', :js do + shared_examples 'performance bar is disabled' do + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + + context 'when user press `pb`' do + before do + find('body').native.send_keys('pb') + end + + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + end + end + + shared_examples 'performance bar is enabled' do + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + + context 'when user press `pb`' do + before do + find('body').native.send_keys('pb') + end + + it 'does not show the performance bar by default' do + expect(page).not_to have_css('#peek') + end + end + end + + context 'when user is logged-out' do + before do + visit root_path + end + + context 'when the gitlab_performance_bar feature is disabled' do + before do + Feature.disable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + + context 'when the gitlab_performance_bar feature is enabled' do + before do + Feature.enable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + end + + context 'when user is logged-in' do + before do + login_as :user + + visit root_path + end + + context 'when the gitlab_performance_bar feature is disabled' do + before do + Feature.disable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is disabled' + end + + context 'when the gitlab_performance_bar feature is enabled' do + before do + Feature.enable('gitlab_performance_bar') + end + + it_behaves_like 'performance bar is enabled' + end + end +end diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 11a4caf6628..622a1e40d07 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -10,7 +10,7 @@ "id": { "type": "integer" }, "list_type": { "type": "string", - "enum": ["label", "closed"] + "enum": ["backlog", "label", "closed"] }, "label": { "type": ["object", "null"], diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 785fb724132..49df91b236f 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe ApplicationHelper do @@ -58,13 +59,13 @@ describe ApplicationHelper do describe 'project_icon' do it 'returns an url for the avatar' do project = create(:empty_project, avatar: File.open(uploaded_image_temp_path)) - avatar_url = "/uploads/project/avatar/#{project.id}/banana_sample.gif" + avatar_url = "/uploads/system/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon(project.full_path).to_s). to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) - avatar_url = "#{gitlab_host}/uploads/project/avatar/#{project.id}/banana_sample.gif" + avatar_url = "#{gitlab_host}/uploads/system/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon(project.full_path).to_s). to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />" @@ -84,12 +85,12 @@ describe ApplicationHelper do it 'returns an url for the avatar' do user = create(:user, avatar: File.open(uploaded_image_temp_path)) - avatar_url = "/uploads/user/avatar/#{user.id}/banana_sample.gif" + avatar_url = "/uploads/system/user/avatar/#{user.id}/banana_sample.gif" expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host) - avatar_url = "#{gitlab_host}/uploads/user/avatar/#{user.id}/banana_sample.gif" + avatar_url = "#{gitlab_host}/uploads/system/user/avatar/#{user.id}/banana_sample.gif" expect(helper.avatar_icon(user.email).to_s).to match(avatar_url) end @@ -102,7 +103,7 @@ describe ApplicationHelper do user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). - to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif") + to match("/gitlab/uploads/system/user/avatar/#{user.id}/banana_sample.gif") end it 'calls gravatar_icon when no User exists with the given email' do @@ -116,7 +117,7 @@ describe ApplicationHelper do user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user).to_s). - to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") + to match("/uploads/system/user/avatar/#{user.id}/banana_sample.gif") end end end diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb index cd112dbb2fb..c68e4f56b05 100644 --- a/spec/helpers/emails_helper_spec.rb +++ b/spec/helpers/emails_helper_spec.rb @@ -52,7 +52,7 @@ describe EmailsHelper do ) expect(header_logo).to eq( - %{<img style="height: 50px" src="/uploads/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />} + %{<img style="height: 50px" src="/uploads/system/appearance/header_logo/#{appearance.id}/dk.png" alt="Dk" />} ) end end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index c8b0d86425f..0337afa4452 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -9,7 +9,7 @@ describe GroupsHelper do group.avatar = fixture_file_upload(avatar_file_path) group.save! expect(group_icon(group.path).to_s). - to match("/uploads/group/avatar/#{group.id}/banana_sample.gif") + to match("/uploads/system/group/avatar/#{group.id}/banana_sample.gif") end it 'gives default avatar_icon when no avatar is present' do diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 355a4845afb..cc861af8533 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -256,4 +256,14 @@ describe NotesHelper do expect(helper.form_resources).to eq([@project.namespace, @project, @note]) end end + + describe '#noteable_note_url' do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note_on_issue, noteable: issue, project: project) } + + it 'returns the noteable url with an anchor to the note' do + expect(noteable_note_url(note)).to match("/#{project.namespace.path}/#{project.path}/issues/#{issue.iid}##{dom_id(note)}") + end + end end diff --git a/spec/helpers/notifications_helper_spec.rb b/spec/helpers/notifications_helper_spec.rb index 9d5f009ebe1..9ecaabc04ed 100644 --- a/spec/helpers/notifications_helper_spec.rb +++ b/spec/helpers/notifications_helper_spec.rb @@ -12,5 +12,11 @@ describe NotificationsHelper do describe 'notification_title' do it { expect(notification_title(:watch)).to match('Watch') } it { expect(notification_title(:mention)).to match('On mention') } + it { expect(notification_title(:global)).to match('Global') } + end + + describe '#notification_event_name' do + it { expect(notification_event_name(:success_pipeline)).to match('Successful pipeline') } + it { expect(notification_event_name(:failed_pipeline)).to match('Failed pipeline') } end end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index 2cc0b40b2d0..dff2784f21f 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -60,7 +60,7 @@ describe PageLayoutHelper do %w(project user group).each do |type| context "with @#{type} assigned" do it "uses #{type.titlecase} avatar if available" do - object = double(avatar_url: 'http://example.com/uploads/avatar.png') + object = double(avatar_url: 'http://example.com/uploads/system/avatar.png') assign(type, object) expect(helper.page_image).to eq object.avatar_url diff --git a/spec/helpers/profiles_helper_spec.rb b/spec/helpers/profiles_helper_spec.rb new file mode 100644 index 00000000000..b33b3f3a228 --- /dev/null +++ b/spec/helpers/profiles_helper_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +describe ProfilesHelper do + describe '#email_provider_label' do + it "returns nil for users without external email" do + user = create(:user) + allow(helper).to receive(:current_user).and_return(user) + + expect(helper.email_provider_label).to be_nil + end + + it "returns omniauth provider label for users with external email" do + stub_cas_omniauth_provider + cas_user = create(:omniauth_user, provider: 'cas3', external_email: true, email_provider: 'cas3') + allow(helper).to receive(:current_user).and_return(cas_user) + + expect(helper.email_provider_label).to eq('CAS') + end + + it "returns 'LDAP' for users with external email but no email provider" do + ldap_user = create(:omniauth_user, external_email: true) + allow(helper).to receive(:current_user).and_return(ldap_user) + + expect(helper.email_provider_label).to eq('LDAP') + end + end + + def stub_cas_omniauth_provider + provider = OpenStruct.new( + 'name' => 'cas3', + 'label' => 'CAS' + ) + + stub_omniauth_setting(providers: [provider]) + end +end diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb index 50060a0925d..18a41ca24e3 100644 --- a/spec/helpers/todos_helper_spec.rb +++ b/spec/helpers/todos_helper_spec.rb @@ -1,6 +1,19 @@ require "spec_helper" describe TodosHelper do + describe '#todos_count_format' do + it 'shows fuzzy count for 100 or more items' do + expect(helper.todos_count_format(100)).to eq '99+' + expect(helper.todos_count_format(1000)).to eq '99+' + end + + it 'shows exact count for 99 or fewer items' do + expect(helper.todos_count_format(99)).to eq '99' + expect(helper.todos_count_format(50)).to eq '50' + expect(helper.todos_count_format(1)).to eq '1' + end + end + describe '#todo_projects_options' do let(:projects) { create_list(:empty_project, 3) } let(:user) { create(:user) } diff --git a/spec/helpers/u2f_helper_spec.rb b/spec/helpers/u2f_helper_spec.rb new file mode 100644 index 00000000000..0d65b4fe0b8 --- /dev/null +++ b/spec/helpers/u2f_helper_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe U2fHelper do + describe 'when not on mobile' do + it 'does not inject u2f on chrome 40' do + device = double(mobile?: false) + browser = double(chrome?: true, opera?: false, version: 40, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq false + end + + it 'injects u2f on chrome 41' do + device = double(mobile?: false) + browser = double(chrome?: true, opera?: false, version: 41, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq true + end + + it 'does not inject u2f on opera 39' do + device = double(mobile?: false) + browser = double(chrome?: false, opera?: true, version: 39, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq false + end + + it 'injects u2f on opera 40' do + device = double(mobile?: false) + browser = double(chrome?: false, opera?: true, version: 40, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq true + end + end + + describe 'when on mobile' do + it 'does not inject u2f on chrome 41' do + device = double(mobile?: true) + browser = double(chrome?: true, opera?: false, version: 41, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq false + end + + it 'does not inject u2f on opera 40' do + device = double(mobile?: true) + browser = double(chrome?: false, opera?: true, version: 40, device: device) + allow(helper).to receive(:browser).and_return(browser) + expect(helper.inject_u2f_api?).to eq false + end + end +end diff --git a/spec/javascripts/blob/create_branch_dropdown_spec.js b/spec/javascripts/blob/create_branch_dropdown_spec.js deleted file mode 100644 index 6dbaa47c544..00000000000 --- a/spec/javascripts/blob/create_branch_dropdown_spec.js +++ /dev/null @@ -1,106 +0,0 @@ -import '~/gl_dropdown'; -import '~/blob/create_branch_dropdown'; -import '~/blob/target_branch_dropdown'; - -describe('CreateBranchDropdown', () => { - const fixtureTemplate = 'static/target_branch_dropdown.html.raw'; - // selectors - const createBranchSel = '.js-new-branch-btn'; - const backBtnSel = '.dropdown-menu-back'; - const cancelBtnSel = '.js-cancel-branch-btn'; - const branchNameSel = '#new_branch_name'; - const branchName = 'new_name'; - let dropdown; - - function createDropdown() { - const dropdownEl = document.querySelector('.js-project-branches-dropdown'); - const projectBranches = getJSONFixture('project_branches.json'); - dropdown = new gl.TargetBranchDropDown(dropdownEl); - dropdown.cachedRefs = projectBranches; - return dropdown; - } - - function createBranchBtn() { - return document.querySelector(createBranchSel); - } - - function backBtn() { - return document.querySelector(backBtnSel); - } - - function cancelBtn() { - return document.querySelector(cancelBtnSel); - } - - function branchNameEl() { - return document.querySelector(branchNameSel); - } - - function changeBranchName(text) { - branchNameEl().value = text; - branchNameEl().dispatchEvent(new Event('change')); - } - - preloadFixtures(fixtureTemplate); - - beforeEach(() => { - loadFixtures(fixtureTemplate); - createDropdown(); - }); - - it('disable submit when branch name is empty', () => { - expect(createBranchBtn()).toBeDisabled(); - }); - - it('enable submit when branch name is present', () => { - changeBranchName(branchName); - - expect(createBranchBtn()).not.toBeDisabled(); - }); - - it('resets the form when cancel btn is clicked and triggers dropdownback', () => { - const spyBackEvent = spyOnEvent(backBtnSel, 'click'); - changeBranchName(branchName); - - cancelBtn().click(); - - expect(branchNameEl()).toHaveValue(''); - expect(spyBackEvent).toHaveBeenTriggered(); - }); - - it('resets the form when back btn is clicked', () => { - changeBranchName(branchName); - - backBtn().click(); - - expect(branchNameEl()).toHaveValue(''); - }); - - describe('new branch creation', () => { - beforeEach(() => { - changeBranchName(branchName); - }); - it('sets the new branch name and updates the dropdown', () => { - spyOn(dropdown, 'setNewBranch'); - - createBranchBtn().click(); - - expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName); - }); - - it('resets the form', () => { - createBranchBtn().click(); - - expect(branchNameEl()).toHaveValue(''); - }); - - it('is triggered with enter keypress', () => { - spyOn(dropdown, 'setNewBranch'); - const enterEvent = new Event('keydown'); - enterEvent.which = 13; - branchNameEl().dispatchEvent(enterEvent); - - expect(dropdown.setNewBranch).toHaveBeenCalledWith(branchName); - }); - }); -}); diff --git a/spec/javascripts/blob/target_branch_dropdown_spec.js b/spec/javascripts/blob/target_branch_dropdown_spec.js deleted file mode 100644 index 99c9537d2ec..00000000000 --- a/spec/javascripts/blob/target_branch_dropdown_spec.js +++ /dev/null @@ -1,118 +0,0 @@ -import '~/gl_dropdown'; -import '~/blob/create_branch_dropdown'; -import '~/blob/target_branch_dropdown'; - -describe('TargetBranchDropdown', () => { - const fixtureTemplate = 'static/target_branch_dropdown.html.raw'; - let dropdown; - - function createDropdown() { - const projectBranches = getJSONFixture('project_branches.json'); - const dropdownEl = document.querySelector('.js-project-branches-dropdown'); - dropdown = new gl.TargetBranchDropDown(dropdownEl); - dropdown.cachedRefs = projectBranches; - dropdown.refreshData(); - return dropdown; - } - - function submitBtn() { - return document.querySelector('button[type="submit"]'); - } - - function searchField() { - return document.querySelector('.dropdown-page-one .dropdown-input-field'); - } - - function element() { - return document.querySelectorAll('div.dropdown-content li a'); - } - - function elementAtIndex(index) { - return element()[index]; - } - - function clickElementAtIndex(index) { - elementAtIndex(index).click(); - } - - preloadFixtures(fixtureTemplate); - - beforeEach(() => { - loadFixtures(fixtureTemplate); - createDropdown(); - }); - - it('disable submit when branch is not selected', () => { - document.querySelector('input[name="target_branch"]').value = null; - clickElementAtIndex(1); - - expect(submitBtn().getAttribute('disabled')).toEqual(''); - }); - - it('enable submit when a branch is selected', () => { - clickElementAtIndex(1); - - expect(submitBtn().getAttribute('disabled')).toBe(null); - }); - - it('triggers change.branch event on a branch click', () => { - spyOnEvent(dropdown.$dropdown, 'change.branch'); - clickElementAtIndex(0); - - expect('change.branch').toHaveBeenTriggeredOn(dropdown.$dropdown); - }); - - describe('dropdownData', () => { - it('cache the refs', () => { - const refs = dropdown.cachedRefs; - dropdown.cachedRefs = null; - - dropdown.dropdownData(refs); - - expect(dropdown.cachedRefs).toEqual(refs); - }); - - it('returns the Branches with the newBranch and defaultBranch', () => { - const refs = dropdown.cachedRefs; - dropdown.branchInput.value = 'master'; - dropdown.newBranch = { id: 'new_branch', text: 'new_branch', title: 'new_branch' }; - - const branches = dropdown.dropdownData(refs).Branches; - - expect(branches.length).toEqual(4); - expect(branches[0]).toEqual(dropdown.newBranch); - expect(branches[1]).toEqual({ id: 'master', text: 'master', title: 'master' }); - expect(branches[2]).toEqual({ id: 'development', text: 'development', title: 'development' }); - expect(branches[3]).toEqual({ id: 'staging', text: 'staging', title: 'staging' }); - }); - }); - - describe('setNewBranch', () => { - it('adds the new branch and select it', () => { - const branchName = 'new_branch'; - - dropdown.setNewBranch(branchName); - - expect(elementAtIndex(0)).toHaveClass('is-active'); - expect(elementAtIndex(0)).toContainHtml(branchName); - }); - - it("doesn't add a new branch if already exists in the list", () => { - const branchName = elementAtIndex(0).text; - const initialLength = element().length; - - dropdown.setNewBranch(branchName); - - expect(element().length).toEqual(initialLength); - }); - - it('clears the search filter', () => { - const branchName = elementAtIndex(0).text; - searchField().value = 'searching'; - - dropdown.setNewBranch(branchName); - - expect(searchField().value).toEqual(''); - }); - }); -}); diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js index 45d12e252c4..832877de71c 100644 --- a/spec/javascripts/boards/board_new_issue_spec.js +++ b/spec/javascripts/boards/board_new_issue_spec.js @@ -19,6 +19,7 @@ describe('Issue boards new issue form', () => { }; }, }; + const submitIssue = () => { vm.$el.querySelector('.btn-success').click(); }; @@ -107,7 +108,7 @@ describe('Issue boards new issue form', () => { setTimeout(() => { submitIssue(); - expect(vm.$el.querySelector('.btn-success').disbled).not.toBe(true); + expect(vm.$el.querySelector('.btn-success').disabled).toBe(false); done(); }, 0); }); @@ -115,36 +116,43 @@ describe('Issue boards new issue form', () => { it('clears title after submit', (done) => { vm.title = 'submit issue'; - setTimeout(() => { + Vue.nextTick(() => { submitIssue(); - expect(vm.title).toBe(''); - done(); - }, 0); + setTimeout(() => { + expect(vm.title).toBe(''); + done(); + }, 0); + }); }); - it('adds new issue to list after submit', (done) => { + it('adds new issue to top of list after submit request', (done) => { vm.title = 'submit issue'; setTimeout(() => { submitIssue(); - expect(list.issues.length).toBe(2); - expect(list.issues[1].title).toBe('submit issue'); - expect(list.issues[1].subscribed).toBe(true); - done(); + setTimeout(() => { + expect(list.issues.length).toBe(2); + expect(list.issues[0].title).toBe('submit issue'); + expect(list.issues[0].subscribed).toBe(true); + done(); + }, 0); }, 0); }); it('sets detail issue after submit', (done) => { + expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe(undefined); vm.title = 'submit issue'; setTimeout(() => { submitIssue(); - expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue'); - done(); - }); + setTimeout(() => { + expect(gl.issueBoards.BoardsStore.detail.issue.title).toBe('submit issue'); + done(); + }, 0); + }, 0); }); it('sets detail list after submit', (done) => { @@ -153,8 +161,10 @@ describe('Issue boards new issue form', () => { setTimeout(() => { submitIssue(); - expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id); - done(); + setTimeout(() => { + expect(gl.issueBoards.BoardsStore.detail.list.id).toBe(list.id); + done(); + }, 0); }, 0); }); }); @@ -169,13 +179,12 @@ describe('Issue boards new issue form', () => { setTimeout(() => { expect(list.issues.length).toBe(1); done(); - }, 500); + }, 0); }, 0); }); it('shows error', (done) => { vm.title = 'error'; - submitIssue(); setTimeout(() => { submitIssue(); @@ -183,7 +192,7 @@ describe('Issue boards new issue form', () => { setTimeout(() => { expect(vm.error).toBe(true); done(); - }, 500); + }, 0); }, 0); }); }); diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js new file mode 100644 index 00000000000..c4e8966ad6c --- /dev/null +++ b/spec/javascripts/boards/components/board_spec.js @@ -0,0 +1,112 @@ +import Vue from 'vue'; +import '~/boards/services/board_service'; +import '~/boards/components/board'; +import '~/boards/models/list'; + +describe('Board component', () => { + let vm; + let el; + + beforeEach((done) => { + loadFixtures('boards/show.html.raw'); + + el = document.createElement('div'); + document.body.appendChild(el); + + // eslint-disable-next-line no-undef + gl.boardService = new BoardService('/', '/', 1); + + vm = new gl.issueBoards.Board({ + propsData: { + boardId: '1', + disabled: false, + issueLinkBase: '/', + rootPath: '/', + // eslint-disable-next-line no-undef + list: new List({ + id: 1, + position: 0, + title: 'test', + list_type: 'backlog', + }), + }, + }).$mount(el); + + Vue.nextTick(done); + }); + + afterEach(() => { + vm.$destroy(); + + // remove the component from the DOM + document.querySelector('.board').remove(); + + localStorage.removeItem(`boards.${vm.boardId}.${vm.list.type}.expanded`); + }); + + it('board is expandable when list type is backlog', () => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(true); + }); + + it('board is expandable when list type is closed', (done) => { + vm.list.type = 'closed'; + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(true); + + done(); + }); + }); + + it('board is not expandable when list type is label', (done) => { + vm.list.type = 'label'; + vm.list.isExpandable = false; + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-expandable'), + ).toBe(false); + + done(); + }); + }); + + it('collapses when clicking header', (done) => { + vm.$el.querySelector('.board-header').click(); + + Vue.nextTick(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + done(); + }); + }); + + it('created sets isExpanded to true from localStorage', (done) => { + vm.$el.querySelector('.board-header').click(); + + return Vue.nextTick() + .then(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + // call created manually + vm.$options.created[0].call(vm); + + return Vue.nextTick(); + }) + .then(() => { + expect( + vm.$el.classList.contains('is-collapsed'), + ).toBe(true); + + done(); + }); + }); +}); diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js index 461908f3fde..be90dbdd88a 100644 --- a/spec/javascripts/build_spec.js +++ b/spec/javascripts/build_spec.js @@ -58,7 +58,7 @@ describe('Build', () => { it('displays the remove date correctly', () => { const removeDateElement = document.querySelector('.js-artifacts-remove'); - expect(removeDateElement.innerText.trim()).toBe('1 year'); + expect(removeDateElement.innerText.trim()).toBe('1 year remaining'); }); }); @@ -132,23 +132,6 @@ describe('Build', () => { expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); expect($('#build-trace .js-build-output').text()).toMatch(/Different/); }); - - it('reloads the page when the build is done', () => { - spyOn(gl.utils, 'visitUrl'); - const deferred = $.Deferred(); - - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ - html: '<span>Final</span>', - status: 'passed', - append: true, - complete: true, - }); - - this.build = new Build(); - - expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); - }); }); describe('truncated information', () => { diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 187db7485a5..44a4386b250 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -28,6 +28,32 @@ import '~/commits'; expect(CommitsList).toBeDefined(); }); + describe('processCommits', () => { + it('should join commit headers', () => { + CommitsList.$contentList = $(` + <div> + <li class="commit-header" data-day="2016-09-20"> + <span class="day">20 Sep, 2016</span> + <span class="commits-count">1 commit</span> + </li> + <li class="commit"></li> + </div> + `); + + const data = ` + <li class="commit-header" data-day="2016-09-20"> + <span class="day">20 Sep, 2016</span> + <span class="commits-count">1 commit</span> + </li> + <li class="commit"></li> + `; + + // The last commit header should be removed + // since the previous one has the same data-day value. + expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); + }); + }); + describe('on entering input', () => { let ajaxSpy; diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index e347c980c78..e54ea11b08c 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -1,7 +1,27 @@ -import '~/lib/utils/datetime_utility'; +import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; (() => { describe('Date time utils', () => { + describe('timeFor', () => { + it('returns `past due` when in past', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() - 1); + + expect( + gl.utils.timeFor(date), + ).toBe('Past due'); + }); + + it('returns remaining time when in the future', () => { + const date = new Date(); + date.setFullYear(date.getFullYear() + 1); + + expect( + gl.utils.timeFor(date), + ).toBe('1 year remaining'); + }); + }); + describe('get day name', () => { it('should return Sunday', () => { const day = gl.utils.getDayName(new Date('07/17/2016')); @@ -62,4 +82,13 @@ import '~/lib/utils/datetime_utility'; }); }); }); + + describe('timeIntervalInWords', () => { + it('should return string with number of minutes and seconds', () => { + expect(timeIntervalInWords(9.54)).toEqual('9 seconds'); + expect(timeIntervalInWords(1)).toEqual('1 second'); + expect(timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); + expect(timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + }); + }); })(); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js index 793ab8c451d..a4b98f6140d 100644 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ b/spec/javascripts/deploy_keys/components/key_spec.js @@ -39,9 +39,15 @@ describe('Deploy keys key', () => { ).toBe(`created ${gl.utils.getTimeago().format(deployKey.created_at)}`); }); + it('shows edit button', () => { + expect( + vm.$el.querySelectorAll('.btn')[0].textContent.trim(), + ).toBe('Edit'); + }); + it('shows remove button', () => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Remove'); }); @@ -71,9 +77,15 @@ describe('Deploy keys key', () => { setTimeout(done); }); + it('shows edit button', () => { + expect( + vm.$el.querySelectorAll('.btn')[0].textContent.trim(), + ).toBe('Edit'); + }); + it('shows enable button', () => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Enable'); }); @@ -82,7 +94,7 @@ describe('Deploy keys key', () => { Vue.nextTick(() => { expect( - vm.$el.querySelector('.btn').textContent.trim(), + vm.$el.querySelectorAll('.btn')[1].textContent.trim(), ).toBe('Disable'); done(); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 6e59ee96c6b..6d00d71f145 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -97,6 +97,49 @@ describe('Filtered Search Manager', () => { }); }); + describe('searchState', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchManager.prototype, 'search').and.callFake(() => {}); + }); + + it('should blur button', () => { + const e = { + currentTarget: { + blur: () => {}, + }, + }; + spyOn(e.currentTarget, 'blur').and.callThrough(); + manager.searchState(e); + + expect(e.currentTarget.blur).toHaveBeenCalled(); + }); + + it('should not call search if there is no state', () => { + const e = { + currentTarget: { + blur: () => {}, + }, + }; + + manager.searchState(e); + expect(gl.FilteredSearchManager.prototype.search).not.toHaveBeenCalled(); + }); + + it('should call search when there is state', () => { + const e = { + currentTarget: { + blur: () => {}, + dataset: { + state: 'opened', + }, + }, + }; + + manager.searchState(e); + expect(gl.FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened'); + }); + }); + describe('search', () => { const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened'; @@ -316,42 +359,6 @@ describe('Filtered Search Manager', () => { }); }); - describe('unselects token', () => { - beforeEach(() => { - tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(` - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug', true)} - ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')} - ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')} - `); - }); - - it('unselects token when input is clicked', () => { - const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); - - expect(selectedToken.classList.contains('selected')).toEqual(true); - expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); - - // Click directly on input attached to document - // so that the click event will propagate properly - document.querySelector('.filtered-search').click(); - - expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); - expect(selectedToken.classList.contains('selected')).toEqual(false); - }); - - it('unselects token when document.body is clicked', () => { - const selectedToken = tokensContainer.querySelector('.js-visual-token .selected'); - - expect(selectedToken.classList.contains('selected')).toEqual(true); - expect(gl.FilteredSearchVisualTokens.unselectTokens).not.toHaveBeenCalled(); - - document.body.click(); - - expect(selectedToken.classList.contains('selected')).toEqual(false); - expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled(); - }); - }); - describe('toggleInputContainerFocus', () => { it('toggles on focus', () => { input.focus(); diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb new file mode 100644 index 00000000000..d7c3dc0a235 --- /dev/null +++ b/spec/javascripts/fixtures/boards.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :repository, namespace: namespace, path: 'boards-project') } + + render_views + + before(:all) do + clean_frontend_fixtures('boards/') + end + + before(:each) do + sign_in(admin) + end + + it 'boards/show.html.raw' do |example| + get(:index, + namespace_id: project.namespace, + project_id: project) + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml index ae745b292e6..84fa5395cb8 100644 --- a/spec/javascripts/fixtures/issuable_filter.html.haml +++ b/spec/javascripts/fixtures/issuable_filter.html.haml @@ -1,6 +1,6 @@ %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: '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'} diff --git a/spec/javascripts/fixtures/project_branches.json b/spec/javascripts/fixtures/project_branches.json deleted file mode 100644 index a96a4c0c095..00000000000 --- a/spec/javascripts/fixtures/project_branches.json +++ /dev/null @@ -1,5 +0,0 @@ -[ - "master", - "development", - "staging" -] diff --git a/spec/javascripts/fixtures/target_branch_dropdown.html.haml b/spec/javascripts/fixtures/target_branch_dropdown.html.haml deleted file mode 100644 index 821fb7940a0..00000000000 --- a/spec/javascripts/fixtures/target_branch_dropdown.html.haml +++ /dev/null @@ -1,28 +0,0 @@ -%form.js-edit-blob-form - %input{type: 'hidden', name: 'target_branch', value: 'master'} - %div - .dropdown - %button.dropdown-menu-toggle.js-project-branches-dropdown.js-target-branch{type: 'button', data: {toggle: 'dropdown', selected: 'master', field_name: 'target_branch', form_id: '.js-edit-blob-form'}} - .dropdown-menu.dropdown-menu-selectable.dropdown-menu-paging - .dropdown-page-one - .dropdown-title 'Select branch' - .dropdown-input - %input.dropdown-input-field{type: 'search', value: ''} - %i.fa.fa-search.dropdown-input-search - %i.fa.fa-times-dropdown-input-clear.js-dropdown-input-clear{role: 'button'} - .dropdown-content - .dropdown-footer - %ul.dropdown-footer-list - %li - %a.create-new-branch.dropdown-toggle-page{href: "#"} - Create new branch - .dropdown-page-two.dropdown-new-branch - %button.dropdown-title-button.dropdown-menu-back{type: 'button'} - .dropdown_title 'Create new branch' - .dropdown_content - %input#new_branch_name.default-dropdown-input{ type: "text", placeholder: "Name new branch" } - %button.btn.btn-primary.pull-left.js-new-branch-btn{ type: "button" } - Create - %button.btn.btn-default.pull-right.js-cancel-branch-btn{ type: "button" } - Cancel - %button{type: 'submit'} diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index 3292590b9ed..10fcc590c89 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -185,7 +185,7 @@ import '~/lib/utils/url_utility'; expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); - it('should focus on input when opening for the second time', () => { + it('should focus on input when opening for the second time after transition', () => { remoteCallback(); this.dropdownContainerElement.trigger({ type: 'keyup', @@ -193,6 +193,7 @@ import '~/lib/utils/url_utility'; keyCode: ARROW_KEYS.ESC }); this.dropdownButtonElement.click(); + this.dropdownContainerElement.trigger('transitionend'); expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); }); @@ -201,6 +202,7 @@ import '~/lib/utils/url_utility'; it('should focus input when passing array data to drop down', () => { initDropDown.call(this, false, true); this.dropdownButtonElement.click(); + this.dropdownContainerElement.trigger('transitionend'); expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR)); }); }); diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js index b2b46640e5b..a09e0072fa8 100644 --- a/spec/javascripts/gl_emoji_spec.js +++ b/spec/javascripts/gl_emoji_spec.js @@ -192,6 +192,9 @@ describe('gl_emoji', () => { }); describe('isFlagEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isFlagEmoji('')).toBeFalsy(); + }); it('should detect flag_ac', () => { expect(isFlagEmoji('🇦🇨')).toBeTruthy(); }); @@ -216,6 +219,9 @@ describe('gl_emoji', () => { }); describe('isKeycapEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isKeycapEmoji('')).toBeFalsy(); + }); it('should detect one(keycap)', () => { expect(isKeycapEmoji('1️⃣')).toBeTruthy(); }); @@ -231,6 +237,9 @@ describe('gl_emoji', () => { }); describe('isSkinToneComboEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isSkinToneComboEmoji('')).toBeFalsy(); + }); it('should detect hand_splayed_tone5', () => { expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy(); }); @@ -255,6 +264,9 @@ describe('gl_emoji', () => { }); describe('isHorceRacingSkinToneComboEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isHorceRacingSkinToneComboEmoji('')).toBeFalsy(); + }); it('should detect horse_racing_tone2', () => { expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy(); }); @@ -264,6 +276,9 @@ describe('gl_emoji', () => { }); describe('isPersonZwjEmoji', () => { + it('should gracefully handle empty string', () => { + expect(isPersonZwjEmoji('')).toBeFalsy(); + }); it('should detect couple_mm', () => { expect(isPersonZwjEmoji('👨❤️👨')).toBeTruthy(); }); @@ -300,6 +315,22 @@ describe('gl_emoji', () => { }); describe('isEmojiUnicodeSupported', () => { + it('should gracefully handle empty string with unicode support', () => { + const isSupported = isEmojiUnicodeSupported( + { '1.0': true }, + '', + '1.0', + ); + expect(isSupported).toBeTruthy(); + }); + it('should gracefully handle empty string without unicode support', () => { + const isSupported = isEmojiUnicodeSupported( + {}, + '', + '1.0', + ); + expect(isSupported).toBeFalsy(); + }); it('bomb(6.0) with 6.0 support', () => { const emojiKey = 'bomb'; const unicodeSupportMap = Object.assign({}, emptySupportMap, { diff --git a/spec/javascripts/groups/group_item_spec.js b/spec/javascripts/groups/group_item_spec.js new file mode 100644 index 00000000000..25e10552d95 --- /dev/null +++ b/spec/javascripts/groups/group_item_spec.js @@ -0,0 +1,102 @@ +import Vue from 'vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import GroupsStore from '~/groups/stores/groups_store'; +import { group1 } from './mock_data'; + +describe('Groups Component', () => { + let GroupItemComponent; + let component; + let store; + let group; + + describe('group with default data', () => { + beforeEach((done) => { + GroupItemComponent = Vue.extend(groupItemComponent); + store = new GroupsStore(); + group = store.decorateGroup(group1); + + component = new GroupItemComponent({ + propsData: { + group, + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + it('should render the group item correctly', () => { + expect(component.$el.classList.contains('group-row')).toBe(true); + expect(component.$el.classList.contains('.no-description')).toBe(false); + expect(component.$el.querySelector('.number-projects').textContent).toContain(group.numberProjects); + expect(component.$el.querySelector('.number-users').textContent).toContain(group.numberUsers); + expect(component.$el.querySelector('.group-visibility')).toBeDefined(); + expect(component.$el.querySelector('.avatar-container')).toBeDefined(); + expect(component.$el.querySelector('.title').textContent).toContain(group.name); + expect(component.$el.querySelector('.access-type').textContent).toContain(group.permissions.humanGroupAccess); + expect(component.$el.querySelector('.description').textContent).toContain(group.description); + expect(component.$el.querySelector('.edit-group')).toBeDefined(); + expect(component.$el.querySelector('.leave-group')).toBeDefined(); + }); + }); + + describe('group without description', () => { + beforeEach((done) => { + GroupItemComponent = Vue.extend(groupItemComponent); + store = new GroupsStore(); + group1.description = ''; + group = store.decorateGroup(group1); + + component = new GroupItemComponent({ + propsData: { + group, + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + it('should render group item correctly', () => { + expect(component.$el.querySelector('.description').textContent).toBe(''); + expect(component.$el.classList.contains('.no-description')).toBe(false); + }); + }); + + describe('user has not access to group', () => { + beforeEach((done) => { + GroupItemComponent = Vue.extend(groupItemComponent); + store = new GroupsStore(); + group1.permissions.human_group_access = null; + group = store.decorateGroup(group1); + + component = new GroupItemComponent({ + propsData: { + group, + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + it('should not display access type', () => { + expect(component.$el.querySelector('.access-type')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/groups/groups_spec.js b/spec/javascripts/groups/groups_spec.js new file mode 100644 index 00000000000..2a77f7259da --- /dev/null +++ b/spec/javascripts/groups/groups_spec.js @@ -0,0 +1,64 @@ +import Vue from 'vue'; +import groupFolderComponent from '~/groups/components/group_folder.vue'; +import groupItemComponent from '~/groups/components/group_item.vue'; +import groupsComponent from '~/groups/components/groups.vue'; +import GroupsStore from '~/groups/stores/groups_store'; +import { groupsData } from './mock_data'; + +describe('Groups Component', () => { + let GroupsComponent; + let store; + let component; + let groups; + + beforeEach((done) => { + Vue.component('group-folder', groupFolderComponent); + Vue.component('group-item', groupItemComponent); + + store = new GroupsStore(); + groups = store.setGroups(groupsData.groups); + + store.storePagination(groupsData.pagination); + + GroupsComponent = Vue.extend(groupsComponent); + + component = new GroupsComponent({ + propsData: { + groups: store.state.groups, + pageInfo: store.state.pageInfo, + }, + }).$mount(); + + Vue.nextTick(() => { + done(); + }); + }); + + afterEach(() => { + component.$destroy(); + }); + + describe('with data', () => { + it('should render a list of groups', () => { + expect(component.$el.classList.contains('groups-list-tree-container')).toBe(true); + expect(component.$el.querySelector('#group-12')).toBeDefined(); + expect(component.$el.querySelector('#group-1119')).toBeDefined(); + expect(component.$el.querySelector('#group-1120')).toBeDefined(); + }); + + it('should render group and its subgroup', () => { + const lists = component.$el.querySelectorAll('.group-list-tree'); + + expect(lists.length).toBe(3); // one parent and two subgroups + + expect(lists[0].querySelector('#group-1119').classList.contains('is-open')).toBe(true); + expect(lists[0].querySelector('#group-1119').classList.contains('has-subgroups')).toBe(true); + + expect(lists[2].querySelector('#group-1120').textContent).toContain(groups[1119].subGroups[1120].name); + }); + + it('should remove prefix of parent group', () => { + expect(component.$el.querySelector('#group-12 #group-1128 .title').textContent).toContain('level2 / level3 / level4'); + }); + }); +}); diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js new file mode 100644 index 00000000000..1c0ec7a97d0 --- /dev/null +++ b/spec/javascripts/groups/mock_data.js @@ -0,0 +1,110 @@ +const group1 = { + id: '12', + name: 'level1', + path: 'level1', + description: 'foo', + visibility: 'public', + avatar_url: null, + web_url: 'http://localhost:3000/groups/level1', + full_name: 'level1', + full_path: 'level1', + parent_id: null, + created_at: '2017-05-15T19:01:23.670Z', + updated_at: '2017-05-15T19:01:23.670Z', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', + has_subgroups: true, + permissions: { + human_group_access: 'Master', + }, +}; + +// This group has no direct parent, should be placed as subgroup of group1 +const group14 = { + id: 1128, + name: 'level4', + path: 'level4', + description: 'foo', + visibility: 'public', + avatar_url: null, + web_url: 'http://localhost:3000/groups/level1/level2/level3/level4', + full_name: 'level1 / level2 / level3 / level4', + full_path: 'level1/level2/level3/level4', + parent_id: 1127, + created_at: '2017-05-15T19:02:01.645Z', + updated_at: '2017-05-15T19:02:01.645Z', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', + has_subgroups: true, + permissions: { + human_group_access: 'Master', + }, +}; + +const group2 = { + id: 1119, + name: 'devops', + path: 'devops', + description: 'foo', + visibility: 'public', + avatar_url: null, + web_url: 'http://localhost:3000/groups/devops', + full_name: 'devops', + full_path: 'devops', + parent_id: null, + created_at: '2017-05-11T19:35:09.635Z', + updated_at: '2017-05-11T19:35:09.635Z', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', + has_subgroups: true, + permissions: { + human_group_access: 'Master', + }, +}; + +const group21 = { + id: 1120, + name: 'chef', + path: 'chef', + description: 'foo', + visibility: 'public', + avatar_url: null, + web_url: 'http://localhost:3000/groups/devops/chef', + full_name: 'devops / chef', + full_path: 'devops/chef', + parent_id: 1119, + created_at: '2017-05-11T19:51:04.060Z', + updated_at: '2017-05-11T19:51:04.060Z', + number_projects_with_delimiter: '1', + number_users_with_delimiter: '1', + has_subgroups: true, + permissions: { + human_group_access: 'Master', + }, +}; + +const groupsData = { + groups: [group1, group14, group2, group21], + pagination: { + Date: 'Mon, 22 May 2017 22:31:52 GMT', + 'X-Prev-Page': '1', + 'X-Content-Type-Options': 'nosniff', + 'X-Total': '31', + 'Transfer-Encoding': 'chunked', + 'X-Runtime': '0.611144', + 'X-Xss-Protection': '1; mode=block', + 'X-Request-Id': 'f5db8368-3ce5-4aa4-89d2-a125d9dead09', + 'X-Ua-Compatible': 'IE=edge', + 'X-Per-Page': '20', + Link: '<http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="prev", <http://localhost:3000/dashboard/groups.json?page=1&per_page=20>; rel="first", <http://localhost:3000/dashboard/groups.json?page=2&per_page=20>; rel="last"', + 'X-Next-Page': '', + Etag: 'W/"a82f846947136271cdb7d55d19ef33d2"', + 'X-Frame-Options': 'DENY', + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'max-age=0, private, must-revalidate', + 'X-Total-Pages': '2', + 'X-Page': '2', + }, +}; + +export { groupsData, group1 }; diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index 49fa2cb8367..45f55395d3a 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -1,7 +1,7 @@ -/* global Issuable */ +/* global IssuableIndex */ import '~/lib/utils/url_utility'; -import '~/issuable'; +import '~/issuable_index'; (() => { const BASE_URL = '/user/project/issues?scope=all&state=closed'; @@ -24,11 +24,11 @@ import '~/issuable'; beforeEach(() => { loadFixtures('static/issuable_filter.html.raw'); - Issuable.init(); + IssuableIndex.init(); }); it('should be defined', () => { - expect(window.Issuable).toBeDefined(); + expect(window.IssuableIndex).toBeDefined(); }); describe('filtering', () => { @@ -43,7 +43,7 @@ import '~/issuable'; it('should contain only the default parameters', () => { spyOn(gl.utils, 'visitUrl'); - Issuable.filterResults($filtersForm); + IssuableIndex.filterResults($filtersForm); expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); }); @@ -52,7 +52,7 @@ import '~/issuable'; spyOn(gl.utils, 'visitUrl'); updateForm({ search: 'broken' }, $filtersForm); - Issuable.filterResults($filtersForm); + IssuableIndex.filterResults($filtersForm); const params = `${DEFAULT_PARAMS}&search=broken`; expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); @@ -64,14 +64,14 @@ import '~/issuable'; // initial filter updateForm({ milestone_title: 'v1.0' }, $filtersForm); - Issuable.filterResults($filtersForm); + IssuableIndex.filterResults($filtersForm); let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); // update filter updateForm({ label_name: 'Frontend' }, $filtersForm); - Issuable.filterResults($filtersForm); + IssuableIndex.filterResults($filtersForm); params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); }); diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js new file mode 100644 index 00000000000..c7179b3e03d --- /dev/null +++ b/spec/javascripts/jobs/header_spec.js @@ -0,0 +1,63 @@ +import Vue from 'vue'; +import headerComponent from '~/jobs/components/header.vue'; + +describe('Job details header', () => { + let HeaderComponent; + let vm; + let props; + + beforeEach(() => { + HeaderComponent = Vue.extend(headerComponent); + + const threeWeeksAgo = new Date(); + threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + + props = { + job: { + status: { + group: 'failed', + icon: 'ci-status-failed', + label: 'failed', + text: 'failed', + details_path: 'path', + }, + id: 123, + created_at: threeWeeksAgo.toISOString(), + user: { + web_url: 'path', + name: 'Foo', + username: 'foobar', + email: 'foo@bar.com', + avatar_url: 'link', + }, + retry_path: 'path', + new_issue_path: 'path', + }, + isLoading: false, + }; + + vm = new HeaderComponent({ propsData: props }).$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render provided job information', () => { + expect( + vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('failed Job #123 triggered 3 weeks ago by Foo'); + }); + + it('should render retry link', () => { + expect( + vm.$el.querySelector('.js-retry-button').getAttribute('href'), + ).toEqual(props.job.retry_path); + }); + + it('should render new issue link', () => { + expect( + vm.$el.querySelector('.js-new-issue').getAttribute('href'), + ).toEqual(props.job.new_issue_path); + }); +}); diff --git a/spec/javascripts/jobs/job_details_mediator_spec.js b/spec/javascripts/jobs/job_details_mediator_spec.js new file mode 100644 index 00000000000..1d7fa7e12fc --- /dev/null +++ b/spec/javascripts/jobs/job_details_mediator_spec.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +import JobMediator from '~/jobs/job_details_mediator'; +import job from './mock_data'; + +describe('JobMediator', () => { + let mediator; + + beforeEach(() => { + mediator = new JobMediator({ endpoint: 'foo' }); + }); + + it('should set defaults', () => { + expect(mediator.store).toBeDefined(); + expect(mediator.service).toBeDefined(); + expect(mediator.options).toEqual({ endpoint: 'foo' }); + expect(mediator.state.isLoading).toEqual(false); + }); + + describe('request and store data', () => { + const interceptor = (request, next) => { + next(request.respondWith(JSON.stringify(job), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor); + }); + + it('should store received data', (done) => { + mediator.fetchJob(); + + setTimeout(() => { + expect(mediator.store.state.job).toEqual(job); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/jobs/job_store_spec.js b/spec/javascripts/jobs/job_store_spec.js new file mode 100644 index 00000000000..d00faf29d1e --- /dev/null +++ b/spec/javascripts/jobs/job_store_spec.js @@ -0,0 +1,26 @@ +import JobStore from '~/jobs/stores/job_store'; +import job from './mock_data'; + +describe('Job Store', () => { + let store; + + beforeEach(() => { + store = new JobStore(); + }); + + it('should set defaults', () => { + expect(store.state.job).toEqual({}); + }); + + describe('storeJob', () => { + it('should store empty object if none is provided', () => { + store.storeJob(); + expect(store.state.job).toEqual({}); + }); + + it('should store provided argument', () => { + store.storeJob(job); + expect(store.state.job).toEqual(job); + }); + }); +}); diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js new file mode 100644 index 00000000000..17e4ef26b2c --- /dev/null +++ b/spec/javascripts/jobs/mock_data.js @@ -0,0 +1,123 @@ +const threeWeeksAgo = new Date(); +threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); + +export default { + id: 4757, + name: 'test', + build_path: '/root/ci-mock/-/jobs/4757', + retry_path: '/root/ci-mock/-/jobs/4757/retry', + cancel_path: '/root/ci-mock/-/jobs/4757/cancel', + new_issue_path: '/root/ci-mock/issues/new', + playable: false, + created_at: threeWeeksAgo.toISOString(), + updated_at: threeWeeksAgo.toISOString(), + finished_at: threeWeeksAgo.toISOString(), + queued: 9.54, + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/-/jobs/4757', + favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'icon_action_retry', + title: 'Retry', + path: '/root/ci-mock/-/jobs/4757/retry', + method: 'post', + }, + }, + coverage: 20, + erased_at: threeWeeksAgo.toISOString(), + duration: 6.785563, + tags: ['tag'], + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + erase_path: '/root/ci-mock/-/jobs/4757/erase', + artifacts: [null], + runner: { + id: 1, + description: 'local ci runner', + edit_path: '/root/ci-mock/runners/1/edit', + }, + pipeline: { + id: 140, + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + active: false, + coverage: null, + source: 'unknown', + created_at: '2017-05-24T09:59:58.634Z', + updated_at: '2017-06-01T17:32:00.062Z', + path: '/root/ci-mock/pipelines/140', + flags: { + latest: true, + stuck: false, + yaml_errors: false, + retryable: false, + cancelable: false, + }, + details: { + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/140', + favicon: '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + }, + duration: 6, + finished_at: '2017-06-01T17:32:00.042Z', + }, + ref: { + name: 'abc', + path: '/root/ci-mock/commits/abc', + tag: false, + branch: true, + }, + commit: { + id: 'c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', + short_id: 'c5864777', + title: 'Add new file', + created_at: '2017-05-24T10:59:52.000+01:00', + parent_ids: ['798e5f902592192afaba73f4668ae30e56eae492'], + message: 'Add new file', + author_name: 'Root', + author_email: 'admin@example.com', + authored_date: '2017-05-24T10:59:52.000+01:00', + committer_name: 'Root', + committer_email: 'admin@example.com', + committed_date: '2017-05-24T10:59:52.000+01:00', + author: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + commit_url: 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', + commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', + }, + }, + merge_request: { + iid: 2, + path: '/root/ci-mock/merge_requests/2', + }, + raw_path: '/root/ci-mock/builds/4757/raw', +}; diff --git a/spec/javascripts/jobs/sidebar_detail_row_spec.js b/spec/javascripts/jobs/sidebar_detail_row_spec.js new file mode 100644 index 00000000000..3ac65709c4a --- /dev/null +++ b/spec/javascripts/jobs/sidebar_detail_row_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; +import sidebarDetailRow from '~/jobs/components/sidebar_detail_row.vue'; + +describe('Sidebar detail row', () => { + let SidebarDetailRow; + let vm; + + beforeEach(() => { + SidebarDetailRow = Vue.extend(sidebarDetailRow); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render no title', () => { + vm = new SidebarDetailRow({ + propsData: { + value: 'this is the value', + }, + }).$mount(); + + expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toEqual('this is the value'); + }); + + beforeEach(() => { + vm = new SidebarDetailRow({ + propsData: { + title: 'this is the title', + value: 'this is the value', + }, + }).$mount(); + }); + + it('should render provided title and value', () => { + expect( + vm.$el.textContent.replace(/\s+/g, ' ').trim(), + ).toEqual('this is the title: this is the value'); + }); +}); diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js new file mode 100644 index 00000000000..95532ef5382 --- /dev/null +++ b/spec/javascripts/jobs/sidebar_details_block_spec.js @@ -0,0 +1,111 @@ +import Vue from 'vue'; +import sidebarDetailsBlock from '~/jobs/components/sidebar_details_block.vue'; +import job from './mock_data'; + +describe('Sidebar details block', () => { + let SidebarComponent; + let vm; + + function trimWhitespace(element) { + return element.textContent.replace(/\s+/g, ' ').trim(); + } + + beforeEach(() => { + SidebarComponent = Vue.extend(sidebarDetailsBlock); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('when it is loading', () => { + it('should render a loading spinner', () => { + vm = new SidebarComponent({ + propsData: { + job: {}, + isLoading: true, + }, + }).$mount(); + + expect(vm.$el.querySelector('.fa-spinner')).toBeDefined(); + }); + }); + + beforeEach(() => { + vm = new SidebarComponent({ + propsData: { + job, + isLoading: false, + }, + }).$mount(); + }); + + describe('actions', () => { + it('should render link to new issue', () => { + expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(job.new_issue_path); + expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue'); + }); + + it('should render link to retry job', () => { + expect(vm.$el.querySelector('.js-retry-job').getAttribute('href')).toEqual(job.retry_path); + }); + + it('should render link to cancel job', () => { + expect(vm.$el.querySelector('.js-cancel-job').getAttribute('href')).toEqual(job.cancel_path); + }); + }); + + describe('information', () => { + it('should render merge request link', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-mr')), + ).toEqual('Merge Request: !2'); + + expect( + vm.$el.querySelector('.js-job-mr a').getAttribute('href'), + ).toEqual(job.merge_request.path); + }); + + it('should render job duration', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-duration')), + ).toEqual('Duration: 6 seconds'); + }); + + it('should render erased date', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-erased')), + ).toEqual('Erased: 3 weeks ago'); + }); + + it('should render finished date', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-finished')), + ).toEqual('Finished: 3 weeks ago'); + }); + + it('should render queued date', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-queued')), + ).toEqual('Queued: 9 seconds'); + }); + + it('should render runner ID', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-runner')), + ).toEqual('Runner: #1'); + }); + + it('should render coverage', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-coverage')), + ).toEqual('Coverage: 20%'); + }); + + it('should render tags', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-tags')), + ).toEqual('Tags: tag'); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index e3938a77680..52cf217c25f 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -150,6 +150,14 @@ import '~/lib/utils/common_utils'; const value = gl.utils.getParameterByName('fakeParameter'); expect(value).toBe(null); }); + + it('should return valid paramentes if URL is provided', () => { + let value = gl.utils.getParameterByName('foo', 'http://cocteau.twins/?foo=bar'); + expect(value).toBe('bar'); + + value = gl.utils.getParameterByName('manan', 'http://cocteau.twins/?foo=bar&manan=canchu'); + expect(value).toBe('canchu'); + }); }); describe('gl.utils.normalizedHeaders', () => { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 24335614e09..bfd8b8648a6 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -461,6 +461,45 @@ import '~/notes'; }); }); + describe('update comment with script tags', () => { + const sampleComment = '<script></script>'; + const updatedComment = '<script></script>'; + const note = { + id: 1234, + html: `<li class="note note-row-1234 timeline-entry" id="note_1234"> + <div class="note-text">${sampleComment}</div> + </li>`, + note: sampleComment, + valid: true + }; + let $form; + let $notesContainer; + + beforeEach(() => { + this.notes = new Notes('', []); + window.gon.current_username = 'root'; + window.gon.current_user_fullname = 'Administrator'; + $form = $('form.js-main-target-form'); + $notesContainer = $('ul.main-notes-list'); + $form.find('textarea.js-note-text').html(sampleComment); + }); + + it('should not render a script tag', () => { + const deferred = $.Deferred(); + spyOn($, 'ajax').and.returnValue(deferred.promise()); + $('.js-comment-button').click(); + + deferred.resolve(note); + const $noteEl = $notesContainer.find(`#note_${note.id}`); + $noteEl.find('.js-note-edit').click(); + $noteEl.find('textarea.js-note-text').html(updatedComment); + $noteEl.find('.js-comment-save-button').click(); + + const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container'); + expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(''); + }); + }); + describe('getFormData', () => { let $form; let sampleComment; diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js index 601eebce38a..f1697840fcd 100644 --- a/spec/javascripts/pipelines/nav_controls_spec.js +++ b/spec/javascripts/pipelines/nav_controls_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import navControlsComp from '~/pipelines/components/nav_controls'; +import navControlsComp from '~/pipelines/components/nav_controls.vue'; describe('Pipelines Nav Controls', () => { let NavControlsComponent; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 13827a26571..2c34402576b 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -51,7 +51,6 @@ if (process.env.BABEL_ENV === 'coverage') { './environments/environments_bundle.js', './filtered_search/filtered_search_bundle.js', './graphs/graphs_bundle.js', - './issuable/issuable_bundle.js', './issuable/time_tracking/time_tracking_bundle.js', './main.js', './merge_conflicts/merge_conflicts_bundle.js', diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 050170a54e9..540245fe71e 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -22,7 +22,7 @@ describe('Commit component', () => { shortSha: 'b7836edd', title: 'Commit message', author: { - avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', + avatar_url: 'https://gitlab.com/uploads/system/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', path: '/jschatz1', username: 'jschatz1', @@ -45,7 +45,7 @@ describe('Commit component', () => { shortSha: 'b7836edd', title: 'Commit message', author: { - avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', + avatar_url: 'https://gitlab.com/uploads/system/user/avatar/300478/avatar.png', web_url: 'https://gitlab.com/jschatz1', path: '/jschatz1', username: 'jschatz1', 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 2b51c89f311..e28639f12f3 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -43,6 +43,7 @@ describe('Header CI Component', () => { isLoading: false, }, ], + hasSidebarButton: true, }; vm = new HeaderCi({ @@ -90,4 +91,8 @@ describe('Header CI Component', () => { done(); }); }); + + it('should render sidebar toggle button', () => { + expect(vm.$el.querySelector('.js-sidebar-build-toggle')).toBeDefined(); + }); }); diff --git a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb index 27684882435..787c2372c5b 100644 --- a/spec/lib/banzai/filter/abstract_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/abstract_reference_filter_spec.rb @@ -47,16 +47,7 @@ describe Banzai::Filter::AbstractReferenceFilter do end end - context 'with RequestStore enabled' do - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - + context 'with RequestStore enabled', :request_store do it 'returns a list of Projects for a list of paths' do expect(filter.find_projects_for_paths([project.path_with_namespace])). to eq([project]) diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb index e5d332efb08..866297f94a9 100644 --- a/spec/lib/banzai/issuable_extractor_spec.rb +++ b/spec/lib/banzai/issuable_extractor_spec.rb @@ -29,16 +29,7 @@ describe Banzai::IssuableExtractor, lib: true do expect(result).to eq(issue_link => issue, merge_request_link => merge_request) end - describe 'caching' do - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - + describe 'caching', :request_store do it 'saves records to cache' do extractor.extract([issue_link, merge_request_link]) diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index d5746107ee1..f4f42bfc3ed 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -30,7 +30,7 @@ describe Banzai::ReferenceParser::BaseParser, lib: true do it 'checks if user can read the resource' do link['data-project'] = project.id.to_s - expect(subject).to receive(:can_read_reference?).with(user, project) + expect(subject).to receive(:can_read_reference?).with(user, project, link) subject.nodes_visible_to_user(user, [link]) end diff --git a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb index d217a775802..620875ece20 100644 --- a/spec/lib/banzai/reference_parser/snippet_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/snippet_parser_spec.rb @@ -4,20 +4,199 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do include ReferenceParserHelpers let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } - let(:snippet) { create(:snippet, project: project) } + let(:external_user) { create(:user, :external) } + let(:project_member) { create(:user) } + subject { described_class.new(project, user) } let(:link) { empty_html_link } + def visible_references(snippet_visibility, user = nil) + snippet = create(:project_snippet, snippet_visibility, project: project) + link['data-project'] = project.id.to_s + link['data-snippet'] = snippet.id.to_s + + subject.nodes_visible_to_user(user, [link]) + end + + before do + project.add_user(project_member, :developer) + end + describe '#nodes_visible_to_user' do - context 'when the link has a data-issue attribute' do - before { link['data-snippet'] = snippet.id.to_s } + context 'when a project is public and the snippets feature is enabled for everyone' do + before do + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::ENABLED) + end + + it 'creates a reference for guest for a public snippet' do + expect(visible_references(:public)).to eq([link]) + end + + it 'creates a reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to eq([link]) + end + + it 'creates a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to eq([link]) + end + + it 'does not create a reference for an external user for an internal snippet' do + expect(visible_references(:internal, external_user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end + end + + context 'when a project is public and the snippets feature is enabled for project team members' do + before do + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE) + end + + it 'creates a reference for a project member for a public snippet' do + expect(visible_references(:public, project_member)).to eq([link]) + end + + it 'does not create a reference for guest for a public snippet' do + expect(visible_references(:public, nil)).to be_empty + end + + it 'does not create a reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to be_empty + end + + it 'creates a reference for a project member for an internal snippet' do + expect(visible_references(:internal, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end + end + + context 'when a project is internal and the snippets feature is enabled for everyone' do + before do + project.update_attribute(:visibility, Gitlab::VisibilityLevel::INTERNAL) + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::ENABLED) + end + + it 'does not create a reference for guest for a public snippet' do + expect(visible_references(:public)).to be_empty + end + + it 'does not create a reference for an external user for a public snippet' do + expect(visible_references(:public, external_user)).to be_empty + end - it_behaves_like "referenced feature visibility", "snippets" + it 'creates a reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to eq([link]) + end + + it 'creates a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to eq([link]) + end + + it 'does not create a reference for an external user for an internal snippet' do + expect(visible_references(:internal, external_user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end + end + + context 'when a project is internal and the snippets feature is enabled for project team members' do + before do + project.update_attribute(:visibility, Gitlab::VisibilityLevel::INTERNAL) + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE) + end + + it 'creates a reference for a project member for a public snippet' do + expect(visible_references(:public, project_member)).to eq([link]) + end + + it 'does not create a reference for guest for a public snippet' do + expect(visible_references(:public, nil)).to be_empty + end + + it 'does not create reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to be_empty + end + + it 'creates a reference for a project member for an internal snippet' do + expect(visible_references(:internal, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end + end + + context 'when a project is private and the snippets feature is enabled for project team members' do + before do + project.update_attribute(:visibility, Gitlab::VisibilityLevel::PRIVATE) + project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::PRIVATE) + end + + it 'creates a reference for a project member for a public snippet' do + expect(visible_references(:public, project_member)).to eq([link]) + end + + it 'does not create a reference for guest for a public snippet' do + expect(visible_references(:public, nil)).to be_empty + end + + it 'does not create a reference for a regular user for a public snippet' do + expect(visible_references(:public, user)).to be_empty + end + + it 'creates a reference for a project member for an internal snippet' do + expect(visible_references(:internal, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for an internal snippet' do + expect(visible_references(:internal, user)).to be_empty + end + + it 'creates a reference for a project member for a private snippet' do + expect(visible_references(:private, project_member)).to eq([link]) + end + + it 'does not create a reference for a regular user for a private snippet' do + expect(visible_references(:private, user)).to be_empty + end end end describe '#referenced_by' do + let(:snippet) { create(:snippet, project: project) } describe 'when the link has a data-snippet attribute' do context 'using an existing snippet ID' do it 'returns an Array of snippets' do @@ -31,7 +210,7 @@ describe Banzai::ReferenceParser::SnippetParser, lib: true do it 'returns an empty Array' do link['data-snippet'] = '' - expect(subject.referenced_by([link])).to eq([]) + expect(subject.referenced_by([link])).to be_empty end end end diff --git a/spec/lib/banzai/reference_parser/user_parser_spec.rb b/spec/lib/banzai/reference_parser/user_parser_spec.rb index 592ed0d2b98..4d560667342 100644 --- a/spec/lib/banzai/reference_parser/user_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/user_parser_spec.rb @@ -43,18 +43,9 @@ describe Banzai::ReferenceParser::UserParser, lib: true do expect(subject.referenced_by([link])).to eq([user]) end - context 'when RequestStore is active' do + context 'when RequestStore is active', :request_store do let(:other_user) { create(:user) } - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - it 'does not return users from the first call in the second' do link['data-user'] = user.id.to_s diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index fe2c00bb2ca..2ca0773ad1d 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' module Ci - describe GitlabCiYamlProcessor, lib: true do + describe GitlabCiYamlProcessor, :lib do + subject { described_class.new(config, path) } let(:path) { 'path' } describe 'our current .gitlab-ci.yml' do @@ -82,6 +83,67 @@ module Ci end end + describe '#stage_seeds' do + context 'when no refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, + rspec: { stage: 'test', script: 'rspec' }, + spinach: { stage: 'test', script: 'spinach' }) + end + + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'correctly fabricates a stage seeds object' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.second.stage[:name]).to eq 'deploy' + expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' + expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, + spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, ref: 'feature', tag: true) + end + + it 'returns stage seeds only assigned to master to master' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + + context 'when source policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, + spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, source: :schedule) + end + + it 'returns stage seeds only assigned to schedules' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + end + describe "#builds_for_ref" do let(:type) { 'test' } @@ -176,26 +238,44 @@ module Ci expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) end - it "returns builds if only has a triggers keyword specified and a trigger is provided" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["triggers"] } - }) + it "returns builds if only has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] - config_processor = GitlabCiYamlProcessor.new(config, path) + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, true).size).to eq(1) + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + end end - it "does not return builds if only has a triggers keyword specified and no trigger is provided" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["triggers"] } - }) + it "does not return builds if only has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] - config_processor = GitlabCiYamlProcessor.new(config, path) + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + end end it "returns builds if only has current repository path" do @@ -332,26 +412,44 @@ module Ci expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) end - it "does not return builds if except has a triggers keyword specified and a trigger is provided" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["triggers"] } - }) + it "does not return builds if except has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] - config_processor = GitlabCiYamlProcessor.new(config, path) + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, true).size).to eq(0) + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + end end - it "returns builds if except has a triggers keyword specified and no trigger is provided" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["triggers"] } - }) + it "returns builds if except has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] - config_processor = GitlabCiYamlProcessor.new(config, path) + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + config_processor = GitlabCiYamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + end end it "does not return builds if except has current repository path" do diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index 50bc3ef1b7c..d09da951869 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -17,7 +17,11 @@ describe Gitlab::Auth, lib: true do end it 'OPTIONAL_SCOPES contains all non-default scopes' do - expect(subject::OPTIONAL_SCOPES).to eq [:read_user, :openid] + expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid] + end + + it 'REGISTRY_SCOPES contains all registry related scopes' do + expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] end end @@ -143,6 +147,13 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities)) end + it 'succeeds for personal access tokens with the `read_registry` scope' do + personal_access_token = create(:personal_access_token, scopes: ['read_registry']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + end + it 'succeeds if it is an impersonation token' do impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api']) @@ -150,18 +161,11 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_token, full_authentication_abilities)) end - it 'fails for personal access tokens with other scopes' do + it 'limits abilities based on scope' do personal_access_token = create(:personal_access_token, scopes: ['read_user']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) - end - - it 'fails for impersonation token with other scopes' do - impersonation_token = create(:personal_access_token, scopes: ['read_user']) - - expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '') - expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [])) end it 'fails if password is nil' do @@ -200,6 +204,12 @@ describe Gitlab::Auth, lib: true do expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new) end + + it 'throws an error suggesting user create a PAT when internal auth is disabled' do + allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false } + + expect { gl_auth.find_for_git_client('foo', 'bar', project: nil, ip: 'ip') }.to raise_error(Gitlab::Auth::MissingPersonalTokenError) + end end describe 'find_with_user_password' do diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb new file mode 100644 index 00000000000..f2073b9bcb3 --- /dev/null +++ b/spec/lib/gitlab/background_migration_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration do + describe '.steal' do + it 'steals jobs from a queue' do + queue = [double(:job, args: ['Foo', [10, 20]])] + + allow(Sidekiq::Queue).to receive(:new). + with(BackgroundMigrationWorker.sidekiq_options['queue']). + and_return(queue) + + expect(queue[0]).to receive(:delete) + + expect(described_class).to receive(:perform).with('Foo', [10, 20]) + + described_class.steal('Foo') + end + + it 'does not steal jobs for a different migration' do + queue = [double(:job, args: ['Foo', [10, 20]])] + + allow(Sidekiq::Queue).to receive(:new). + with(BackgroundMigrationWorker.sidekiq_options['queue']). + and_return(queue) + + expect(described_class).not_to receive(:perform) + + expect(queue[0]).not_to receive(:delete) + + described_class.steal('Bar') + end + end + + describe '.perform' do + it 'performs a background migration' do + instance = double(:instance) + klass = double(:klass, new: instance) + + expect(described_class).to receive(:const_get). + with('Foo'). + and_return(klass) + + expect(instance).to receive(:perform).with(10, 20) + + described_class.perform('Foo', [10, 20]) + end + end +end diff --git a/spec/lib/gitlab/backup/repository_spec.rb b/spec/lib/gitlab/backup/repository_spec.rb new file mode 100644 index 00000000000..51c1e9d657b --- /dev/null +++ b/spec/lib/gitlab/backup/repository_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Backup::Repository, lib: true do + let(:progress) { StringIO.new } + let!(:project) { create(:empty_project) } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + allow_any_instance_of(described_class).to receive(:progress).and_return(progress) + end + + describe '#dump' do + describe 'repo failure' do + before do + allow_any_instance_of(Repository).to receive(:empty_repo?).and_raise(Rugged::OdbError) + allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) + end + + it 'does not raise error' do + expect { described_class.new.dump }.not_to raise_error + end + + it 'shows the appropriate error' do + described_class.new.dump + + expect(progress).to have_received(:puts).with("Ignoring repository error and continuing backing up project: #{project.full_path} - Rugged::OdbError") + end + end + + describe 'command failure' do + before do + allow_any_instance_of(Repository).to receive(:empty_repo?).and_return(false) + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + end + + it 'shows the appropriate error' do + described_class.new.dump + + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + end + end + end + + describe '#restore' do + describe 'command failure' do + before do + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + end + + it 'shows the appropriate error' do + described_class.new.restore + + expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error") + end + end + end +end diff --git a/spec/lib/gitlab/ci/stage/seed_spec.rb b/spec/lib/gitlab/ci/stage/seed_spec.rb new file mode 100644 index 00000000000..d7e91a5a62c --- /dev/null +++ b/spec/lib/gitlab/ci/stage/seed_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Gitlab::Ci::Stage::Seed do + let(:pipeline) { create(:ci_empty_pipeline) } + + let(:builds) do + [{ name: 'rspec' }, { name: 'spinach' }] + end + + subject do + described_class.new(pipeline, 'test', builds) + end + + describe '#stage' do + it 'returns hash attributes of a stage' do + expect(subject.stage).to be_a Hash + expect(subject.stage).to include(:name, :project) + end + end + + describe '#builds' do + it 'returns hash attributes of all builds' do + expect(subject.builds.size).to eq 2 + expect(subject.builds).to all(include(ref: 'master')) + expect(subject.builds).to all(include(tag: false)) + expect(subject.builds).to all(include(project: pipeline.project)) + expect(subject.builds) + .to all(include(trigger_request: pipeline.trigger_requests.first)) + end + end + + describe '#user=' do + let(:user) { build(:user) } + + it 'assignes relevant pipeline attributes' do + subject.user = user + + expect(subject.builds).to all(include(user: user)) + end + end + + describe '#create!' do + it 'creates all stages and builds' do + subject.create! + + expect(pipeline.reload.stages.count).to eq 1 + expect(pipeline.reload.builds.count).to eq 2 + expect(pipeline.builds).to all(satisfy { |job| job.stage_id.present? }) + expect(pipeline.builds).to all(satisfy { |job| job.pipeline.present? }) + expect(pipeline.builds).to all(satisfy { |job| job.project.present? }) + expect(pipeline.stages) + .to all(satisfy { |stage| stage.pipeline.present? }) + expect(pipeline.stages) + .to all(satisfy { |stage| stage.project.present? }) + end + end +end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index c796c98ec9f..fda39d78610 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -14,20 +14,20 @@ describe Gitlab::CurrentSettings do end it 'attempts to use cached values first' do - expect(ApplicationSetting).to receive(:current) - expect(ApplicationSetting).not_to receive(:last) + expect(ApplicationSetting).to receive(:cached) expect(current_application_settings).to be_a(ApplicationSetting) end it 'falls back to DB if Redis returns an empty value' do + expect(ApplicationSetting).to receive(:cached).and_return(nil) expect(ApplicationSetting).to receive(:last).and_call_original expect(current_application_settings).to be_a(ApplicationSetting) end it 'falls back to DB if Redis fails' do - expect(ApplicationSetting).to receive(:current).and_raise(::Redis::BaseError) + expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError) expect(ApplicationSetting).to receive(:last).and_call_original expect(current_application_settings).to be_a(ApplicationSetting) @@ -37,6 +37,7 @@ describe Gitlab::CurrentSettings do context 'with DB unavailable' do before do allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(false) + allow_any_instance_of(described_class).to receive(:retrieve_settings_from_database_cache?).and_return(nil) end it 'returns an in-memory ApplicationSetting object' do diff --git a/spec/lib/gitlab/diff/diff_refs_spec.rb b/spec/lib/gitlab/diff/diff_refs_spec.rb new file mode 100644 index 00000000000..a8173558c00 --- /dev/null +++ b/spec/lib/gitlab/diff/diff_refs_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Gitlab::Diff::DiffRefs, lib: true do + let(:project) { create(:project, :repository) } + + describe '#compare_in' do + context 'with diff refs for the initial commit' do + let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + subject { commit.diff_refs } + + it 'returns an appropriate comparison' do + compare = subject.compare_in(project) + + expect(compare.diff_refs).to eq(subject) + end + end + + context 'with diff refs for a commit' do + let(:commit) { project.commit('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } + subject { commit.diff_refs } + + it 'returns an appropriate comparison' do + compare = subject.compare_in(project) + + expect(compare.diff_refs).to eq(subject) + end + end + + context 'with diff refs for a comparison through the base' do + subject do + described_class.new( + start_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', # feature + base_sha: 'ae73cb07c9eeaf35924a10f713b364d32b2dd34f', + head_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a' # master + ) + end + + it 'returns an appropriate comparison' do + compare = subject.compare_in(project) + + expect(compare.diff_refs).to eq(subject) + end + end + + context 'with diff refs for a straight comparison' do + subject do + described_class.new( + start_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', # feature + base_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', + head_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a' # master + ) + end + + it 'returns an appropriate comparison' do + compare = subject.compare_in(project) + + expect(compare.diff_refs).to eq(subject) + end + end + end +end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index f2bc15d39d7..d81774c8b8f 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -5,15 +5,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do let(:diff_files) { described_class.new(merge_request.merge_request_diff, diff_options: nil).diff_files } it 'does not highlight binary files' do - allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => false)) - - expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) - - diff_files - end - - it 'does not highlight file if blob is not accessable' do - allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(nil) + allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(false) expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) @@ -21,7 +13,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do end it 'does not files marked as undiffable in .gitattributes' do - allow_any_instance_of(Repository).to receive(:diffable?).and_return(false) + allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(false) expect_any_instance_of(Gitlab::Diff::File).not_to receive(:highlighted_diff_lines) diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index 050689b7c9a..a9953bb0d01 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do let(:project) { create(:project, :repository) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.raw_diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } + let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) } describe '#diff_lines' do let(:diff_lines) { diff_file.diff_lines } @@ -63,11 +63,33 @@ describe Gitlab::Diff::File, lib: true do end end - describe '#blob' do + describe '#new_blob' do it 'returns blob of new commit' do - data = diff_file.blob.data + data = diff_file.new_blob.data expect(data).to include('raise RuntimeError, "System commands must be given as an array of strings"') end end + + describe '#diffable?' do + let(:commit) { project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863') } + let(:diffs) { commit.diffs } + + before do + info_dir_path = File.join(project.repository.path_to_repo, 'info') + + FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) + File.write(File.join(info_dir_path, 'attributes'), "*.md -diff\n") + end + + it "returns true for files that do not have attributes" do + diff_file = diffs.diff_file_with_new_path('LICENSE') + expect(diff_file.diffable?).to be_truthy + end + + it "returns false for files that have been marked as not being diffable in attributes" do + diff_file = diffs.diff_file_with_new_path('README.md') + expect(diff_file.diffable?).to be_falsey + end + end end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 7095104d75c..b3d46e69ccb 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -381,6 +381,54 @@ describe Gitlab::Diff::Position, lib: true do end end + describe "position for a file in a straight comparison" do + let(:diff_refs) do + Gitlab::Diff::DiffRefs.new( + start_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', # feature + base_sha: '0b4bc9a49b562e85de7cc9e834518ea6828729b9', + head_sha: 'e63f41fe459e62e1228fcef60d7189127aeba95a' # master + ) + end + + subject do + described_class.new( + old_path: "files/ruby/feature.rb", + new_path: "files/ruby/feature.rb", + old_line: 3, + new_line: nil, + diff_refs: diff_refs + ) + end + + describe "#diff_file" do + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.deleted_file?).to be true + expect(diff_file.old_path).to eq(subject.old_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + + describe "#diff_line" do + it "returns the correct diff line" do + diff_line = subject.diff_line(project.repository) + + expect(diff_line.removed?).to be true + expect(diff_line.old_line).to eq(subject.old_line) + expect(diff_line.text).to eq("- puts 'bar'") + end + end + + describe "#line_code" do + it "returns the correct line code" do + line_code = Gitlab::Diff::LineCode.generate(subject.file_path, 0, subject.old_line) + + expect(subject.line_code(project.repository)).to eq(line_code) + end + end + end + describe "#to_json" do let(:hash) do { diff --git a/spec/lib/gitlab/etag_caching/middleware_spec.rb b/spec/lib/gitlab/etag_caching/middleware_spec.rb index 24df04e985a..3c6ef7c7ccb 100644 --- a/spec/lib/gitlab/etag_caching/middleware_spec.rb +++ b/spec/lib/gitlab/etag_caching/middleware_spec.rb @@ -164,6 +164,25 @@ describe Gitlab::EtagCaching::Middleware do end end + context 'when GitLab instance is using a relative URL' do + before do + mock_app_response + end + + it 'uses full path as cache key' do + env = { + 'PATH_INFO' => enabled_path, + 'SCRIPT_NAME' => '/relative-gitlab' + } + + expect_any_instance_of(Gitlab::EtagCaching::Store) + .to receive(:get).with("/relative-gitlab#{enabled_path}") + .and_return(nil) + + middleware.call(env) + end + end + def mock_app_response allow(app).to receive(:call).and_return([app_status_code, {}, ['body']]) end diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb index 269798c7c9e..2bb40827fcf 100644 --- a/spec/lib/gitlab/etag_caching/router_spec.rb +++ b/spec/lib/gitlab/etag_caching/router_spec.rb @@ -2,115 +2,115 @@ require 'spec_helper' describe Gitlab::EtagCaching::Router do it 'matches issue notes endpoint' do - env = build_env( + request = build_request( '/my-group/and-subgroup/here-comes-the-project/noteable/issue/1/notes' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'issue_notes' end it 'matches issue title endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/issues/123/realtime_changes' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'issue_title' end it 'matches project pipelines endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/pipelines.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'project_pipelines' end it 'matches commit pipelines endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/commit/aa8260d253a53f73f6c26c734c72fdd600f6e6d4/pipelines.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'commit_pipelines' end it 'matches new merge request pipelines endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/merge_requests/new.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'new_merge_request_pipelines' end it 'matches merge request pipelines endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/merge_requests/234/pipelines.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'merge_request_pipelines' end it 'matches build endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/builds/234.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'project_build' end it 'does not match blob with confusing name' do - env = build_env( + request = build_request( '/my-group/my-project/blob/master/pipelines.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_blank end it 'matches the environments path' do - env = build_env( + request = build_request( '/my-group/my-project/environments.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'environments' end it 'matches pipeline#show endpoint' do - env = build_env( + request = build_request( '/my-group/my-project/pipelines/2.json' ) - result = described_class.match(env) + result = described_class.match(request) expect(result).to be_present expect(result.name).to eq 'project_pipeline' end - def build_env(path) - { 'PATH_INFO' => path } + def build_request(path) + double(path_info: path) end end diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index 7c45071ec45..4c9f4a28f32 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" describe Gitlab::Git::Compare, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } - let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) } - let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) } + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) } + let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) } describe '#commits' do subject do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 26215381cc4..e1e4aa9fde9 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1235,47 +1235,6 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#diffable' do - info_dir_path = attributes_path = File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info') - attributes_path = File.join(info_dir_path, 'attributes') - - before(:all) do - FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) - File.write(attributes_path, "*.md -diff\n") - end - - it "should return true for files which are text and do not have attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'LICENSE' - ) - expect(repository.diffable?(blob)).to be_truthy - end - - it "should return false for binary files which do not have attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'files/images/logo-white.png' - ) - expect(repository.diffable?(blob)).to be_falsey - end - - it "should return false for text files which have been marked as not being diffable in attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'README.md' - ) - expect(repository.diffable?(blob)).to be_falsey - end - - after(:all) do - FileUtils.rm_rf(info_dir_path) - end - end - describe '#tag_exists?' do it 'returns true for an existing tag' do tag = repository.tag_names.first diff --git a/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb new file mode 100644 index 00000000000..ed757ed60d8 --- /dev/null +++ b/spec/lib/gitlab/health_checks/prometheus_text_format_spec.rb @@ -0,0 +1,41 @@ +describe Gitlab::HealthChecks::PrometheusTextFormat do + let(:metric_class) { Gitlab::HealthChecks::Metric } + subject { described_class.new } + + describe '#marshal' do + let(:sample_metrics) do + [metric_class.new('metric1', 1), + metric_class.new('metric2', 2)] + end + + it 'marshal to text with non repeating type definition' do + expected = <<-EXPECTED.strip_heredoc + # TYPE metric1 gauge + metric1 1 + # TYPE metric2 gauge + metric2 2 + EXPECTED + + expect(subject.marshal(sample_metrics)).to eq(expected) + end + + context 'metrics where name repeats' do + let(:sample_metrics) do + [metric_class.new('metric1', 1), + metric_class.new('metric1', 2), + metric_class.new('metric2', 3)] + end + + it 'marshal to text with non repeating type definition' do + expected = <<-EXPECTED.strip_heredoc + # TYPE metric1 gauge + metric1 1 + metric1 2 + # TYPE metric2 gauge + metric2 3 + EXPECTED + expect(subject.marshal(sample_metrics)).to eq(expected) + end + end + end +end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index a20cef3b000..c2bb9f9a166 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -7,30 +7,6 @@ describe Gitlab::Highlight, lib: true do let(:repository) { project.repository } let(:commit) { project.commit(sample_commit.id) } - describe '.highlight_lines' do - let(:lines) do - Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') - end - - it 'highlights all the lines properly' do - expect(lines[4]).to eq(%Q{<span id="LC5" class="line" lang="ruby"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) - expect(lines[21]).to eq(%Q{<span id="LC22" class="line" lang="ruby"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) - expect(lines[26]).to eq(%Q{<span id="LC27" class="line" lang="ruby"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) - end - - describe 'with CRLF' do - let(:branch) { 'crlf-diff' } - let(:blob) { repository.blob_at_branch(branch, path) } - let(:lines) do - Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff', 'files/whitespace') - end - - it 'strips extra LFs' do - expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>") - end - end - end - describe 'custom highlighting from .gitattributes' do let(:branch) { 'gitattributes' } let(:blob) { repository.blob_at_branch(branch, path) } @@ -59,6 +35,19 @@ describe Gitlab::Highlight, lib: true do end describe '#highlight' do + describe 'with CRLF' do + let(:branch) { 'crlf-diff' } + let(:path) { 'files/whitespace' } + let(:blob) { repository.blob_at_branch(branch, path) } + let(:lines) do + Gitlab::Highlight.highlight(blob.path, blob.data, repository: repository).lines + end + + it 'strips extra LFs' do + expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\" lang=\"plaintext\">test </span>") + end + end + it 'links dependencies via DependencyLinker' do expect(Gitlab::DependencyLinker).to receive(:link). with('file.name', 'Contents', anything).and_call_original diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 21296a36729..412eb33b35b 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -91,6 +91,7 @@ merge_request_diff: pipelines: - project - user +- stages - statuses - builds - trigger_requests @@ -104,9 +105,15 @@ pipelines: - artifacts - pipeline_schedule - merge_requests +stages: +- project +- pipeline +- statuses +- builds statuses: - project - pipeline +- stage - user - auto_canceled_by variables: diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 54ce8051f30..50ff6ecc1e0 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -92,6 +92,7 @@ Milestone: ProjectSnippet: - id - title +- description - content - author_id - project_id @@ -174,6 +175,7 @@ MergeRequestDiff: Ci::Pipeline: - id - project_id +- source - ref - sha - before_sha @@ -191,7 +193,13 @@ Ci::Pipeline: - lock_version - auto_canceled_by_id - pipeline_schedule_id -- source +Ci::Stage: +- id +- name +- project_id +- pipeline_id +- created_at +- updated_at CommitStatus: - id - project_id @@ -213,6 +221,7 @@ CommitStatus: - stage - trigger_request_id - stage_idx +- stage_id - tag - ref - user_id diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index f4aab429931..a0eda685ca3 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::LDAP::User, lib: true do end it "does not mark existing ldap user as changed" do - create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', ldap_email: true) + create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', external_email: true, email_provider: 'ldapmain') expect(ldap_user.changed?).to be_falsey end end @@ -141,8 +141,12 @@ describe Gitlab::LDAP::User, lib: true do expect(ldap_user.gl_user.email).to eq(info[:email]) end - it "has ldap_email set to true" do - expect(ldap_user.gl_user.ldap_email?).to be(true) + it "has external_email set to true" do + expect(ldap_user.gl_user.external_email?).to be(true) + end + + it "has email_provider set to provider" do + expect(ldap_user.gl_user.email_provider).to eql 'ldapmain' end end @@ -155,8 +159,8 @@ describe Gitlab::LDAP::User, lib: true do expect(ldap_user.gl_user.temp_oauth_email?).to be(true) end - it "has ldap_email set to false" do - expect(ldap_user.gl_user.ldap_email?).to be(false) + it "has external_email set to false" do + expect(ldap_user.gl_user.external_email?).to be(false) end end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 208a8d028cd..5a87b906609 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::Metrics do + include StubENV + describe '.settings' do it 'returns a Hash' do expect(described_class.settings).to be_an_instance_of(Hash) @@ -9,7 +11,19 @@ describe Gitlab::Metrics do describe '.enabled?' do it 'returns a boolean' do - expect([true, false].include?(described_class.enabled?)).to eq(true) + expect(described_class.enabled?).to be_in([true, false]) + end + end + + describe '.prometheus_metrics_enabled?' do + it 'returns a boolean' do + expect(described_class.prometheus_metrics_enabled?).to be_in([true, false]) + end + end + + describe '.influx_metrics_enabled?' do + it 'returns a boolean' do + expect(described_class.influx_metrics_enabled?).to be_in([true, false]) end end @@ -177,4 +191,133 @@ describe Gitlab::Metrics do end end end + + shared_examples 'prometheus metrics API' do + describe '#counter' do + subject { described_class.counter(:couter, 'doc') } + + describe '#increment' do + it 'successfully calls #increment without arguments' do + expect { subject.increment }.not_to raise_exception + end + + it 'successfully calls #increment with 1 argument' do + expect { subject.increment({}) }.not_to raise_exception + end + + it 'successfully calls #increment with 2 arguments' do + expect { subject.increment({}, 1) }.not_to raise_exception + end + end + end + + describe '#summary' do + subject { described_class.summary(:summary, 'doc') } + + describe '#observe' do + it 'successfully calls #observe with 2 arguments' do + expect { subject.observe({}, 2) }.not_to raise_exception + end + end + end + + describe '#gauge' do + subject { described_class.gauge(:gauge, 'doc') } + + describe '#set' do + it 'successfully calls #set with 2 arguments' do + expect { subject.set({}, 1) }.not_to raise_exception + end + end + end + + describe '#histogram' do + subject { described_class.histogram(:histogram, 'doc') } + + describe '#observe' do + it 'successfully calls #observe with 2 arguments' do + expect { subject.observe({}, 2) }.not_to raise_exception + end + end + end + end + + context 'prometheus metrics disabled' do + before do + allow(described_class).to receive(:prometheus_metrics_enabled?).and_return(false) + end + + it_behaves_like 'prometheus metrics API' + + describe '#null_metric' do + subject { described_class.provide_metric(:test) } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#counter' do + subject { described_class.counter(:counter, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#summary' do + subject { described_class.summary(:summary, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#gauge' do + subject { described_class.gauge(:gauge, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#histogram' do + subject { described_class.histogram(:histogram, 'doc') } + + it { is_expected.to be_a(Gitlab::Metrics::NullMetric) } + end + end + + context 'prometheus metrics enabled' do + let(:metrics_multiproc_dir) { Dir.mktmpdir } + + before do + stub_const('Prometheus::Client::Multiprocdir', metrics_multiproc_dir) + allow(described_class).to receive(:prometheus_metrics_enabled?).and_return(true) + end + + it_behaves_like 'prometheus metrics API' + + describe '#null_metric' do + subject { described_class.provide_metric(:test) } + + it { is_expected.to be_nil } + end + + describe '#counter' do + subject { described_class.counter(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#summary' do + subject { described_class.summary(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#gauge' do + subject { described_class.gauge(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } + end + + describe '#histogram' do + subject { described_class.histogram(:name, 'doc') } + + it { is_expected.not_to be_a(Gitlab::Metrics::NullMetric) } + end + end end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 828c953197d..8943d1aa488 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -28,11 +28,11 @@ describe Gitlab::OAuth::User, lib: true do end end - describe '#save' do - def stub_omniauth_config(messages) - allow(Gitlab.config.omniauth).to receive_messages(messages) - end + def stub_omniauth_config(messages) + allow(Gitlab.config.omniauth).to receive_messages(messages) + end + describe '#save' do def stub_ldap_config(messages) allow(Gitlab::LDAP::Config).to receive_messages(messages) end @@ -377,4 +377,40 @@ describe Gitlab::OAuth::User, lib: true do end end end + + describe 'updating email' do + let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') } + + before do + stub_omniauth_config(sync_email_from_provider: 'my-provider') + end + + context "when provider sets an email" do + it "updates the user email" do + expect(gl_user.email).to eq(info_hash[:email]) + end + + it "has external_email set to true" do + expect(gl_user.external_email?).to be(true) + end + + it "has email_provider set to provider" do + expect(gl_user.email_provider).to eql 'my-provider' + end + end + + context "when provider doesn't set an email" do + before do + info_hash.delete(:email) + end + + it "does not update the user email" do + expect(gl_user.email).not_to eq(info_hash[:email]) + end + + it "has external_email set to false" do + expect(gl_user.external_email?).to be(false) + end + end + end end diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb new file mode 100644 index 00000000000..109559bb01c --- /dev/null +++ b/spec/lib/gitlab/uploads_transfer_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Gitlab::UploadsTransfer do + it 'leaves avatar uploads where they are' do + project_with_avatar = create(:empty_project, :with_avatar) + + described_class.new.rename_namespace('project', 'project-renamed') + + expect(File.exist?(project_with_avatar.avatar.path)).to be_truthy + end +end diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb index 3fe8cf43934..e8a37e8d77b 100644 --- a/spec/lib/gitlab/url_builder_spec.rb +++ b/spec/lib/gitlab/url_builder_spec.rb @@ -97,6 +97,17 @@ describe Gitlab::UrlBuilder, lib: true do end end + context 'on a PersonalSnippet' do + it 'returns a proper URL' do + personal_snippet = create(:personal_snippet) + note = build_stubbed(:note_on_personal_snippet, noteable: personal_snippet) + + url = described_class.build(note) + + expect(url).to eq "#{Settings.gitlab['url']}/snippets/#{note.noteable_id}#note_#{note.id}" + end + end + context 'on another object' do it 'returns a proper URL' do project = build_stubbed(:empty_project) diff --git a/spec/migrations/clean_upload_symlinks_spec.rb b/spec/migrations/clean_upload_symlinks_spec.rb new file mode 100644 index 00000000000..cecb3ddac53 --- /dev/null +++ b/spec/migrations/clean_upload_symlinks_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170406111121_clean_upload_symlinks.rb') + +describe CleanUploadSymlinks do + let(:migration) { described_class.new } + let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_uploads_test") } + let(:uploads_dir) { File.join(test_dir, "public", "uploads") } + let(:new_uploads_dir) { File.join(uploads_dir, "system") } + let(:original_path) { File.join(new_uploads_dir, 'user') } + let(:symlink_path) { File.join(uploads_dir, 'user') } + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + allow(migration).to receive(:base_directory).and_return(test_dir) + allow(migration).to receive(:say) + end + + describe "#up" do + before do + FileUtils.mkdir_p(original_path) + FileUtils.ln_s(original_path, symlink_path) + end + + it 'removes the symlink' do + migration.up + + expect(File.symlink?(symlink_path)).to be(false) + end + end + + describe '#down' do + before do + FileUtils.mkdir_p(File.join(original_path)) + FileUtils.touch(File.join(original_path, 'dummy.file')) + end + + it 'creates a symlink' do + expected_path = File.join(symlink_path, "dummy.file") + migration.down + + expect(File.exist?(expected_path)).to be(true) + expect(File.symlink?(symlink_path)).to be(true) + end + end +end diff --git a/spec/migrations/migrate_build_stage_reference_spec.rb b/spec/migrations/migrate_build_stage_reference_spec.rb new file mode 100644 index 00000000000..80b321860c2 --- /dev/null +++ b/spec/migrations/migrate_build_stage_reference_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170526185921_migrate_build_stage_reference.rb') + +describe MigrateBuildStageReference, :migration do + ## + # Create test data - pipeline and CI/CD jobs. + # + + let(:jobs) { table(:ci_builds) } + let(:stages) { table(:ci_stages) } + let(:pipelines) { table(:ci_pipelines) } + let(:projects) { table(:projects) } + + before do + # Create projects + # + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 456, name: 'gitlab2', path: 'gitlab2') + + # Create CI/CD pipelines + # + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + pipelines.create!(id: 2, project_id: 456, ref: 'feature', sha: '21a3deb') + + # Create CI/CD jobs + # + jobs.create!(id: 1, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 2, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 3, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + jobs.create!(id: 4, commit_id: 1, project_id: 123, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 5, commit_id: 2, project_id: 456, stage_idx: 2, stage: 'test:2') + jobs.create!(id: 6, commit_id: 2, project_id: 456, stage_idx: 1, stage: 'test:1') + jobs.create!(id: 7, commit_id: 2, project_id: 456, stage_idx: 1, stage: 'test:1') + jobs.create!(id: 8, commit_id: 3, project_id: 789, stage_idx: 3, stage: 'deploy') + + # Create CI/CD stages + # + stages.create(id: 101, pipeline_id: 1, project_id: 123, name: 'test') + stages.create(id: 102, pipeline_id: 1, project_id: 123, name: 'build') + stages.create(id: 103, pipeline_id: 1, project_id: 123, name: 'deploy') + stages.create(id: 104, pipeline_id: 2, project_id: 456, name: 'test:1') + stages.create(id: 105, pipeline_id: 2, project_id: 456, name: 'test:2') + stages.create(id: 106, pipeline_id: 2, project_id: 456, name: 'deploy') + end + + it 'correctly migrate build stage references' do + expect(jobs.where(stage_id: nil).count).to eq 8 + + migrate! + + expect(jobs.where(stage_id: nil).count).to eq 1 + + expect(jobs.find(1).stage_id).to eq 102 + expect(jobs.find(2).stage_id).to eq 102 + expect(jobs.find(3).stage_id).to eq 101 + expect(jobs.find(4).stage_id).to eq 103 + expect(jobs.find(5).stage_id).to eq 105 + expect(jobs.find(6).stage_id).to eq 104 + expect(jobs.find(7).stage_id).to eq 104 + expect(jobs.find(8).stage_id).to eq nil + end +end diff --git a/spec/migrations/migrate_pipeline_stages_spec.rb b/spec/migrations/migrate_pipeline_stages_spec.rb new file mode 100644 index 00000000000..c47f2bb8ff9 --- /dev/null +++ b/spec/migrations/migrate_pipeline_stages_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170526185842_migrate_pipeline_stages.rb') + +describe MigratePipelineStages, :migration do + ## + # Create test data - pipeline and CI/CD jobs. + # + + let(:jobs) { table(:ci_builds) } + let(:stages) { table(:ci_stages) } + let(:pipelines) { table(:ci_pipelines) } + let(:projects) { table(:projects) } + + before do + # Create projects + # + projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1') + projects.create!(id: 456, name: 'gitlab2', path: 'gitlab2') + + # Create CI/CD pipelines + # + pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') + pipelines.create!(id: 2, project_id: 456, ref: 'feature', sha: '21a3deb') + + # Create CI/CD jobs + # + jobs.create!(id: 1, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 2, commit_id: 1, project_id: 123, stage_idx: 2, stage: 'build') + jobs.create!(id: 3, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + jobs.create!(id: 4, commit_id: 1, project_id: 123, stage_idx: 1, stage: 'test') + jobs.create!(id: 5, commit_id: 1, project_id: 123, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 6, commit_id: 2, project_id: 456, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 7, commit_id: 2, project_id: 456, stage_idx: 2, stage: 'test:2') + jobs.create!(id: 8, commit_id: 2, project_id: 456, stage_idx: 1, stage: 'test:1') + jobs.create!(id: 9, commit_id: 2, project_id: 456, stage_idx: 1, stage: 'test:1') + jobs.create!(id: 10, commit_id: 2, project_id: 456, stage_idx: 2, stage: 'test:2') + jobs.create!(id: 11, commit_id: 3, project_id: 456, stage_idx: 3, stage: 'deploy') + jobs.create!(id: 12, commit_id: 2, project_id: 789, stage_idx: 3, stage: 'deploy') + end + + it 'correctly migrates pipeline stages' do + expect(stages.count).to be_zero + + migrate! + + expect(stages.count).to eq 6 + expect(stages.all.pluck(:name)) + .to match_array %w[test build deploy test:1 test:2 deploy] + expect(stages.where(pipeline_id: 1).order(:id).pluck(:name)) + .to eq %w[test build deploy] + expect(stages.where(pipeline_id: 2).order(:id).pluck(:name)) + .to eq %w[test:1 test:2 deploy] + expect(stages.where(pipeline_id: 3).count).to be_zero + expect(stages.where(project_id: 789).count).to be_zero + end +end diff --git a/spec/migrations/move_uploads_to_system_dir_spec.rb b/spec/migrations/move_uploads_to_system_dir_spec.rb new file mode 100644 index 00000000000..37d66452447 --- /dev/null +++ b/spec/migrations/move_uploads_to_system_dir_spec.rb @@ -0,0 +1,68 @@ +require "spec_helper" +require Rails.root.join("db", "migrate", "20170316163845_move_uploads_to_system_dir.rb") + +describe MoveUploadsToSystemDir do + let(:migration) { described_class.new } + let(:test_dir) { File.join(Rails.root, "tmp", "move_uploads_test") } + let(:uploads_dir) { File.join(test_dir, "public", "uploads") } + let(:new_uploads_dir) { File.join(uploads_dir, "system") } + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + allow(migration).to receive(:base_directory).and_return(test_dir) + allow(migration).to receive(:say) + end + + describe "#up" do + before do + FileUtils.mkdir_p(File.join(uploads_dir, 'user')) + FileUtils.touch(File.join(uploads_dir, 'user', 'dummy.file')) + end + + it 'moves the directory to the new path' do + expected_path = File.join(new_uploads_dir, 'user', 'dummy.file') + + migration.up + + expect(File.exist?(expected_path)).to be(true) + end + + it 'creates a symlink in the old location' do + symlink_path = File.join(uploads_dir, 'user') + expected_path = File.join(symlink_path, 'dummy.file') + + migration.up + + expect(File.exist?(expected_path)).to be(true) + expect(File.symlink?(symlink_path)).to be(true) + end + end + + describe "#down" do + before do + FileUtils.mkdir_p(File.join(new_uploads_dir, 'user')) + FileUtils.touch(File.join(new_uploads_dir, 'user', 'dummy.file')) + end + + it 'moves the directory to the old path' do + expected_path = File.join(uploads_dir, 'user', 'dummy.file') + + migration.down + + expect(File.exist?(expected_path)).to be(true) + end + + it 'removes the symlink if it existed' do + FileUtils.ln_s(File.join(new_uploads_dir, 'user'), File.join(uploads_dir, 'user')) + + directory = File.join(uploads_dir, 'user') + expected_path = File.join(directory, 'dummy.file') + + migration.down + + expect(File.exist?(expected_path)).to be(true) + expect(File.symlink?(directory)).to be(false) + end + end +end diff --git a/spec/migrations/rename_system_namespaces_spec.rb b/spec/migrations/rename_system_namespaces_spec.rb new file mode 100644 index 00000000000..626a6005838 --- /dev/null +++ b/spec/migrations/rename_system_namespaces_spec.rb @@ -0,0 +1,254 @@ +require "spec_helper" +require Rails.root.join("db", "migrate", "20170316163800_rename_system_namespaces.rb") + +describe RenameSystemNamespaces, truncate: true do + let(:migration) { described_class.new } + let(:test_dir) { File.join(Rails.root, "tmp", "tests", "rename_namespaces_test") } + let(:uploads_dir) { File.join(test_dir, "public", "uploads") } + let(:system_namespace) do + namespace = build(:namespace, path: "system") + namespace.save(validate: false) + namespace + end + + def save_invalid_routable(routable) + routable.__send__(:prepare_route) + routable.save(validate: false) + end + + before do + FileUtils.remove_dir(test_dir) if File.directory?(test_dir) + FileUtils.mkdir_p(uploads_dir) + FileUtils.remove_dir(TestEnv.repos_path) if File.directory?(TestEnv.repos_path) + allow(migration).to receive(:say) + allow(migration).to receive(:uploads_dir).and_return(uploads_dir) + end + + describe "#system_namespace" do + it "only root namespaces called with path `system`" do + system_namespace + system_namespace_with_parent = build(:namespace, path: 'system', parent: create(:namespace)) + system_namespace_with_parent.save(validate: false) + + expect(migration.system_namespace.id).to eq(system_namespace.id) + end + end + + describe "#up" do + before do + system_namespace + end + + it "doesn't break if there are no namespaces called system" do + Namespace.delete_all + + migration.up + end + + it "renames namespaces called system" do + migration.up + + expect(system_namespace.reload.path).to eq("system0") + end + + it "renames the route to the namespace" do + migration.up + + expect(system_namespace.reload.full_path).to eq("system0") + end + + it "renames the route for projects of the namespace" do + project = build(:project, path: "project-path", namespace: system_namespace) + save_invalid_routable(project) + + migration.up + + expect(project.route.reload.path).to eq("system0/project-path") + end + + it "doesn't touch routes of namespaces that look like system" do + namespace = create(:group, path: 'systemlookalike') + project = create(:project, namespace: namespace, path: 'the-project') + + migration.up + + expect(project.route.reload.path).to eq('systemlookalike/the-project') + expect(namespace.route.reload.path).to eq('systemlookalike') + end + + it "moves the the repository for a project in the namespace" do + project = build(:project, namespace: system_namespace, path: "system-project") + save_invalid_routable(project) + TestEnv.copy_repo(project, + bare_repo: TestEnv.factory_repo_path_bare, + refs: TestEnv::BRANCH_SHA) + expected_repo = File.join(TestEnv.repos_path, "system0", "system-project.git") + + migration.up + + expect(File.directory?(expected_repo)).to be(true) + end + + it "moves the uploads for the namespace" do + allow(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0") + expect(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0") + + migration.up + end + + it "moves the pages for the namespace" do + allow(migration).to receive(:move_namespace_folders).with(uploads_dir, "system", "system0") + expect(migration).to receive(:move_namespace_folders).with(Settings.pages.path, "system", "system0") + + migration.up + end + + describe "clears the markdown cache for projects in the system namespace" do + let!(:project) do + project = build(:project, namespace: system_namespace) + save_invalid_routable(project) + project + end + + it 'removes description_html from projects' do + migration.up + + expect(project.reload.description_html).to be_nil + end + + it 'removes issue descriptions' do + issue = create(:issue, project: project, description_html: 'Issue description') + + migration.up + + expect(issue.reload.description_html).to be_nil + end + + it 'removes merge request descriptions' do + merge_request = create(:merge_request, + source_project: project, + target_project: project, + description_html: 'MergeRequest description') + + migration.up + + expect(merge_request.reload.description_html).to be_nil + end + + it 'removes note html' do + note = create(:note, + project: project, + noteable: create(:issue, project: project), + note_html: 'note description') + + migration.up + + expect(note.reload.note_html).to be_nil + end + + it 'removes milestone description' do + milestone = create(:milestone, + project: project, + description_html: 'milestone description') + + migration.up + + expect(milestone.reload.description_html).to be_nil + end + end + + context "system namespace -> subgroup -> system0 project" do + it "updates the route of the project correctly" do + subgroup = build(:group, path: "subgroup", parent: system_namespace) + save_invalid_routable(subgroup) + project = build(:project, path: "system0", namespace: subgroup) + save_invalid_routable(project) + + migration.up + + expect(project.route.reload.path).to eq("system0/subgroup/system0") + end + end + end + + describe "#move_repositories" do + let(:namespace) { create(:group, name: "hello-group") } + it "moves a project for a namespace" do + create(:project, namespace: namespace, path: "hello-project") + expected_path = File.join(TestEnv.repos_path, "bye-group", "hello-project.git") + + migration.move_repositories(namespace, "hello-group", "bye-group") + + expect(File.directory?(expected_path)).to be(true) + end + + it "moves a namespace in a subdirectory correctly" do + child_namespace = create(:group, name: "sub-group", parent: namespace) + create(:project, namespace: child_namespace, path: "hello-project") + + expected_path = File.join(TestEnv.repos_path, "hello-group", "renamed-sub-group", "hello-project.git") + + migration.move_repositories(child_namespace, "hello-group/sub-group", "hello-group/renamed-sub-group") + + expect(File.directory?(expected_path)).to be(true) + end + + it "moves a parent namespace with subdirectories" do + child_namespace = create(:group, name: "sub-group", parent: namespace) + create(:project, namespace: child_namespace, path: "hello-project") + expected_path = File.join(TestEnv.repos_path, "renamed-group", "sub-group", "hello-project.git") + + migration.move_repositories(child_namespace, "hello-group", "renamed-group") + + expect(File.directory?(expected_path)).to be(true) + end + end + + describe "#move_namespace_folders" do + it "moves a namespace with files" do + source = File.join(uploads_dir, "parent-group", "sub-group") + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, "parent-group", "moved-group") + FileUtils.touch(File.join(source, "test.txt")) + expected_file = File.join(destination, "test.txt") + + migration.move_namespace_folders(uploads_dir, File.join("parent-group", "sub-group"), File.join("parent-group", "moved-group")) + + expect(File.exist?(expected_file)).to be(true) + end + + it "moves a parent namespace uploads" do + source = File.join(uploads_dir, "parent-group", "sub-group") + FileUtils.mkdir_p(source) + destination = File.join(uploads_dir, "moved-parent", "sub-group") + FileUtils.touch(File.join(source, "test.txt")) + expected_file = File.join(destination, "test.txt") + + migration.move_namespace_folders(uploads_dir, "parent-group", "moved-parent") + + expect(File.exist?(expected_file)).to be(true) + end + end + + describe "#child_ids_for_parent" do + it "collects child ids for all levels" do + parent = create(:group) + first_child = create(:group, parent: parent) + second_child = create(:group, parent: parent) + third_child = create(:group, parent: second_child) + all_ids = [parent.id, first_child.id, second_child.id, third_child.id] + + collected_ids = migration.child_ids_for_parent(parent, ids: [parent.id]) + + expect(collected_ids).to contain_exactly(*all_ids) + end + end + + describe "#remove_last_ocurrence" do + it "removes only the last occurance of a string" do + input = "this/is/system/namespace/with/system" + + expect(migration.remove_last_occurrence(input, "system")).to eq("this/is/system/namespace/with/") + end + end +end diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb new file mode 100644 index 00000000000..7df44515424 --- /dev/null +++ b/spec/migrations/update_upload_paths_to_system_spec.rb @@ -0,0 +1,53 @@ +require "spec_helper" +require Rails.root.join("db", "post_migrate", "20170317162059_update_upload_paths_to_system.rb") + +describe UpdateUploadPathsToSystem do + let(:migration) { described_class.new } + + before do + allow(migration).to receive(:say) + end + + describe "#uploads_to_switch_to_new_path" do + it "contains only uploads with the old path for the correct models" do + _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") + _upload_with_system_path = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg") + _upload_with_other_path = create(:upload, model: create(:empty_project), path: "thelongsecretforafileupload/avatar.jpg") + old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg") + group_upload = create(:upload, model: create(:group), path: "uploads/group/avatar.jpg") + + expect(Upload.where(migration.uploads_to_switch_to_new_path)).to contain_exactly(old_upload, group_upload) + end + end + + describe "#uploads_to_switch_to_old_path" do + it "contains only uploads with the new path for the correct models" do + _upload_for_other_type = create(:upload, model: create(:ci_pipeline), path: "uploads/ci_pipeline/avatar.jpg") + upload_with_system_path = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg") + _upload_with_other_path = create(:upload, model: create(:empty_project), path: "thelongsecretforafileupload/avatar.jpg") + _old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg") + + expect(Upload.where(migration.uploads_to_switch_to_old_path)).to contain_exactly(upload_with_system_path) + end + end + + describe "#up", truncate: true do + it "updates old upload records to the new path" do + old_upload = create(:upload, model: create(:empty_project), path: "uploads/project/avatar.jpg") + + migration.up + + expect(old_upload.reload.path).to eq("uploads/system/project/avatar.jpg") + end + end + + describe "#down", truncate: true do + it "updates the new system patsh to the old paths" do + new_upload = create(:upload, model: create(:empty_project), path: "uploads/system/project/avatar.jpg") + + migration.down + + expect(new_upload.reload.path).to eq("uploads/project/avatar.jpg") + end + end +end diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index f19e1af65a6..e1193e0d19a 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -199,6 +199,14 @@ describe Blob do end end + describe '#file_type' do + it 'returns the file type' do + blob = fake_blob(path: 'README.md') + + expect(blob.file_type).to eq(:readme) + end + end + describe '#simple_viewer' do context 'when the blob is empty' do it 'returns an empty viewer' do diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb index d56379eb59d..574438838d8 100644 --- a/spec/models/blob_viewer/base_spec.rb +++ b/spec/models/blob_viewer/base_spec.rb @@ -106,9 +106,9 @@ describe BlobViewer::Base, model: true do end describe '#render_error' do - context 'when expanded' do + context 'when the blob is expanded' do before do - viewer.expanded = true + blob.expand! end context 'when the blob size is larger than the size limit' do diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb index 8f6ab908987..48116c7e701 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/legacy_stage_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::Stage, models: true do +describe Ci::LegacyStage, :models do let(:stage) { build(:ci_stage) } let(:pipeline) { stage.pipeline } let(:stage_name) { stage.name } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index ae1b01b76ab..b50c7700bd3 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -224,8 +224,19 @@ describe Ci::Pipeline, models: true do status: 'success') end - describe '#stages' do - subject { pipeline.stages } + describe '#stage_seeds' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: { script: 'rake' } }) + end + + it 'returns preseeded stage seeds object' do + expect(pipeline.stage_seeds).to all(be_a Gitlab::Ci::Stage::Seed) + expect(pipeline.stage_seeds.count).to eq 1 + end + end + + describe '#legacy_stages' do + subject { pipeline.legacy_stages } context 'stages list' do it 'returns ordered list of stages' do @@ -274,7 +285,7 @@ describe Ci::Pipeline, models: true do end it 'populates stage with correct number of warnings' do - deploy_stage = pipeline.stages.third + deploy_stage = pipeline.legacy_stages.third expect(deploy_stage).not_to receive(:statuses) expect(deploy_stage).to have_warnings @@ -288,22 +299,22 @@ describe Ci::Pipeline, models: true do end end - describe '#stages_name' do + describe '#stages_names' do it 'returns a valid names of stages' do - expect(pipeline.stages_name).to eq(%w(build test deploy)) + expect(pipeline.stages_names).to eq(%w(build test deploy)) end end end - describe '#stage' do - subject { pipeline.stage('test') } + describe '#legacy_stage' do + subject { pipeline.legacy_stage('test') } context 'with status in stage' do before do create(:commit_status, pipeline: pipeline, stage: 'test') end - it { expect(subject).to be_a Ci::Stage } + it { expect(subject).to be_a Ci::LegacyStage } it { expect(subject.name).to eq 'test' } it { expect(subject.statuses).not_to be_empty } end @@ -524,6 +535,20 @@ describe Ci::Pipeline, models: true do end end + describe '#has_stage_seeds?' do + context 'when pipeline has stage seeds' do + subject { build(:ci_pipeline_with_one_job) } + + it { is_expected.to have_stage_seeds } + end + + context 'when pipeline does not have stage seeds' do + subject { create(:ci_pipeline_without_jobs) } + + it { is_expected.not_to have_stage_seeds } + end + end + describe '#has_warnings?' do subject { pipeline.has_warnings? } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 72f83d63224..ba247dcc5cf 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -20,8 +20,8 @@ describe Commit, models: true do end it 'caches the author' do + allow(RequestStore).to receive(:active?).and_return(true) user = create(:user, email: commit.author_email) - expect(RequestStore).to receive(:active?).twice.and_return(true) expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original expect(commit.author).to eq(user) @@ -67,11 +67,11 @@ describe Commit, models: true do expect(commit.title).to eq("--no commit message") end - it "truncates a message without a newline at 80 characters" do + it 'truncates a message without a newline at natural break to 80 characters' do message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' allow(commit).to receive(:safe_message).and_return(message) - expect(commit.title).to eq("#{message[0..79]}…") + expect(commit.title).to eq('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis…') end it "truncates a message with a newline before 80 characters at the newline" do @@ -113,6 +113,28 @@ eos end end + describe 'description' do + it 'returns description of commit message if title less than 100 characters' do + message = <<eos +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. +Vivamus egestas lacinia lacus, sed rutrum mauris. +eos + + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.description).to eq('Vivamus egestas lacinia lacus, sed rutrum mauris.') + end + + it 'returns full commit message if commit title more than 100 characters' do + message = <<eos +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris. +Vivamus egestas lacinia lacus, sed rutrum mauris. +eos + + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.description).to eq(message) + end + end + describe "delegation" do subject { commit } diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 0e10d91836d..65f05121b40 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -122,16 +122,7 @@ describe Group, 'Routable' do it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.full_path}/#{nested_group.path}") } - context 'with RequestStore active' do - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - + context 'with RequestStore active', :request_store do it 'does not load the route table more than once' do expect(group).to receive(:uncached_full_path).once.and_call_original diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index 454550c9710..6e8d43f988c 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe ForkedProjectLink, "add link on fork" do let(:project_from) { create(:project, :repository) } - let(:namespace) { create(:namespace) } - let(:user) { create(:user, namespace: namespace) } + let(:user) { create(:user) } + let(:namespace) { user.namespace } before do create(:project_member, :reporter, user: user, project: project_from) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 316bf153660..3d437ca0fcc 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -179,7 +179,7 @@ describe Group, models: true do let!(:group) { create(:group, :access_requestable, :with_avatar) } let(:user) { create(:user) } let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" } - let(:avatar_path) { "/uploads/group/avatar/#{group.id}/dk.png" } + let(:avatar_path) { "/uploads/system/group/avatar/#{group.id}/dk.png" } context 'when avatar file is uploaded' do before { group.add_master(user) } diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 0a10ee01506..ed9fde57bf7 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -139,4 +139,15 @@ describe MergeRequestDiff, models: true do expect(subject.commits_count).to eq 2 end end + + describe '#utf8_st_diffs' do + it 'does not raise error when a hash value is in binary' do + subject.st_diffs = [ + { diff: "\0" }, + { diff: "\x05\x00\x68\x65\x6c\x6c\x6f" } + ] + + expect { subject.utf8_st_diffs }.not_to raise_error + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 0e74f1ab1bd..145c7ad5770 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -43,6 +43,12 @@ describe Namespace, models: true do end end + context "is case insensitive" do + let(:group) { build(:group, path: "System") } + + it { expect(group).not_to be_valid } + end + context 'top-level group' do let(:group) { build(:group, path: 'tree') } @@ -178,8 +184,8 @@ describe Namespace, models: true do let(:parent) { create(:group, name: 'parent', path: 'parent') } let(:child) { create(:group, name: 'child', path: 'child', parent: parent) } let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child) } - let(:uploads_dir) { File.join(CarrierWave.root, 'uploads') } - let(:pages_dir) { TestEnv.pages_path } + let(:uploads_dir) { File.join(CarrierWave.root, FileUploader.base_dir) } + let(:pages_dir) { File.join(TestEnv.pages_path) } before do FileUtils.mkdir_p(File.join(uploads_dir, 'parent', 'child', 'the-project')) diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index c6c45d78990..f9d060d4e0e 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -6,7 +6,7 @@ describe PagesDomain, models: true do end describe 'validate domain' do - subject { build(:pages_domain, domain: domain) } + subject(:pages_domain) { build(:pages_domain, domain: domain) } context 'is unique' do let(:domain) { 'my.domain.com' } @@ -14,36 +14,25 @@ describe PagesDomain, models: true do it { is_expected.to validate_uniqueness_of(:domain) } end - context 'valid domain' do - let(:domain) { 'my.domain.com' } - - it { is_expected.to be_valid } - end - - context 'valid hexadecimal-looking domain' do - let(:domain) { '0x12345.com'} - - it { is_expected.to be_valid } - end - - context 'no domain' do - let(:domain) { nil } - - it { is_expected.not_to be_valid } - end - - context 'invalid domain' do - let(:domain) { '0123123' } - - it { is_expected.not_to be_valid } - end - - context 'domain from .example.com' do - let(:domain) { 'my.domain.com' } - - before { allow(Settings.pages).to receive(:host).and_return('domain.com') } - - it { is_expected.not_to be_valid } + { + 'my.domain.com' => true, + '123.456.789' => true, + '0x12345.com' => true, + '0123123' => true, + '_foo.com' => false, + 'reserved.com' => false, + 'a.reserved.com' => false, + nil => false + }.each do |value, validity| + context "domain #{value.inspect} validity" do + before do + allow(Settings.pages).to receive(:host).and_return('reserved.com') + end + + let(:domain) { value } + + it { expect(pages_domain.valid?).to eq(validity) } + end end end diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index 823623d96fa..fa781195608 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -35,6 +35,16 @@ describe PersonalAccessToken, models: true do end end + describe 'revoke!' do + let(:active_personal_access_token) { create(:personal_access_token) } + + it 'revokes the token' do + active_personal_access_token.revoke! + + expect(active_personal_access_token.revoked?).to be true + end + end + context "validations" do let(:personal_access_token) { build(:personal_access_token) } @@ -51,11 +61,17 @@ describe PersonalAccessToken, models: true do expect(personal_access_token).to be_valid end - it "rejects creating a token with non-API scopes" do + it "allows creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).to be_valid + end + + it "rejects creating a token with unavailable scopes" do personal_access_token.scopes = [:openid, :api] expect(personal_access_token).not_to be_valid - expect(personal_access_token.errors[:scopes].first).to eq "can only contain API scopes" + expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes" end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3ed52d42f86..454eeb58ecd 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -812,7 +812,7 @@ describe Project, models: true do context 'when avatar file is uploaded' do let(:project) { create(:empty_project, :with_avatar) } - let(:avatar_path) { "/uploads/project/avatar/#{project.id}/dk.png" } + let(:avatar_path) { "/uploads/system/project/avatar/#{project.id}/dk.png" } let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" } it 'shows correct url' do diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 362565506e5..497e3cdf415 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -389,16 +389,7 @@ describe ProjectTeam, models: true do end describe '#max_member_access_for_user_ids' do - context 'with RequestStore enabled' do - before do - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! - end - + context 'with RequestStore enabled', :request_store do include_examples 'max member access for users' def access_levels(users) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 718b7d5e86b..a6d4d92c450 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1905,19 +1905,43 @@ describe Repository, models: true do end describe '#is_ancestor?' do - context 'Gitaly is_ancestor feature enabled' do - let(:commit) { repository.commit } - let(:ancestor) { commit.parents.first } + let(:commit) { repository.commit } + let(:ancestor) { commit.parents.first } + context 'with Gitaly enabled' do + it 'it is an ancestor' do + expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true) + end + + it 'it is not an ancestor' do + expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false) + end + + it 'returns false on nil-values' do + expect(repository.is_ancestor?(nil, commit.id)).to eq(false) + expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false) + expect(repository.is_ancestor?(nil, nil)).to eq(false) + end + end + + context 'with Gitaly disabled' do before do - allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(true) - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true) + allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false) + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false) end - it "asks Gitaly server if it's an ancestor" do - expect_any_instance_of(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).with(ancestor.id, commit.id) + it 'it is an ancestor' do + expect(repository.is_ancestor?(ancestor.id, commit.id)).to eq(true) + end + + it 'it is not an ancestor' do + expect(repository.is_ancestor?(commit.id, ancestor.id)).to eq(false) + end - repository.is_ancestor?(ancestor.id, commit.id) + it 'returns false on nil-values' do + expect(repository.is_ancestor?(nil, commit.id)).to eq(false) + expect(repository.is_ancestor?(ancestor.id, nil)).to eq(false) + expect(repository.is_ancestor?(nil, nil)).to eq(false) end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1c3541da44f..d5bd9946ab6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -13,6 +13,10 @@ describe User, models: true do it { is_expected.to include_module(TokenAuthenticatable) } end + describe 'delegations' do + it { is_expected.to delegate_method(:path).to(:namespace).with_prefix } + end + describe 'associations' do it { is_expected.to have_one(:namespace) } it { is_expected.to have_many(:snippets).dependent(:destroy) } @@ -983,7 +987,7 @@ describe User, models: true do context 'when avatar file is uploaded' do let(:gitlab_host) { "http://#{Gitlab.config.gitlab.host}" } - let(:avatar_path) { "/uploads/user/avatar/#{user.id}/dk.png" } + let(:avatar_path) { "/uploads/system/user/avatar/#{user.id}/dk.png" } it 'shows correct avatar url' do expect(user.avatar_url).to eq(avatar_path) diff --git a/spec/policies/deploy_key_policy_spec.rb b/spec/policies/deploy_key_policy_spec.rb new file mode 100644 index 00000000000..28e10f0bfe2 --- /dev/null +++ b/spec/policies/deploy_key_policy_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe DeployKeyPolicy, models: true do + subject { described_class.abilities(current_user, deploy_key).to_set } + + describe 'updating a deploy_key' do + context 'when a regular user' do + let(:current_user) { create(:user) } + + context 'tries to update private deploy key attached to project' do + let(:deploy_key) { create(:deploy_key, public: false) } + let(:project) { create(:project_empty_repo) } + + before do + project.add_master(current_user) + project.deploy_keys << deploy_key + end + + it { is_expected.to include(:update_deploy_key) } + end + + context 'tries to update private deploy key attached to other project' do + let(:deploy_key) { create(:deploy_key, public: false) } + let(:other_project) { create(:project_empty_repo) } + + before do + other_project.deploy_keys << deploy_key + end + + it { is_expected.not_to include(:update_deploy_key) } + end + + context 'tries to update public deploy key' do + let(:deploy_key) { create(:another_deploy_key, public: true) } + + it { is_expected.not_to include(:update_deploy_key) } + end + end + + context 'when an admin user' do + let(:current_user) { create(:user, :admin) } + + context ' tries to update private deploy key' do + let(:deploy_key) { create(:deploy_key, public: false) } + + it { is_expected.to include(:update_deploy_key) } + end + + context 'when an admin user tries to update public deploy key' do + let(:deploy_key) { create(:another_deploy_key, public: true) } + + it { is_expected.to include(:update_deploy_key) } + end + end + end +end diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb index e1771b636b8..ddbed5f781e 100644 --- a/spec/policies/project_snippet_policy_spec.rb +++ b/spec/policies/project_snippet_policy_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe ProjectSnippetPolicy, models: true do let(:regular_user) { create(:user) } let(:external_user) { create(:user, :external) } - let(:project) { create(:empty_project) } + let(:project) { create(:empty_project, :public) } let(:author_permissions) do [ @@ -107,7 +107,7 @@ describe ProjectSnippetPolicy, models: true do end context 'snippet author' do - let(:snippet) { create(:project_snippet, :private, author: regular_user) } + let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) } subject { described_class.abilities(regular_user, snippet).to_set } diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 44720fc4448..f5a14b1d04d 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -132,6 +132,11 @@ describe MergeRequestPresenter do it 'does not present related issues links' do is_expected.not_to match("#{project.full_path}/issues/#{issue_b.iid}") end + + it 'appends status when closing issue is already closed' do + issue_a.close + is_expected.to match('(closed)') + end end describe '#mentioned_issues_links' do @@ -147,6 +152,11 @@ describe MergeRequestPresenter do it 'does not present closing issues links' do is_expected.not_to match("#{project.full_path}/issues/#{issue_a.iid}") end + + it 'appends status when mentioned issue is already closed' do + issue_b.close + is_expected.to match('(closed)') + end end describe '#assign_to_closing_issues_link' do diff --git a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb index 6443f86b6a1..5c39e1b5f96 100644 --- a/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb +++ b/spec/presenters/projects/settings/deploy_keys_presenter_spec.rb @@ -51,10 +51,6 @@ describe Projects::Settings::DeployKeysPresenter do expect(presenter.available_project_keys).not_to be_empty end - it 'returns false if any available_project_keys are enabled' do - expect(presenter.any_available_project_keys_enabled?).to eq(true) - end - it 'returns the available_project_keys size' do expect(presenter.available_project_keys_size).to eq(1) end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 843e9862b0c..4d9cd5f3a27 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -13,7 +13,7 @@ describe API::DeployKeys do describe 'GET /deploy_keys' do context 'when unauthenticated' do - it 'should return authentication error' do + it 'returns authentication error' do get api('/deploy_keys') expect(response.status).to eq(401) @@ -21,7 +21,7 @@ describe API::DeployKeys do end context 'when authenticated as non-admin user' do - it 'should return a 403 error' do + it 'returns a 403 error' do get api('/deploy_keys', user) expect(response.status).to eq(403) @@ -29,7 +29,7 @@ describe API::DeployKeys do end context 'when authenticated as admin' do - it 'should return all deploy keys' do + it 'returns all deploy keys' do get api('/deploy_keys', admin) expect(response.status).to eq(200) @@ -43,7 +43,7 @@ describe API::DeployKeys do describe 'GET /projects/:id/deploy_keys' do before { deploy_key } - it 'should return array of ssh keys' do + it 'returns array of ssh keys' do get api("/projects/#{project.id}/deploy_keys", admin) expect(response).to have_http_status(200) @@ -54,14 +54,14 @@ describe API::DeployKeys do end describe 'GET /projects/:id/deploy_keys/:key_id' do - it 'should return a single key' do + it 'returns a single key' do get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) expect(response).to have_http_status(200) expect(json_response['title']).to eq(deploy_key.title) end - it 'should return 404 Not Found with invalid ID' do + it 'returns 404 Not Found with invalid ID' do get api("/projects/#{project.id}/deploy_keys/404", admin) expect(response).to have_http_status(404) @@ -69,26 +69,26 @@ describe API::DeployKeys do end describe 'POST /projects/:id/deploy_keys' do - it 'should not create an invalid ssh key' do + it 'does not create an invalid ssh key' do post api("/projects/#{project.id}/deploy_keys", admin), { title: 'invalid key' } expect(response).to have_http_status(400) expect(json_response['error']).to eq('key is missing') end - it 'should not create a key without title' do + it 'does not create a key without title' do post api("/projects/#{project.id}/deploy_keys", admin), key: 'some key' expect(response).to have_http_status(400) expect(json_response['error']).to eq('title is missing') end - it 'should create new ssh key' do + it 'creates new ssh key' do key_attrs = attributes_for :another_key expect do post api("/projects/#{project.id}/deploy_keys", admin), key_attrs - end.to change{ project.deploy_keys.count }.by(1) + end.to change { project.deploy_keys.count }.by(1) end it 'returns an existing ssh key when attempting to add a duplicate' do @@ -117,10 +117,53 @@ describe API::DeployKeys do end end + describe 'PUT /projects/:id/deploy_keys/:key_id' do + let(:private_deploy_key) { create(:another_deploy_key, public: false) } + let(:project_private_deploy_key) do + create(:deploy_keys_project, project: project, deploy_key: private_deploy_key) + end + + it 'updates a public deploy key as admin' do + expect do + put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin), { title: 'new title' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(200) + end + + it 'does not update a public deploy key as non admin' do + expect do + put api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user), { title: 'new title' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(404) + end + + it 'does not update a private key with invalid title' do + project_private_deploy_key + + expect do + put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: '' } + end.not_to change(deploy_key, :title) + + expect(response).to have_http_status(400) + end + + it 'updates a private ssh key with correct attributes' do + project_private_deploy_key + + put api("/projects/#{project.id}/deploy_keys/#{private_deploy_key.id}", admin), { title: 'new title', can_push: true } + + expect(json_response['id']).to eq(private_deploy_key.id) + expect(json_response['title']).to eq('new title') + expect(json_response['can_push']).to eq(true) + end + end + describe 'DELETE /projects/:id/deploy_keys/:key_id' do before { deploy_key } - it 'should delete existing key' do + it 'deletes existing key' do expect do delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) @@ -128,7 +171,7 @@ describe API::DeployKeys do end.to change{ project.deploy_keys.count }.by(-1) end - it 'should return 404 Not Found with invalid ID' do + it 'returns 404 Not Found with invalid ID' do delete api("/projects/#{project.id}/deploy_keys/404", admin) expect(response).to have_http_status(404) @@ -150,7 +193,7 @@ describe API::DeployKeys do end context 'when authenticated as non-admin user' do - it 'should return a 404 error' do + it 'returns a 404 error' do post api("/projects/#{project2.id}/deploy_keys/#{deploy_key.id}/enable", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index deb2cac6869..d325c6eff9d 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -258,6 +258,25 @@ describe API::Files do expect(last_commit.author_name).to eq(user.name) end + it "returns a 400 bad request if update existing file with stale last commit id" do + params_with_stale_id = valid_params.merge(last_commit_id: 'stale') + + put api(route(file_path), user), params_with_stale_id + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('You are attempting to update a file that has changed since you started editing it.') + end + + it "updates existing file in project repo with accepts correct last commit id" do + last_commit = Gitlab::Git::Commit + .last_for_path(project.repository, 'master', URI.unescape(file_path)) + params_with_correct_id = valid_params.merge(last_commit_id: last_commit.id) + + put api(route(file_path), user), params_with_correct_id + + expect(response).to have_http_status(200) + end + it "returns a 400 bad request if no params given" do put api(route(file_path), user) diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 3ab1764f5c3..4d4631322b1 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -36,11 +36,34 @@ describe API::ProjectSnippets do end end + describe 'GET /projects/:project_id/snippets/:id' do + let(:user) { create(:user) } + let(:snippet) { create(:project_snippet, :public, project: project) } + + it 'returns snippet json' do + get api("/projects/#{project.id}/snippets/#{snippet.id}", user) + + expect(response).to have_http_status(200) + + expect(json_response['title']).to eq(snippet.title) + expect(json_response['description']).to eq(snippet.description) + expect(json_response['file_name']).to eq(snippet.file_name) + end + + it 'returns 404 for invalid snippet id' do + get api("/projects/#{project.id}/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + describe 'POST /projects/:project_id/snippets/' do let(:params) do { title: 'Test Title', file_name: 'test.rb', + description: 'test description', code: 'puts "hello world"', visibility: 'public' } @@ -52,6 +75,7 @@ describe API::ProjectSnippets do expect(response).to have_http_status(201) snippet = ProjectSnippet.find(json_response['id']) expect(snippet.content).to eq(params[:code]) + expect(snippet.description).to eq(params[:description]) expect(snippet.title).to eq(params[:title]) expect(snippet.file_name).to eq(params[:file_name]) expect(snippet.visibility_level).to eq(Snippet::PUBLIC) @@ -106,12 +130,14 @@ describe API::ProjectSnippets do it 'updates snippet' do new_content = 'New content' + new_description = 'New description' - put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content + put api("/projects/#{snippet.project.id}/snippets/#{snippet.id}/", admin), code: new_content, description: new_description expect(response).to have_http_status(200) snippet.reload expect(snippet.content).to eq(new_content) + expect(snippet.description).to eq(new_description) end it 'returns 404 for invalid snippet id' do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 86c57204971..3e831373514 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -398,6 +398,15 @@ describe API::Projects do expect(json_response['tag_list']).to eq(%w[tagFirst tagSecond]) end + it 'uploads avatar for project a project' do + project = attributes_for(:project, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')) + + post api('/projects', user), project + + project_id = json_response['id'] + expect(json_response['avatar_url']).to eq("http://localhost/uploads/system/project/avatar/#{project_id}/banana_sample.gif") + end + it 'sets a project as allowing merge even if build fails' do project = attributes_for(:project, { only_allow_merge_if_pipeline_succeeds: false }) post api('/projects', user), project diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index be83514ed9c..9556c99dea1 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -431,8 +431,29 @@ describe API::Runner do expect(response).to have_http_status(201) expect(json_response['id']).to eq(test_job.id) expect(json_response['dependencies'].count).to eq(2) - expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token }, - { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + expect(json_response['dependencies']).to include( + { 'id' => job.id, 'name' => job.name, 'token' => job.token }, + { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token }) + end + end + + context 'when pipeline have jobs with artifacts' do + let!(:job) { create(:ci_build_tag, :artifacts, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) } + let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) } + + before do + job.success + end + + it 'returns dependent jobs' do + request_job + + expect(response).to have_http_status(201) + expect(json_response['id']).to eq(test_job.id) + expect(json_response['dependencies'].count).to eq(1) + expect(json_response['dependencies']).to include( + { 'id' => job.id, 'name' => job.name, 'token' => job.token, + 'artifacts_file' => { 'filename' => 'ci_build_artifacts.zip', 'size' => 106365 } }) end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index e429cddcf6a..8741cbd4e80 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -80,11 +80,33 @@ describe API::Snippets do end end + describe 'GET /snippets/:id' do + let(:snippet) { create(:personal_snippet, author: user) } + + it 'returns snippet json' do + get api("/snippets/#{snippet.id}", user) + + expect(response).to have_http_status(200) + + expect(json_response['title']).to eq(snippet.title) + expect(json_response['description']).to eq(snippet.description) + expect(json_response['file_name']).to eq(snippet.file_name) + end + + it 'returns 404 for invalid snippet id' do + get api("/snippets/1234", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + describe 'POST /snippets/' do let(:params) do { title: 'Test Title', file_name: 'test.rb', + description: 'test description', content: 'puts "hello world"', visibility: 'public' } @@ -97,6 +119,7 @@ describe API::Snippets do expect(response).to have_http_status(201) expect(json_response['title']).to eq(params[:title]) + expect(json_response['description']).to eq(params[:description]) expect(json_response['file_name']).to eq(params[:file_name]) end @@ -150,12 +173,14 @@ describe API::Snippets do it 'updates snippet' do new_content = 'New content' + new_description = 'New description' - put api("/snippets/#{snippet.id}", user), content: new_content + put api("/snippets/#{snippet.id}", user), content: new_content, description: new_description expect(response).to have_http_status(200) snippet.reload expect(snippet.content).to eq(new_content) + expect(snippet.description).to eq(new_description) end it 'returns 404 for invalid snippet id' do diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 4efc3e1a1e2..ec51b96c86b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -426,9 +426,14 @@ describe API::Users do expect(user.reload.email).not_to eq('invalid email') end - it "is not available for non admin users" do - put api("/users/#{user.id}", user), attributes_for(:user) - expect(response).to have_http_status(403) + context 'when the current user is not an admin' do + it "is not available" do + expect do + put api("/users/#{user.id}", user), attributes_for(:user) + end.not_to change { user.reload.attributes } + + expect(response).to have_http_status(403) + end end it "returns 404 for non-existing user" do @@ -649,7 +654,7 @@ describe API::Users do end it "returns a 404 for invalid ID" do - put api("/users/ASDF/emails", admin) + get api("/users/ASDF/emails", admin) expect(response).to have_http_status(404) end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index f018b48ceb2..6a83024d0d5 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -418,17 +418,17 @@ describe 'Git HTTP requests', lib: true do end context 'when username and password are provided' do - it 'rejects pulls with 2FA error message' do + it 'rejects pulls with personal access token error message' do download(path, user: user.username, password: user.password) do |response| expect(response).to have_http_status(:unauthorized) - expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end - it 'rejects the push attempt' do + it 'rejects the push attempt with personal access token error message' do upload(path, user: user.username, password: user.password) do |response| expect(response).to have_http_status(:unauthorized) - expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end end @@ -441,6 +441,41 @@ describe 'Git HTTP requests', lib: true do end end + context 'when internal auth is disabled' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false } + end + + it 'rejects pulls with personal access token error message' do + download(path, user: 'foo', password: 'bar') do |response| + expect(response).to have_http_status(:unauthorized) + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end + + it 'rejects pushes with personal access token error message' do + upload(path, user: 'foo', password: 'bar') do |response| + expect(response).to have_http_status(:unauthorized) + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end + + context 'when LDAP is configured' do + before do + allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) + allow_any_instance_of(Gitlab::LDAP::Authentication). + to receive(:login).and_return(nil) + end + + it 'does not display the personal access token error message' do + upload(path, user: 'foo', password: 'bar') do |response| + expect(response).to have_http_status(:unauthorized) + expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end + end + end + context "when blank password attempts follow a valid login" do def attempt_login(include_password) password = include_password ? user.password : "" @@ -648,7 +683,7 @@ describe 'Git HTTP requests', lib: true do # Provide a dummy file in its place allow_any_instance_of(Repository).to receive(:blob_at).and_call_original allow_any_instance_of(Repository).to receive(:blob_at).with('b83d6e391c22777fca1ed3012fce84f633d7fed0', 'info/refs') do - Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt') + Blob.decorate(Gitlab::Git::Blob.find(project.repository, 'master', 'bar/branch-test.txt'), project) end get "/#{project.path_with_namespace}/blob/master/info/refs" diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index a3e7844b2f3..54d7cf5f10d 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -41,6 +41,19 @@ describe JwtController do it { expect(response).to have_http_status(401) } end + + context 'using personal access tokens' do + let(:user) { create(:user) } + let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) } + let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } + + subject! { get '/jwt/auth', parameters, headers } + + it 'authenticates correctly' do + expect(response).to have_http_status(200) + expect(service_class).to have_received(:new).with(nil, user, parameters) + end + end end context 'using User login' do @@ -57,7 +70,7 @@ describe JwtController do context 'without personal token' do it 'rejects the authorization attempt' do expect(response).to have_http_status(401) - expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP') + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') end end @@ -75,9 +88,24 @@ describe JwtController do context 'using invalid login' do let(:headers) { { authorization: credentials('invalid', 'password') } } - subject! { get '/jwt/auth', parameters, headers } + context 'when internal auth is enabled' do + it 'rejects the authorization attempt' do + get '/jwt/auth', parameters, headers - it { expect(response).to have_http_status(401) } + expect(response).to have_http_status(401) + expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end + + context 'when internal auth is disabled' do + it 'rejects the authorization attempt with personal access token message' do + allow_any_instance_of(ApplicationSetting).to receive(:signin_enabled?) { false } + get '/jwt/auth', parameters, headers + + expect(response).to have_http_status(401) + expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP') + end + end end end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 05176c3beaa..6d1f0b24196 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -79,7 +79,7 @@ describe 'OpenID Connect requests' do 'email_verified' => true, 'website' => 'https://example.com', 'profile' => 'http://localhost/alice', - 'picture' => "http://localhost/uploads/user/avatar/#{user.id}/dk.png" + 'picture' => "http://localhost/uploads/system/user/avatar/#{user.id}/dk.png" }) end end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 54417f6b3e1..0a6778ae2ef 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -201,10 +201,12 @@ describe 'project routing' do # POST /:project_id/deploy_keys(.:format) deploy_keys#create # new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new # project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show + # edit_project_deploy_key GET /:project_id/deploy_keys/:id/edit(.:format) deploy_keys#edit + # project_deploy_key PATCH /:project_id/deploy_keys/:id(.:format) deploy_keys#update # DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy describe Projects::DeployKeysController, 'routing' do it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :new, :create] } + let(:actions) { [:index, :new, :create, :edit, :update] } let(:controller) { 'deploy_keys' } end end diff --git a/spec/rubocop/cop/activerecord_serialize_spec.rb b/spec/rubocop/cop/activerecord_serialize_spec.rb index a303b16d264..5bd7e5fa926 100644 --- a/spec/rubocop/cop/activerecord_serialize_spec.rb +++ b/spec/rubocop/cop/activerecord_serialize_spec.rb @@ -10,7 +10,7 @@ describe RuboCop::Cop::ActiverecordSerialize do context 'inside the app/models directory' do it 'registers an offense when serialize is used' do - allow(cop).to receive(:in_models?).and_return(true) + allow(cop).to receive(:in_model?).and_return(true) inspect_source(cop, 'serialize :foo') @@ -23,7 +23,7 @@ describe RuboCop::Cop::ActiverecordSerialize do context 'outside the app/models directory' do it 'does nothing' do - allow(cop).to receive(:in_models?).and_return(false) + allow(cop).to receive(:in_model?).and_return(false) inspect_source(cop, 'serialize :foo') diff --git a/spec/rubocop/cop/polymorphic_associations_spec.rb b/spec/rubocop/cop/polymorphic_associations_spec.rb new file mode 100644 index 00000000000..49959aa6419 --- /dev/null +++ b/spec/rubocop/cop/polymorphic_associations_spec.rb @@ -0,0 +1,33 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../rubocop/cop/polymorphic_associations' + +describe RuboCop::Cop::PolymorphicAssociations do + include CopHelper + + subject(:cop) { described_class.new } + + context 'inside the app/models directory' do + it 'registers an offense when polymorphic: true is used' do + allow(cop).to receive(:in_model?).and_return(true) + + inspect_source(cop, 'belongs_to :foo, polymorphic: true') + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([1]) + end + end + end + + context 'outside the app/models directory' do + it 'does nothing' do + allow(cop).to receive(:in_model?).and_return(false) + + inspect_source(cop, 'belongs_to :foo, polymorphic: true') + + expect(cop.offenses).to be_empty + end + end +end diff --git a/spec/rubocop/cop/redirect_with_status_spec.rb b/spec/rubocop/cop/redirect_with_status_spec.rb new file mode 100644 index 00000000000..5ad63567f84 --- /dev/null +++ b/spec/rubocop/cop/redirect_with_status_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +require 'rubocop' +require 'rubocop/rspec/support' + +require_relative '../../../rubocop/cop/redirect_with_status' + +describe RuboCop::Cop::RedirectWithStatus do + include CopHelper + + subject(:cop) { described_class.new } + let(:controller_fixture_without_status) do + %q( + class UserController < ApplicationController + def show + user = User.find(params[:id]) + redirect_to user_path if user.name == 'John Wick' + end + + def destroy + user = User.find(params[:id]) + + if user.destroy + redirect_to root_path + else + render :show + end + end + end + ) + end + + let(:controller_fixture_with_status) do + %q( + class UserController < ApplicationController + def show + user = User.find(params[:id]) + redirect_to user_path if user.name == 'John Wick' + end + + def destroy + user = User.find(params[:id]) + + if user.destroy + redirect_to root_path, status: 302 + else + render :show + end + end + end + ) + end + + context 'in controller' do + before do + allow(cop).to receive(:in_controller?).and_return(true) + end + + it 'registers an offense when a "destroy" action uses "redirect_to" without "status"' do + inspect_source(cop, controller_fixture_without_status) + + aggregate_failures do + expect(cop.offenses.size).to eq(1) + expect(cop.offenses.map(&:line)).to eq([12]) # 'redirect_to' is located on 12th line in controller_fixture. + expect(cop.highlights).to eq(['redirect_to']) + end + end + + it 'does not register an offense when a "destroy" action uses "redirect_to" with "status"' do + inspect_source(cop, controller_fixture_with_status) + + aggregate_failures do + expect(cop.offenses.size).to eq(0) + end + end + end + + context 'outside of controller' do + it 'registers no offense' do + inspect_source(cop, controller_fixture_without_status) + inspect_source(cop, controller_fixture_with_status) + + expect(cop.offenses.size).to eq(0) + end + end +end diff --git a/spec/serializers/build_entity_spec.rb b/spec/serializers/build_entity_spec.rb index 46d43a80ef7..e51ff9fc709 100644 --- a/spec/serializers/build_entity_spec.rb +++ b/spec/serializers/build_entity_spec.rb @@ -2,12 +2,13 @@ require 'spec_helper' describe BuildEntity do let(:user) { create(:user) } - let(:build) { create(:ci_build, :failed) } + let(:build) { create(:ci_build) } let(:project) { build.project } let(:request) { double('request') } before do allow(request).to receive(:current_user).and_return(user) + project.add_developer(user) end let(:entity) do @@ -16,9 +17,8 @@ describe BuildEntity do subject { entity.as_json } - it 'contains paths to build page and retry action' do - expect(subject).to include(:build_path, :retry_path) - expect(subject[:retry_path]).not_to be_nil + it 'contains paths to build page action' do + expect(subject).to include(:build_path) end it 'does not contain sensitive information' do @@ -39,12 +39,32 @@ describe BuildEntity do expect(subject[:status]).to include :icon, :favicon, :text, :label end - context 'when build is a regular job' do + context 'when build is retryable' do + before do + build.update(status: :failed) + end + + it 'contains cancel path' do + expect(subject).to include(:retry_path) + end + end + + context 'when build is cancelable' do + before do + build.update(status: :running) + end + + it 'contains cancel path' do + expect(subject).to include(:cancel_path) + end + end + + context 'when build is a regular build' do it 'does not contain path to play action' do expect(subject).not_to include(:play_path) end - it 'is not a playable job' do + it 'is not a playable build' do expect(subject[:playable]).to be false end end diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb index e73fbe190ca..ed89fccc3d0 100644 --- a/spec/serializers/deploy_key_entity_spec.rb +++ b/spec/serializers/deploy_key_entity_spec.rb @@ -12,27 +12,44 @@ describe DeployKeyEntity do let(:entity) { described_class.new(deploy_key, user: user) } - it 'returns deploy keys with projects a user can read' do - expected_result = { - id: deploy_key.id, - user_id: deploy_key.user_id, - title: deploy_key.title, - fingerprint: deploy_key.fingerprint, - can_push: deploy_key.can_push, - destroyed_when_orphaned: true, - almost_orphaned: false, - created_at: deploy_key.created_at, - updated_at: deploy_key.updated_at, - projects: [ - { - id: project.id, - name: project.name, - full_path: namespace_project_path(project.namespace, project), - full_name: project.full_name - } - ] - } - - expect(entity.as_json).to eq(expected_result) + describe 'returns deploy keys with projects a user can read' do + let(:expected_result) do + { + id: deploy_key.id, + user_id: deploy_key.user_id, + title: deploy_key.title, + fingerprint: deploy_key.fingerprint, + can_push: deploy_key.can_push, + destroyed_when_orphaned: true, + almost_orphaned: false, + created_at: deploy_key.created_at, + updated_at: deploy_key.updated_at, + can_edit: false, + projects: [ + { + id: project.id, + name: project.name, + full_path: namespace_project_path(project.namespace, project), + full_name: project.full_name + } + ] + } + end + + it { expect(entity.as_json).to eq(expected_result) } + end + + describe 'returns can_edit true if user is a master of project' do + before do + project.add_master(user) + end + + it { expect(entity.as_json).to include(can_edit: true) } + end + + describe 'returns can_edit true if a user admin' do + let(:user) { create(:user, :admin) } + + it { expect(entity.as_json).to include(can_edit: true) } end end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb index 088f24eb180..34b19fb9fc4 100644 --- a/spec/serializers/pipeline_serializer_spec.rb +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -102,18 +102,11 @@ describe PipelineSerializer do Ci::Pipeline::AVAILABLE_STATUSES.each do |status| create_pipeline(status) end - - RequestStore.begin! - end - - after do - RequestStore.end! - RequestStore.clear! end - it "verifies number of queries" do + it 'verifies number of queries', :request_store do recorded = ActiveRecord::QueryRecorder.new { subject } - expect(recorded.count).to be_within(1).of(60) + expect(recorded.count).to be_within(1).of(57) expect(recorded.cached_count).to eq(0) end diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb index a8555f5b4a0..effa4633d13 100644 --- a/spec/services/boards/create_service_spec.rb +++ b/spec/services/boards/create_service_spec.rb @@ -14,8 +14,9 @@ describe Boards::CreateService, services: true do it 'creates the default lists' do board = service.execute - expect(board.lists.size).to eq 1 - expect(board.lists.first).to be_closed + expect(board.lists.size).to eq 2 + expect(board.lists.first).to be_backlog + expect(board.lists.last).to be_closed end end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index c982031c791..a1e220c2322 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -13,6 +13,7 @@ describe Boards::Issues::ListService, services: true do let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } + let!(:backlog) { create(:backlog_list, board: board) } let!(:list1) { create(:list, board: board, label: development, position: 0) } let!(:list2) { create(:list, board: board, label: testing, position: 1) } let!(:closed) { create(:closed_list, board: board) } @@ -53,12 +54,20 @@ describe Boards::Issues::ListService, services: true do expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] end + it 'returns opened issues when listing issues from Backlog' do + params = { board_id: board.id, id: backlog.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] + end + it 'returns closed issues when listing issues from Closed' do params = { board_id: board.id, id: closed.id } issues = described_class.new(project, user, params).execute - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1] + expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1] end it 'returns opened issues that have label list applied when listing issues from a label list' do diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index ab9fb1bc914..68140759600 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -1,16 +1,33 @@ require 'spec_helper' describe Boards::Lists::ListService, services: true do + let(:project) { create(:empty_project) } + let(:board) { create(:board, project: project) } + let(:label) { create(:label, project: project) } + let!(:list) { create(:list, board: board, label: label) } + let(:service) { described_class.new(project, double) } + describe '#execute' do - it "returns board's lists" do - project = create(:empty_project) - board = create(:board, project: project) - label = create(:label, project: project) - list = create(:list, board: board, label: label) + context 'when the board has a backlog list' do + let!(:backlog_list) { create(:backlog_list, board: board) } + + it 'does not create a backlog list' do + expect { service.execute(board) }.not_to change(board.lists, :count) + end + + it "returns board's lists" do + expect(service.execute(board)).to eq [backlog_list, list, board.closed_list] + end + end - service = described_class.new(project, double) + context 'when the board does not have a backlog list' do + it 'creates a backlog list' do + expect { service.execute(board) }.to change(board.lists, :count).by(1) + end - expect(service.execute(board)).to eq [list, board.closed_list] + it "returns board's lists" do + expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list] + end end end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 597c3947e71..77c07b71c68 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::CreatePipelineService, services: true do +describe Ci::CreatePipelineService, :services do let(:project) { create(:project, :repository) } let(:user) { create(:admin) } @@ -30,6 +30,7 @@ describe Ci::CreatePipelineService, services: true do it 'creates a pipeline' do expect(pipeline).to be_kind_of(Ci::Pipeline) expect(pipeline).to be_valid + expect(pipeline).to be_persisted expect(pipeline).to be_push expect(pipeline).to eq(project.pipelines.last) expect(pipeline).to have_attributes(user: user) @@ -37,6 +38,14 @@ describe Ci::CreatePipelineService, services: true do expect(pipeline.builds.first).to be_kind_of(Ci::Build) end + it 'increments the prometheus counter' do + expect(Gitlab::Metrics).to receive(:counter) + .with(:pipelines_created_count, "Pipelines created count") + .and_call_original + + pipeline + end + context 'when merge requests already exist for this source branch' do it 'updates head pipeline of each merge request' do merge_request_1 = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project) diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index 7254e6b357a..ef9927c5969 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -25,12 +25,24 @@ describe Ci::RetryBuildService, :services do user_id auto_canceled_by_id retried].freeze shared_examples 'build duplication' do + let(:stage) do + # TODO, we still do not have factory for new stages, we will need to + # switch existing factory to persist stages, instead of using LegacyStage + # + Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test') + end + let(:build) do create(:ci_build, :failed, :artifacts_expired, :erased, :queued, :coverage, :tags, :allowed_to_fail, :on_tag, - :teardown_environment, :triggered, :trace, - description: 'some build', pipeline: pipeline, - auto_canceled_by: create(:ci_empty_pipeline)) + :triggered, :trace, :teardown_environment, + description: 'my-job', stage: 'test', pipeline: pipeline, + auto_canceled_by: create(:ci_empty_pipeline)) do |build| + ## + # TODO, workaround for FactoryGirl limitation when having both + # stage (text) and stage_id (integer) columns in the table. + build.stage_id = stage.id + end end describe 'clone accessors' do diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 6437d00e451..eb9b1670c71 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -72,7 +72,7 @@ describe Issuable::BulkUpdateService, services: true do end context "when the new assignee ID is #{IssuableFinder::NONE}" do - it "unassigns the issues" do + it 'unassigns the issues' do expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) } .to change { merge_request.reload.assignee }.to(nil) end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 5184c1d5f19..a78866a2c32 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -31,6 +31,13 @@ describe Issues::UpdateService, services: true do end end + def find_notes(action) + issue + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + def update_issue(opts) described_class.new(project, user, opts).execute(issue) end @@ -330,6 +337,9 @@ describe Issues::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -345,6 +355,9 @@ describe Issues::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -354,10 +367,12 @@ describe Issues::UpdateService, services: true do update_issue(description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2") end - it 'does not create a system note' do - note = find_note('marked the task **Task 2** as incomplete') + it 'does not create a system note for the task' do + task_note = find_note('marked the task **Task 2** as incomplete') + description_notes = find_notes('description') - expect(note).to be_nil + expect(task_note).to be_nil + expect(description_notes.length).to eq(2) end end @@ -368,9 +383,11 @@ describe Issues::UpdateService, services: true do end it 'does not create a system note referencing the position the old item' do - note = find_note('marked the task **Two** as incomplete') + task_note = find_note('marked the task **Two** as incomplete') + description_notes = find_notes('description') - expect(note).to be_nil + expect(task_note).to be_nil + expect(description_notes.length).to eq(2) end it 'does not generate a new note at all' do diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 0670ac2faa2..5ce8e17976b 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -11,7 +11,7 @@ describe Members::CreateService, services: true do params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST } result = described_class.new(project, user, params).execute - expect(result).to be_truthy + expect(result[:status]).to eq(:success) expect(project.users).to include project_user end @@ -19,7 +19,19 @@ describe Members::CreateService, services: true do params = { user_ids: '', access_level: Gitlab::Access::GUEST } result = described_class.new(project, user, params).execute - expect(result).to be_falsey + expect(result[:status]).to eq(:error) + expect(result[:message]).to be_present + expect(project.users).not_to include project_user + end + + it 'limits the number of users to 100' do + user_ids = 1.upto(101).to_a.join(',') + params = { user_ids: user_ids, access_level: Gitlab::Access::GUEST } + + result = described_class.new(project, user, params).execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to be_present expect(project.users).not_to include project_user end end diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 935f4710851..bb46e1dd9ab 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -10,8 +10,8 @@ describe MergeRequests::MergeRequestDiffCacheService do expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) - allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true)) - allow_any_instance_of(Repository).to receive(:diffable?).and_return(true) + allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true) + allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true) subject.execute(merge_request) end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index d371fc68312..091c193aaa6 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -30,6 +30,13 @@ describe MergeRequests::UpdateService, services: true do end end + def find_notes(action) + @merge_request + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + def update_merge_request(opts) @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) @merge_request.reload @@ -394,6 +401,9 @@ describe MergeRequests::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -409,6 +419,9 @@ describe MergeRequests::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 3c566c04d6b..40298dcb723 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -115,7 +115,7 @@ describe Projects::CreateService, '#execute', services: true do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) opts.merge!( - visibility_level: Gitlab::VisibilityLevel.options['Public'] + visibility_level: Gitlab::VisibilityLevel::PUBLIC ) end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index f8eb34f2ef4..0df81f3abcb 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Projects::ForkService, services: true do describe 'fork by user' do before do - @from_namespace = create(:namespace) - @from_user = create(:user, namespace: @from_namespace ) + @from_user = create(:user) + @from_namespace = @from_user.namespace avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @from_project = create(:project, :repository, @@ -13,8 +13,8 @@ describe Projects::ForkService, services: true do star_count: 107, avatar: avatar, description: 'wow such project') - @to_namespace = create(:namespace) - @to_user = create(:user, namespace: @to_namespace) + @to_user = create(:user) + @to_namespace = @to_user.namespace @from_project.add_user(@to_user, :developer) end diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb index 0657b7e93fe..d75851134ee 100644 --- a/spec/services/projects/participants_service_spec.rb +++ b/spec/services/projects/participants_service_spec.rb @@ -13,7 +13,7 @@ describe Projects::ParticipantsService, services: true do groups = participants.groups expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq("/uploads/group/avatar/#{group.id}/dk.png") + expect(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 @@ -24,7 +24,7 @@ describe Projects::ParticipantsService, services: true do groups = participants.groups expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/group/avatar/#{group.id}/dk.png") + expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/system/group/avatar/#{group.id}/dk.png") end end end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index e5e400ee281..4db491fd5f3 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -384,7 +384,7 @@ describe SlashCommands::InterpretService, services: true do it 'fetches assignee and populates assignee_id if content contains /assign' do _, updates = service.execute(content, issue) - expect(updates[:assignee_ids]).to match_array([developer.id, developer2.id]) + expect(updates[:assignee_ids]).to match_array([developer.id]) end end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index c499b1bb343..9295c09aefc 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1052,7 +1052,7 @@ describe SystemNoteService, services: true do let(:action) { 'task' } end - it "posts the 'marked as a Work In Progress from commit' system note" do + it "posts the 'marked the task as complete' system note" do expect(subject.note).to eq("marked the task **task** as completed") end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 994c7dcbb46..1979347a178 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,7 @@ SimpleCovEnv.start! ENV["RAILS_ENV"] ||= 'test' ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true' +# ENV['prometheus_multiproc_dir'] = 'tmp/prometheus_multiproc_dir_test' require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' @@ -55,6 +56,7 @@ RSpec.configure do |config| config.include StubGitlabCalls config.include StubGitlabData config.include ApiHelpers, :api + config.include MigrationsHelpers, :migration config.infer_spec_type_from_file_location! @@ -72,10 +74,18 @@ RSpec.configure do |config| TestEnv.cleanup end + config.before(:example, :request_store) do + RequestStore.begin! + end + + config.after(:example, :request_store) do + RequestStore.end! + RequestStore.clear! + end + if ENV['CI'] - # Retry only on feature specs that use JS - config.around :each, :js do |ex| - ex.run_with_retry retry: 3 + config.around(:each) do |ex| + ex.run_with_retry retry: 2 end end @@ -96,6 +106,17 @@ RSpec.configure do |config| Sidekiq.redis(&:flushall) end + config.around(:example, :migration) do |example| + begin + ActiveRecord::Migrator + .migrate(migrations_paths, previous_migration.version) + + example.run + ensure + ActiveRecord::Migrator.migrate(migrations_paths) + end + end + config.around(:each, :nested_groups) do |example| example.run if Group.supports_nested_groups? end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index b8ca8f22a3d..c34e76fa72f 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -14,8 +14,10 @@ Capybara.register_driver :poltergeist do |app| js_errors: true, timeout: timeout, window_size: [1366, 768], + url_whitelist: %w[localhost 127.0.0.1], + url_blacklist: %w[.mp4 .png .gif .avi .bmp .jpg .jpeg], phantomjs_options: [ - '--load-images=no' + '--load-images=yes' ] ) end diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb index 6f31828b825..7f5769209bb 100644 --- a/spec/support/db_cleaner.rb +++ b/spec/support/db_cleaner.rb @@ -19,6 +19,10 @@ RSpec.configure do |config| DatabaseCleaner.strategy = :truncation end + config.before(:each, :migration) do + DatabaseCleaner.strategy = :truncation + end + config.before(:each) do DatabaseCleaner.start end diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb new file mode 100644 index 00000000000..0d80c95e826 --- /dev/null +++ b/spec/support/features/reportable_note_shared_examples.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +shared_examples 'reportable note' do + include NotesHelper + + let(:comment) { find("##{ActionView::RecordIdentifier.dom_id(note)}") } + let(:more_actions_selector) { '.more-actions.dropdown' } + let(:abuse_report_path) { new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) } + + it 'has a `More actions` dropdown' do + expect(comment).to have_selector(more_actions_selector) + end + + it 'dropdown has Edit, Report and Delete links' do + dropdown = comment.find(more_actions_selector) + + dropdown.click + dropdown.find('.dropdown-menu li', match: :first) + + expect(dropdown).to have_button('Edit comment') + expect(dropdown).to have_link('Report as abuse', href: abuse_report_path) + expect(dropdown).to have_link('Delete comment', href: note_url(note, project)) + end + + it 'Report button links to a report page' do + dropdown = comment.find(more_actions_selector) + + dropdown.click + dropdown.find('.dropdown-menu li', match: :first) + + dropdown.click_link('Report as abuse') + + expect(find('#user_name')['value']).to match(note.author.username) + expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note)) + end +end diff --git a/spec/support/helpers/note_interaction_helpers.rb b/spec/support/helpers/note_interaction_helpers.rb new file mode 100644 index 00000000000..551c759133c --- /dev/null +++ b/spec/support/helpers/note_interaction_helpers.rb @@ -0,0 +1,8 @@ +module NoteInteractionHelpers + def open_more_actions_dropdown(note) + note_element = find("#note_#{note.id}") + + note_element.find('.more-actions').click + note_element.find('.more-actions .dropdown-menu li', match: :first) + end +end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index a982b159b48..aace4b3adee 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -48,7 +48,7 @@ module JavaScriptFixturesHelpers link_tags = doc.css('link') link_tags.remove - scripts = doc.css("script:not([type='text/template'])") + scripts = doc.css("script:not([type='text/template']):not([type='text/x-template'])") scripts.remove fixture = doc.to_html diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb new file mode 100644 index 00000000000..91fbb4eaf48 --- /dev/null +++ b/spec/support/migrations_helpers.rb @@ -0,0 +1,29 @@ +module MigrationsHelpers + def table(name) + Class.new(ActiveRecord::Base) { self.table_name = name } + end + + def migrations_paths + ActiveRecord::Migrator.migrations_paths + end + + def table_exists?(name) + ActiveRecord::Base.connection.table_exists?(name) + end + + def migrations + ActiveRecord::Migrator.migrations(migrations_paths) + end + + def previous_migration + migrations.each_cons(2) do |previous, migration| + break previous if migration.name == described_class.name + end + end + + def migrate! + ActiveRecord::Migrator.up(migrations_paths) do |migration| + migration.name == described_class.name + end + end +end diff --git a/spec/support/milestone_tabs_examples.rb b/spec/support/milestone_tabs_examples.rb index 4ad8b0a16e1..7cfc1e06975 100644 --- a/spec/support/milestone_tabs_examples.rb +++ b/spec/support/milestone_tabs_examples.rb @@ -1,10 +1,14 @@ shared_examples 'milestone tabs' do def go(path, extra_params = {}) - params = if milestone.is_a?(GlobalMilestone) - { group_id: group.to_param, id: milestone.safe_title, title: milestone.title } - else - { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } - end + params = + case milestone + when DashboardMilestone + { id: milestone.safe_title, title: milestone.title } + when GroupMilestone + { group_id: group.to_param, id: milestone.safe_title, title: milestone.title } + else + { namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid } + end get path, params.merge(extra_params) end diff --git a/spec/support/target_branch_helpers.rb b/spec/support/target_branch_helpers.rb deleted file mode 100644 index 01d1c53fe6c..00000000000 --- a/spec/support/target_branch_helpers.rb +++ /dev/null @@ -1,16 +0,0 @@ -module TargetBranchHelpers - def select_branch(name) - first('button.js-target-branch').click - wait_for_requests - all('a[data-group="Branches"]').find do |el| - el.text == name - end.click - end - - def create_new_branch(name) - first('button.js-target-branch').click - click_link 'Create new branch' - fill_in 'new_branch_name', with: name - click_button 'Create' - end -end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 72b3b226c1e..3f472e59c49 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -54,6 +54,8 @@ module TestEnv 'conflict-resolvable-fork' => '404fa3f' }.freeze + TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**') + # Test environment # # See gitlab.yml.example test section for paths @@ -98,9 +100,7 @@ module TestEnv # # Keeps gitlab-shell and gitlab-test def clean_test_path - tmp_test_path = Rails.root.join('tmp', 'tests', '**') - - Dir[tmp_test_path].each do |entry| + Dir[TMP_TEST_PATH].each do |entry| unless File.basename(entry) =~ /\A(gitaly|gitlab-(shell|test|test_bare|test-fork|test-fork_bare))\z/ FileUtils.rm_rf(entry) end @@ -111,6 +111,14 @@ module TestEnv FileUtils.mkdir_p(pages_path) end + def clean_gitlab_test_path + Dir[TMP_TEST_PATH].each do |entry| + if File.basename(entry) =~ /\A(gitlab-(test|test_bare|test-fork|test-fork_bare))\z/ + FileUtils.rm_rf(entry) + end + end + end + def setup_gitlab_shell unless File.directory?(Gitlab.config.gitlab_shell.path) unless system('rake', 'gitlab:shell:install') @@ -249,7 +257,7 @@ module TestEnv # Before we used Git clone's --mirror option, bare repos could end up # with missing refs, clearing them and retrying should fix the issue. - cleanup && init unless reset.call + cleanup && clean_gitlab_test_path && init unless reset.call end end diff --git a/spec/unicorn/unicorn_spec.rb b/spec/unicorn/unicorn_spec.rb index 8518c047a47..41de94d35c2 100644 --- a/spec/unicorn/unicorn_spec.rb +++ b/spec/unicorn/unicorn_spec.rb @@ -67,8 +67,8 @@ describe 'Unicorn' do end def wait_unicorn_boot!(master_pid, ready_file) - # Unicorn should boot in under 60 seconds so 120 seconds seems like a good timeout. - timeout = 120 + # We have seen the boot timeout after 2 minutes in CI so let's set it to 5 minutes. + timeout = 5 * 60 timeout.times do return if File.exist?(ready_file) pid = Process.waitpid(master_pid, Process::WNOHANG) diff --git a/spec/uploaders/artifact_uploader_spec.rb b/spec/uploaders/artifact_uploader_spec.rb index 24e2e3a9f0e..2a3bd0e3bb2 100644 --- a/spec/uploaders/artifact_uploader_spec.rb +++ b/spec/uploaders/artifact_uploader_spec.rb @@ -17,22 +17,45 @@ describe ArtifactUploader do describe '.artifacts_upload_path' do subject { described_class.artifacts_upload_path } - + it { is_expected.to start_with(path) } it { is_expected.to end_with('tmp/uploads/') } end describe '#store_dir' do subject { uploader.store_dir } - + it { is_expected.to start_with(path) } it { is_expected.to end_with("#{job.project_id}/#{job.id}") } end describe '#cache_dir' do subject { uploader.cache_dir } - + + it { is_expected.to start_with(path) } + it { is_expected.to end_with('/tmp/cache') } + end + + describe '#work_dir' do + subject { uploader.work_dir } + it { is_expected.to start_with(path) } - it { is_expected.to end_with('tmp/cache') } + it { is_expected.to end_with('/tmp/work') } + end + + describe '#filename' do + # we need to use uploader, as this makes to use mounter + # which initialises uploader.file object + let(:uploader) { job.artifacts_file } + + subject { uploader.filename } + + it { is_expected.to be_nil } + + context 'with artifacts' do + let(:job) { create(:ci_build, :artifacts) } + + it { is_expected.not_to be_nil } + end end end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb index ea714fb08f0..d82dbe871d5 100644 --- a/spec/uploaders/attachment_uploader_spec.rb +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -3,6 +3,17 @@ require 'spec_helper' describe AttachmentUploader do let(:uploader) { described_class.new(build_stubbed(:user)) } + describe "#store_dir" do + it "stores in the system dir" do + expect(uploader.store_dir).to start_with("uploads/system/user") + end + + it "uses the old path when using object storage" do + expect(described_class).to receive(:file_storage?).and_return(false) + expect(uploader.store_dir).to start_with("uploads/user") + end + end + describe '#move_to_cache' do it 'is true' do expect(uploader.move_to_cache).to eq(true) diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb index c4d558805ab..201fe6949aa 100644 --- a/spec/uploaders/avatar_uploader_spec.rb +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -3,6 +3,17 @@ require 'spec_helper' describe AvatarUploader do let(:uploader) { described_class.new(build_stubbed(:user)) } + describe "#store_dir" do + it "stores in the system dir" do + expect(uploader.store_dir).to start_with("uploads/system/user") + end + + it "uses the old path when using object storage" do + expect(described_class).to receive(:file_storage?).and_return(false) + expect(uploader.store_dir).to start_with("uploads/user") + end + end + describe '#move_to_cache' do it 'is false' do expect(uploader.move_to_cache).to eq(false) diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb new file mode 100644 index 00000000000..896cb410ed5 --- /dev/null +++ b/spec/uploaders/file_mover_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe FileMover do + let(:filename) { 'banana_sample.gif' } + let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) } + let(:temp_description) do + 'test ![banana_sample](/uploads/temp/secret55/banana_sample.gif) same ![banana_sample]'\ + '(/uploads/temp/secret55/banana_sample.gif)' + end + let(:temp_file_path) { File.join('secret55', filename).to_s } + let(:file_path) { File.join('uploads', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s } + + let(:snippet) { create(:personal_snippet, description: temp_description) } + + subject { described_class.new(file_path, snippet).execute } + + describe '#execute' do + before do + expect(FileUtils).to receive(:mkdir_p).with(a_string_including(File.dirname(file_path))) + expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path)) + allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true) + allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10) + end + + context 'when move and field update successful' do + it 'updates the description correctly' do + subject + + expect(snippet.reload.description) + .to eq( + "test ![banana_sample](/uploads/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\ + " same ![banana_sample](/uploads/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)" + ) + end + + it 'creates a new update record' do + expect { subject }.to change { Upload.count }.by(1) + end + end + + context 'when update_markdown fails' do + before do + expect(FileUtils).to receive(:move).with(a_string_including(file_path), a_string_including(temp_file_path)) + end + + subject { described_class.new(file_path, snippet, :non_existing_field).execute } + + it 'does not update the description' do + subject + + expect(snippet.reload.description) + .to eq( + "test ![banana_sample](/uploads/temp/secret55/banana_sample.gif)"\ + " same ![banana_sample](/uploads/temp/secret55/banana_sample.gif)" + ) + end + + it 'does not create a new update record' do + expect { subject }.not_to change { Upload.count } + end + end + end +end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index d9113ef4095..47e9365e13d 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -15,6 +15,16 @@ describe FileUploader do end end + describe "#store_dir" do + it "stores in the namespace path" do + project = build_stubbed(:empty_project) + uploader = described_class.new(project) + + expect(uploader.store_dir).to include(project.path_with_namespace) + expect(uploader.store_dir).not_to include("system") + end + end + describe 'initialize' do it 'generates a secret if none is provided' do expect(SecureRandom).to receive(:hex).and_return('secret') diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb index 78e9d9cf46c..a144b39f74f 100644 --- a/spec/uploaders/gitlab_uploader_spec.rb +++ b/spec/uploaders/gitlab_uploader_spec.rb @@ -53,4 +53,19 @@ describe GitlabUploader do expect(subject.move_to_store).to eq(true) end end + + describe '#cache!' do + it 'moves the file from the working directory to the cache directory' do + # One to get the work dir, the other to remove it + expect(subject).to receive(:workfile_path).exactly(2).times.and_call_original + # Test https://github.com/carrierwavesubject/carrierwave/blob/v1.0.0/lib/carrierwave/sanitized_file.rb#L200 + expect(FileUtils).to receive(:mv).with(anything, /^#{subject.work_dir}/).and_call_original + expect(FileUtils).to receive(:mv).with(/^#{subject.work_dir}/, /#{subject.cache_dir}/).and_call_original + + fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') + subject.cache!(fixture_file_upload(fixture)) + + expect(subject.file.path).to match(/#{subject.cache_dir}/) + end + end end diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb index c3b72e7d677..7088bc23334 100644 --- a/spec/uploaders/lfs_object_uploader_spec.rb +++ b/spec/uploaders/lfs_object_uploader_spec.rb @@ -1,21 +1,9 @@ require 'spec_helper' describe LfsObjectUploader do - let(:uploader) { described_class.new(build_stubbed(:empty_project)) } - - describe '#cache!' do - it 'caches the file in the cache directory' do - # One to get the work dir, the other to remove it - expect(uploader).to receive(:workfile_path).exactly(2).times.and_call_original - expect(FileUtils).to receive(:mv).with(anything, /^#{uploader.work_dir}/).and_call_original - expect(FileUtils).to receive(:mv).with(/^#{uploader.work_dir}/, /^#{uploader.cache_dir}/).and_call_original - - fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg') - uploader.cache!(fixture_file_upload(fixture)) - - expect(uploader.file.path).to start_with(uploader.cache_dir) - end - end + let(:lfs_object) { create(:lfs_object, :with_file) } + let(:uploader) { described_class.new(lfs_object) } + let(:path) { Gitlab.config.lfs.storage_path } describe '#move_to_cache' do it 'is true' do @@ -28,4 +16,25 @@ describe LfsObjectUploader do expect(uploader.move_to_store).to eq(true) end end + + describe '#store_dir' do + subject { uploader.store_dir } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with("#{lfs_object.oid[0, 2]}/#{lfs_object.oid[2, 2]}") } + end + + describe '#cache_dir' do + subject { uploader.cache_dir } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with('/tmp/cache') } + end + + describe '#work_dir' do + subject { uploader.work_dir } + + it { is_expected.to start_with(path) } + it { is_expected.to end_with('/tmp/work') } + end end diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb index 5c26e334a6e..bb32ee62ccb 100644 --- a/spec/uploaders/records_uploads_spec.rb +++ b/spec/uploaders/records_uploads_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe RecordsUploads do - let(:uploader) do + let!(:uploader) do class RecordsUploadsExampleUploader < GitlabUploader include RecordsUploads @@ -57,6 +57,13 @@ describe RecordsUploads do uploader.store!(upload_fixture('rails_sample.jpg')) end + it 'does not create an Upload record if model is missing' do + expect_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil) + expect(Upload).not_to receive(:record).with(uploader) + + uploader.store!(upload_fixture('rails_sample.jpg')) + end + it 'it destroys Upload records at the same path before recording' do existing = Upload.create!( path: File.join('uploads', 'rails_sample.jpg'), diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb index 6b07fcfc987..1f8261cc46b 100644 --- a/spec/views/help/index.html.haml_spec.rb +++ b/spec/views/help/index.html.haml_spec.rb @@ -21,7 +21,7 @@ describe 'help/index' do render expect(rendered).to match '8.0.2' - expect(rendered).to match 'abcdefg' + expect(rendered).to have_link('abcdefg', 'https://gitlab.com/gitlab-org/gitlab-ce/commits/abcdefg') end end diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index 8f2822f5dc5..d9a7ba265f8 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -15,36 +15,6 @@ describe 'projects/jobs/show', :view do allow(view).to receive(:can?).and_return(true) end - describe 'job information in header' do - let(:build) do - create(:ci_build, :success, environment: 'staging') - end - - before do - render - end - - it 'shows status name' do - expect(rendered).to have_css('.ci-status.ci-success', text: 'passed') - end - - it 'does not render a link to the job' do - expect(rendered).not_to have_link('passed') - end - - it 'shows job id' do - expect(rendered).to have_css('.js-build-id', text: build.id) - end - - it 'shows a link to the pipeline' do - expect(rendered).to have_link(build.pipeline.id) - end - - it 'shows a link to the commit' do - expect(rendered).to have_link(build.pipeline.short_sha) - end - end - describe 'environment info in job view' do context 'job with latest deployment' do let(:build) do @@ -215,34 +185,6 @@ describe 'projects/jobs/show', :view do end end - context 'when job is not running' do - before do - build.success! - render - end - - it 'shows retry button' do - expect(rendered).to have_link('Retry') - end - - context 'if build passed' do - it 'does not show New issue button' do - expect(rendered).not_to have_link('New issue') - end - end - - context 'if build failed' do - before do - build.status = 'failed' - render - end - - it 'shows New issue button' do - expect(rendered).to have_link('New issue') - end - end - end - describe 'commit title in sidebar' do let(:commit_title) { project.commit.title } @@ -269,25 +211,4 @@ describe 'projects/jobs/show', :view do expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2') end end - - describe 'New issue button' do - before do - build.status = 'failed' - render - end - - it 'links to issues/new with the title and description filled in' do - title = "Build Failed ##{build.id}" - build_url = namespace_project_job_url(project.namespace, project, build) - href = new_namespace_project_issue_path( - project.namespace, - project, - issue: { - title: title, - description: build_url - } - ) - expect(rendered).to have_link('New issue', href: href) - end - end end diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb new file mode 100644 index 00000000000..0d742ae9dc7 --- /dev/null +++ b/spec/workers/background_migration_worker_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe BackgroundMigrationWorker do + describe '.perform' do + it 'performs a background migration' do + expect(Gitlab::BackgroundMigration). + to receive(:perform). + with('Foo', [10, 20]) + + described_class.new.perform('Foo', [10, 20]) + end + end +end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index f4bc63bcc6a..44163c735ba 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -94,26 +94,23 @@ describe PostReceive do it { expect{ subject }.not_to change{ Ci::Pipeline.count } } end end - end - describe '#process_repository_update' do - let(:changes) {'123456 789012 refs/heads/tést'} - let(:fake_hook_data) do - { event_name: 'repository_update' } - end + context 'after project changes hooks' do + let(:changes) { '123456 789012 refs/heads/tést' } + let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } - before do - allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) - 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(subject).to receive(:process_project_changes).and_return(true) - end + before 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) + end - it 'calls SystemHooksService' do - expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) + it 'calls SystemHooksService' do + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - subject.perform(pwd(project), key_id, base64_changes) + described_class.new.perform(project_identifier, key_id, base64_changes) + end end end |