diff options
Diffstat (limited to 'spec')
154 files changed, 6942 insertions, 2278 deletions
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb index 288984cfba9..19fbc2f7748 100644 --- a/spec/controllers/dashboard/todos_controller_spec.rb +++ b/spec/controllers/dashboard/todos_controller_spec.rb @@ -12,7 +12,7 @@ describe Dashboard::TodosController do end context 'when using pagination' do - let(:last_page) { user.todos.page().total_pages } + let(:last_page) { user.todos.page.total_pages } let!(:issues) { create_list(:issue, 2, project: project, assignee: user) } before do diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb index b9d9117c928..17dc101b7ee 100644 --- a/spec/controllers/projects/group_links_controller_spec.rb +++ b/spec/controllers/projects/group_links_controller_spec.rb @@ -31,7 +31,7 @@ describe Projects::GroupLinksController do it 'redirects to project group links page' do expect(response).to redirect_to( - namespace_project_group_links_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) end end @@ -62,7 +62,7 @@ describe Projects::GroupLinksController do it 'redirects to project group links page' do expect(response).to redirect_to( - namespace_project_group_links_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) end end @@ -76,7 +76,7 @@ describe Projects::GroupLinksController do it 'redirects to project group links page' do expect(response).to redirect_to( - namespace_project_group_links_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) expect(flash[:alert]).to eq('Please select a group.') end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 5fe7e6407cc..1ed2ee3ab4a 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -5,13 +5,33 @@ describe Projects::PipelinesController do let(:user) { create(:user) } let(:project) { create(:empty_project, :public) } - let(:pipeline) { create(:ci_pipeline, project: project) } before do sign_in(user) end + describe 'GET index.json' do + before do + create_list(:ci_empty_pipeline, 2, project: project) + + get :index, namespace_id: project.namespace.path, + project_id: project.path, + format: :json + end + + it 'returns JSON with serialized pipelines' do + expect(response).to have_http_status(:ok) + + expect(json_response).to include('pipelines') + expect(json_response['pipelines'].count).to eq 2 + expect(json_response['count']['all']).to eq 2 + expect(json_response['count']['running_or_pending']).to eq 2 + end + end + describe 'GET stages.json' do + let(:pipeline) { create(:ci_pipeline, project: project) } + context 'when accessing existing stage' do before do create(:ci_build, pipeline: pipeline, stage: 'build') diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index b52137fbe7e..442f81187dc 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -5,11 +5,11 @@ describe Projects::ProjectMembersController do let(:project) { create(:empty_project, :public, :access_requestable) } describe 'GET index' do - it 'renders index with 200 status code' do + it 'should have the settings/members address with a 302 status code' do get :index, namespace_id: project.namespace, project_id: project - expect(response).to have_http_status(200) - expect(response).to render_template(:index) + expect(response).to have_http_status(302) + expect(response.location).to include namespace_project_settings_members_path(project.namespace, project) end end @@ -44,7 +44,7 @@ describe Projects::ProjectMembersController do access_level: Gitlab::Access::GUEST expect(response).to set_flash.to 'Users were successfully added.' - expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) + expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project)) end it 'adds no user to members' do @@ -56,7 +56,7 @@ describe Projects::ProjectMembersController do access_level: Gitlab::Access::GUEST expect(response).to set_flash.to 'No users or groups specified.' - expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project)) + expect(response).to redirect_to(namespace_project_settings_members_path(project.namespace, project)) end end end @@ -99,7 +99,7 @@ describe Projects::ProjectMembersController do id: member expect(response).to redirect_to( - namespace_project_project_members_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) expect(project.members).not_to include member end @@ -259,7 +259,7 @@ describe Projects::ProjectMembersController do expect(project.team_members).to include member expect(response).to set_flash.to 'Successfully imported' expect(response).to redirect_to( - namespace_project_project_members_path(project.namespace, project) + namespace_project_settings_members_path(project.namespace, project) ) end end diff --git a/spec/controllers/projects/settings/members_controller_spec.rb b/spec/controllers/projects/settings/members_controller_spec.rb new file mode 100644 index 00000000000..076d6cd9c6e --- /dev/null +++ b/spec/controllers/projects/settings/members_controller_spec.rb @@ -0,0 +1,14 @@ +require('spec_helper') + +describe Projects::Settings::MembersController do + let(:project) { create(:empty_project, :public, :access_requestable) } + + describe 'GET show' do + it 'renders show with 200 status code' do + get :show, namespace_id: project.namespace, project_id: project + + expect(response).to have_http_status(200) + expect(response).to render_template(:show) + end + end +end diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb index a7c5283df94..007b35bbb77 100644 --- a/spec/db/production/settings.rb +++ b/spec/db/production/settings.rb @@ -2,10 +2,11 @@ require 'spec_helper' require 'rainbow/ext/string' describe 'seed production settings', lib: true do + include StubENV + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do before do - allow(ENV).to receive(:[]).and_call_original - allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789') + stub_env('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN', '013456789') end it 'writes the token to the database' do diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb index 1735791f644..77404f46c92 100644 --- a/spec/factories/ci/pipelines.rb +++ b/spec/factories/ci/pipelines.rb @@ -31,6 +31,14 @@ FactoryGirl.define do File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) end end + + # Populates pipeline with errors + # + pipeline.config_processor if evaluator.config + end + + trait :invalid do + config(rspec: nil) end end end diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index e3b73e29987..ed4acca23f1 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -8,6 +8,10 @@ FactoryGirl.define do is_shared false active true + trait :online do + contacted_at Time.now + end + trait :shared do is_shared true end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index f7fa834d7a2..1cdbe4fc9a5 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -24,6 +24,10 @@ FactoryGirl.define do visibility_level Gitlab::VisibilityLevel::PRIVATE end + trait :archived do + archived true + end + trait :access_requestable do request_access_enabled true end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 9c19db6b420..a871e370ba2 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -1,15 +1,39 @@ require 'spec_helper' feature 'Admin Groups', feature: true do + include Select2Helper + let(:internal) { Gitlab::VisibilityLevel::INTERNAL } + let(:user) { create :user } + let!(:group) { create :group } + let!(:current_user) { login_as :admin } before do - login_as(:admin) - stub_application_setting(default_group_visibility: internal) end + describe 'list' do + it 'renders groups' do + visit admin_groups_path + + expect(page).to have_content(group.name) + end + end + describe 'create a group' do + it 'creates new group' do + visit admin_groups_path + + click_link "New Group" + fill_in 'group_path', with: 'gitlab' + fill_in 'group_description', with: 'Group description' + click_button "Create group" + + expect(current_path).to eq admin_group_path(Group.find_by(path: 'gitlab')) + expect(page).to have_content('Group: gitlab') + expect(page).to have_content('Group description') + end + scenario 'shows the visibility level radio populated with the default value' do visit new_admin_group_path @@ -37,6 +61,91 @@ feature 'Admin Groups', feature: true do end end + describe 'add user into a group', js: true do + shared_context 'adds user into a group' do + it do + visit admin_group_path(group) + + select2(user_selector, from: '#user_ids', multiple: true) + page.within '#new_project_member' do + select2(Gitlab::Access::REPORTER, from: '#access_level') + end + click_button "Add users to group" + page.within ".group-users-list" do + expect(page).to have_content(user.name) + expect(page).to have_content('Reporter') + end + end + end + + it_behaves_like 'adds user into a group' do + let(:user_selector) { user.id } + end + + it_behaves_like 'adds user into a group' do + let(:user_selector) { user.email } + end + end + + describe 'add admin himself to a group' do + before do + group.add_user(:user, Gitlab::Access::OWNER) + end + + it 'adds admin a to a group as developer', js: true do + visit group_group_members_path(group) + + page.within '.users-group-form' do + select2(current_user.id, from: '#user_ids', multiple: true) + select 'Developer', from: 'access_level' + end + + click_button 'Add to group' + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + end + end + + describe 'admin remove himself from a group', js: true do + it 'removes admin from the group' do + group.add_user(current_user, Gitlab::Access::DEVELOPER) + + visit group_group_members_path(group) + + page.within '.content-list' do + expect(page).to have_content(current_user.name) + expect(page).to have_content('Developer') + end + + find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click + + visit group_group_members_path(group) + + page.within '.content-list' do + expect(page).not_to have_content(current_user.name) + expect(page).not_to have_content('Developer') + end + end + end + + describe 'shared projects' do + it 'renders shared project' do + empty_project = create(:empty_project) + empty_project.project_group_links.create!( + group_access: Gitlab::Access::MASTER, + group: group + ) + + visit admin_group_path(group) + + expect(page).to have_content(empty_project.name_with_namespace) + expect(page).to have_content('Projects shared with') + end + end + def expect_selected_visibility(level) selector = "#group_visibility_level_#{level}[checked=checked]" diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index 55ffc6761f8..a586f8d3184 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -1,9 +1,13 @@ require 'spec_helper' -describe "Admin::Users", feature: true do +describe "Admin::Users", feature: true do include WaitForAjax - before { login_as :admin } + let!(:user) do + create(:omniauth_user, provider: 'twitter', extern_uid: '123456') + end + + let!(:current_user) { login_as :admin } describe "GET /admin/users" do before do @@ -15,8 +19,10 @@ describe "Admin::Users", feature: true do end it "has users list" do - expect(page).to have_content(@user.email) - expect(page).to have_content(@user.name) + expect(page).to have_content(current_user.email) + expect(page).to have_content(current_user.name) + expect(page).to have_content(user.email) + expect(page).to have_content(user.name) end describe 'Two-factor Authentication filters' do @@ -40,8 +46,6 @@ describe "Admin::Users", feature: true do end it 'counts users who have not enabled 2FA' do - create(:user) - visit admin_users_path page.within('.filter-two-factor-disabled small') do @@ -50,8 +54,6 @@ describe "Admin::Users", feature: true do end it 'filters by users who have not enabled 2FA' do - user = create(:user) - visit admin_users_path click_link '2FA Disabled' @@ -110,10 +112,10 @@ describe "Admin::Users", feature: true do describe "GET /admin/users/:id" do it "has user info" do visit admin_users_path - click_link @user.name + click_link user.name - expect(page).to have_content(@user.email) - expect(page).to have_content(@user.name) + expect(page).to have_content(user.email) + expect(page).to have_content(user.name) end describe 'Impersonation' do @@ -126,7 +128,7 @@ describe "Admin::Users", feature: true do end it 'does not show impersonate button for admin itself' do - visit admin_user_path(@user) + visit admin_user_path(current_user) expect(page).not_to have_content('Impersonate') end @@ -158,7 +160,7 @@ describe "Admin::Users", feature: true do it 'logs out of impersonated user back to original user' do find(:css, 'li.impersonation a').click - expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(@user.username) + expect(page.find(:css, '.header-user .profile-link')['data-user']).to eql(current_user.username) end it 'is redirected back to the impersonated users page in the admin after stopping' do @@ -171,15 +173,15 @@ describe "Admin::Users", feature: true do describe 'Two-factor Authentication status' do it 'shows when enabled' do - @user.update_attribute(:otp_required_for_login, true) + user.update_attribute(:otp_required_for_login, true) - visit admin_user_path(@user) + visit admin_user_path(user) expect_two_factor_status('Enabled') end it 'shows when disabled' do - visit admin_user_path(@user) + visit admin_user_path(user) expect_two_factor_status('Disabled') end @@ -194,9 +196,8 @@ describe "Admin::Users", feature: true do describe "GET /admin/users/:id/edit" do before do - @simple_user = create(:user) visit admin_users_path - click_link "edit_user_#{@simple_user.id}" + click_link "edit_user_#{user.id}" end it "has user edit page" do @@ -214,45 +215,58 @@ describe "Admin::Users", feature: true do click_button "Save changes" end - it "shows page with new data" do + it "shows page with new data" do expect(page).to have_content('bigbang@mail.com') expect(page).to have_content('Big Bang') end it "changes user entry" do - @simple_user.reload - expect(@simple_user.name).to eq('Big Bang') - expect(@simple_user.is_admin?).to be_truthy - expect(@simple_user.password_expires_at).to be <= Time.now + user.reload + expect(user.name).to eq('Big Bang') + expect(user.is_admin?).to be_truthy + expect(user.password_expires_at).to be <= Time.now + end + end + + describe 'update username to non ascii char' do + it do + fill_in 'user_username', with: '\u3042\u3044' + click_button('Save') + + page.within '#error_explanation' do + expect(page).to have_content('Username') + end + + expect(page).to have_selector(%(form[action="/admin/users/#{user.username}"])) end end end describe "GET /admin/users/:id/projects" do + let(:group) { create(:group) } + let!(:project) { create(:project, group: group) } + before do - @group = create(:group) - @project = create(:project, group: @group) - @simple_user = create(:user) - @group.add_developer(@simple_user) + group.add_developer(user) - visit projects_admin_user_path(@simple_user) + visit projects_admin_user_path(user) end it "lists group projects" do within(:css, '.append-bottom-default + .panel') do expect(page).to have_content 'Group projects' - expect(page).to have_link @group.name, admin_group_path(@group) + expect(page).to have_link group.name, admin_group_path(group) end end it 'allows navigation to the group details' do within(:css, '.append-bottom-default + .panel') do - click_link @group.name + click_link group.name end within(:css, 'h3.page-title') do - expect(page).to have_content "Group: #{@group.name}" + expect(page).to have_content "Group: #{group.name}" end - expect(page).to have_content @project.name + expect(page).to have_content project.name end it 'shows the group access level' do @@ -270,4 +284,99 @@ describe "Admin::Users", feature: true do expect(page).not_to have_selector('.group_member') end end + + describe 'show user attributes' do + it do + visit admin_users_path + + click_link user.name + + expect(page).to have_content 'Account' + expect(page).to have_content 'Personal projects limit' + end + end + + describe 'remove users secondary email', js: true do + let!(:secondary_email) do + create :email, email: 'secondary@example.com', user: user + end + + it do + visit admin_user_path(user.username) + + expect(page).to have_content("Secondary email: #{secondary_email.email}") + + find("#remove_email_#{secondary_email.id}").click + + expect(page).not_to have_content(secondary_email.email) + end + end + + describe 'show user keys' do + let!(:key1) do + create(:key, user: user, title: "ssh-rsa Key1", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC4FIEBXGi4bPU8kzxMefudPIJ08/gNprdNTaO9BR/ndy3+58s2HCTw2xCHcsuBmq+TsAqgEidVq4skpqoTMB+Uot5Uzp9z4764rc48dZiI661izoREoKnuRQSsRqUTHg5wrLzwxlQbl1MVfRWQpqiz/5KjBC7yLEb9AbusjnWBk8wvC1bQPQ1uLAauEA7d836tgaIsym9BrLsMVnR4P1boWD3Xp1B1T/ImJwAGHvRmP/ycIqmKdSpMdJXwxcb40efWVj0Ibbe7ii9eeoLdHACqevUZi6fwfbymdow+FeqlkPoHyGg3Cu4vD/D8+8cRc7mE/zGCWcQ15Var83Tczour Key1") + end + + let!(:key2) do + create(:key, user: user, title: "ssh-rsa Key2", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQSTWXhJAX/He+nG78MiRRRn7m0Pb0XbcgTxE0etArgoFoh9WtvDf36HG6tOSg/0UUNcp0dICsNAmhBKdncp6cIyPaXJTURPRAGvhI0/VDk4bi27bRnccGbJ/hDaUxZMLhhrzY0r22mjVf8PF6dvv5QUIQVm1/LeaWYsHHvLgiIjwrXirUZPnFrZw6VLREoBKG8uWvfSXw1L5eapmstqfsME8099oi+vWLR8MgEysZQmD28M73fgW4zek6LDQzKQyJx9nB+hJkKUDvcuziZjGmRFlNgSA2mguERwL1OXonD8WYUrBDGKroIvBT39zS5d9tQDnidEJZ9Y8gv5ViYP7x Key2") + end + + it do + visit admin_users_path + + click_link user.name + click_link 'SSH keys' + + expect(page).to have_content(key1.title) + expect(page).to have_content(key2.title) + + click_link key2.title + + expect(page).to have_content(key2.title) + expect(page).to have_content(key2.key) + + click_link 'Remove' + + expect(page).not_to have_content(key2.title) + end + end + + describe 'show user identities' do + it 'shows user identities' do + visit admin_user_identities_path(user) + + expect(page).to have_content(user.name) + expect(page).to have_content('twitter') + end + end + + describe 'update user identities' do + before do + allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated]) + end + + it 'modifies twitter identity' do + visit admin_user_identities_path(user) + + find('.table').find(:link, 'Edit').click + fill_in 'identity_extern_uid', with: '654321' + select 'twitter_updated', from: 'identity_provider' + click_button 'Save changes' + + expect(page).to have_content(user.name) + expect(page).to have_content('twitter_updated') + expect(page).to have_content('654321') + end + end + + describe 'remove user with identities' do + it 'removes user with twitter identity' do + visit admin_user_identities_path(user) + + click_link 'Delete' + + expect(page).to have_content(user.name) + expect(page).not_to have_content('twitter') + end + end end diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb index 81077f4b005..3ebc432206a 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/ci_lint_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'CI Lint' do +describe 'CI Lint', js: true do before do login_as :user end @@ -8,7 +8,10 @@ describe 'CI Lint' do describe 'YAML parsing' do before do visit ci_lint_path - fill_in 'content', with: yaml_content + # Ace editor updates a hidden textarea and it happens asynchronously + # `sleep 0.1` is actually needed here because of this + execute_script("ace.edit('ci-editor').setValue(" + yaml_content.to_json + ");") + sleep 0.1 click_on 'Validate' end @@ -40,7 +43,7 @@ describe 'CI Lint' do let(:yaml_content) { 'my yaml content' } it 'loads previous YAML content after validation' do - expect(page).to have_field('content', with: 'my yaml content', type: 'textarea') + expect(page).to have_field('content', with: 'my yaml content', visible: false, type: 'textarea') end end end diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index e48a2b0c92e..0648c89a5c7 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' feature 'Cycle Analytics', feature: true, js: true do include WaitForAjax - let(:project) { create(:project) } let(:user) { create(:user) } let(:guest) { create(:user) } let(:project) { create(:project) } diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb new file mode 100644 index 00000000000..7d59fcac517 --- /dev/null +++ b/spec/features/dashboard/active_tab_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Active Tab', feature: true do + before do + login_as :user + end + + shared_examples 'page has active tab' do |title| + it "#{title} tab" do + expect(page).to have_selector('.nav-sidebar li.active', count: 1) + expect(find('.nav-sidebar li.active')).to have_content(title) + end + end + + context 'on dashboard projects' do + before do + visit dashboard_projects_path + end + + it_behaves_like 'page has active tab', 'Projects' + end + + context 'on dashboard issues' do + before do + visit issues_dashboard_path + end + + it_behaves_like 'page has active tab', 'Issues' + end + + context 'on dashboard merge requests' do + before do + visit merge_requests_dashboard_path + end + + it_behaves_like 'page has active tab', 'Merge Requests' + end + + context 'on dashboard groups' do + before do + visit dashboard_groups_path + end + + it_behaves_like 'page has active tab', 'Groups' + end +end diff --git a/spec/features/dashboard/archived_projects_spec.rb b/spec/features/dashboard/archived_projects_spec.rb new file mode 100644 index 00000000000..038c1641be9 --- /dev/null +++ b/spec/features/dashboard/archived_projects_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Archived Project', feature: true do + let(:user) { create :user } + let(:project) { create :project} + let(:archived_project) { create(:project, :archived) } + + before do + project.team << [user, :master] + archived_project.team << [user, :master] + + login_as(user) + + visit dashboard_projects_path + end + + it 'renders non archived projects' do + expect(page).to have_link(project.name) + expect(page).not_to have_link(archived_project.name) + end + + it 'renders all projects' do + click_link 'Show archived projects' + + expect(page).to have_link(project.name) + expect(page).to have_link(archived_project.name) + end +end diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb new file mode 100644 index 00000000000..d5f8470fab0 --- /dev/null +++ b/spec/features/dashboard/group_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Group', feature: true do + before do + login_as(:user) + end + + it 'creates new grpup' do + visit dashboard_groups_path + click_link 'New Group' + + fill_in 'group_path', with: 'Samurai' + fill_in 'group_description', with: 'Tokugawa Shogunate' + click_button 'Create group' + + expect(current_path).to eq group_path(Group.find_by(name: 'Samurai')) + expect(page).to have_content('Samurai') + expect(page).to have_content('Tokugawa Shogunate') + end +end diff --git a/spec/features/dashboard/help_spec.rb b/spec/features/dashboard/help_spec.rb new file mode 100644 index 00000000000..2803f7ec62b --- /dev/null +++ b/spec/features/dashboard/help_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +RSpec.describe 'Dashboard Help', feature: true do + before do + login_as(:user) + end + + it 'renders correctly markdown' do + visit help_page_path("administration/raketasks/maintenance") + + expect(page).to have_content('Gather information about GitLab and the system it runs on') + + node = find('.documentation h2 a#user-content-check-gitlab-configuration') + expect(node[:href]).to eq '#check-gitlab-configuration' + expect(find(:xpath, "#{node.path}/..").text).to eq 'Check GitLab configuration' + end +end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 3934c936f20..8b3e2fa93a2 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -4,10 +4,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do include WaitForAjax let(:branch) { 'expand-collapse-diffs' } + let(:project) { create(:project) } before do login_as :admin - project = create(:project) # Ensure that undiffable.md is in .gitattributes project.repository.copy_gitattributes(branch) @@ -31,6 +31,33 @@ feature 'Expand and collapse diffs', js: true, feature: true do define_method(file.split('.').first) { file_container(file) } end + it 'should show the diff content with a highlighted line when linking to line' do + expect(large_diff).not_to have_selector('.code') + expect(large_diff).to have_selector('.nothing-here-block') + + visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: "#{large_diff[:id]}_0_1") + execute_script('window.location.reload()') + + wait_for_ajax + + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + expect(large_diff).to have_selector('.hll') + end + + it 'should show the diff content when linking to file' do + expect(large_diff).not_to have_selector('.code') + expect(large_diff).to have_selector('.nothing-here-block') + + visit namespace_project_commit_path(project.namespace, project, project.commit(branch), anchor: large_diff[:id]) + execute_script('window.location.reload()') + + wait_for_ajax + + expect(large_diff).to have_selector('.code') + expect(large_diff).not_to have_selector('.nothing-here-block') + end + context 'visiting a commit with collapsed diffs' do it 'shows small diffs immediately' do expect(small_diff).to have_selector('.code') diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb deleted file mode 100644 index 9dfa5d1de19..00000000000 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'rails_helper' - -feature 'Issue filtering by Milestone', feature: true do - let(:project) { create(:project, :public) } - let(:milestone) { create(:milestone, project: project) } - - scenario 'filters by no Milestone', js: true do - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::None.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'No Milestone') - expect(page).to have_css('.issue', count: 1) - end - - context 'filters by upcoming milestone', js: true do - it 'does not show issues with no expiry' do - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 0) - end - - it 'shows issues in future' do - milestone = create(:milestone, project: project, due_date: Date.tomorrow) - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 1) - end - - it 'does not show issues in past' do - milestone = create(:milestone, project: project, due_date: Date.yesterday) - create(:issue, project: project) - create(:issue, project: project, milestone: milestone) - - visit_issues(project) - filter_by_milestone(Milestone::Upcoming.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: 'Upcoming') - expect(page).to have_css('.issue', count: 0) - end - end - - scenario 'filters by a specific Milestone', js: true do - create(:issue, project: project, milestone: milestone) - create(:issue, project: project) - - visit_issues(project) - filter_by_milestone(milestone.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) - expect(page).to have_css('.issue', count: 1) - end - - context 'when milestone has single quotes in title' do - background do - milestone.update(name: "rock 'n' roll") - end - - scenario 'filters by a specific Milestone', js: true do - create(:issue, project: project, milestone: milestone) - create(:issue, project: project) - - visit_issues(project) - filter_by_milestone(milestone.title) - - expect(page).to have_css('.milestone-filter .dropdown-toggle-text', text: milestone.title) - expect(page).to have_css('.issue', count: 1) - end - end - - def visit_issues(project) - visit namespace_project_issues_path(project.namespace, project) - end - - def filter_by_milestone(title) - find(".js-milestone-select").click - find(".milestone-filter .dropdown-content a", text: title).click - end -end diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb new file mode 100644 index 00000000000..6f6a2532c04 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb @@ -0,0 +1,166 @@ +require 'rails_helper' + +describe 'Dropdown assignee', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_assignee) { '#js-dropdown-assignee' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 5 + wait_for_ajax + end + end + + def dropdown_assignee_size + page.all('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item').size + end + + def click_assignee(text) + find('#js-dropdown-assignee .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + project.team << [user_john, :master] + project.team << [user_jacob, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has assignee:' do + filtered_search.set('assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + + expect(page).to have_css(js_dropdown_assignee, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('assignee:') + + expect(page).to have_css('#js-dropdown-assignee .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('assignee:') + + expect(page).not_to have_css('#js-dropdown-assignee .filter-dropdown-loading') + end + + it 'should load all the assignees when opened' do + send_keys_to_filtered_search('assignee:') + + expect(dropdown_assignee_size).to eq(3) + end + end + + describe 'filtering' do + before do + send_keys_to_filtered_search('assignee:') + end + + it 'filters by name' do + send_keys_to_filtered_search('j') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('J') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by username with symbol' do + send_keys_to_filtered_search('@ot') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive username with symbol' do + send_keys_to_filtered_search('@OT') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by username without symbol' do + send_keys_to_filtered_search('ot') + + expect(dropdown_assignee_size).to eq(2) + end + + it 'filters by case insensitive username without symbol' do + send_keys_to_filtered_search('OT') + + expect(dropdown_assignee_size).to eq(2) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('assignee:') + end + + it 'fills in the assignee username when the assignee has not been filtered' do + click_assignee(user_jacob.name) + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}") + end + + it 'fills in the assignee username when the assignee has been filtered' do + send_keys_to_filtered_search('roo') + click_assignee(user.name) + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:@#{user.username}") + end + + it 'selects `no assignee`' do + find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click + + expect(page).to have_css(js_dropdown_assignee, visible: false) + expect(filtered_search.value).to eq("assignee:none") + end + end + + describe 'input has existing content' do + it 'opens assignee dropdown with existing search term' do + filtered_search.set('searchTerm assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing author' do + filtered_search.set('author:@user assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing label' do + filtered_search.set('label:~bug assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + + it 'opens assignee dropdown with existing milestone' do + filtered_search.set('milestone:%v1.0 assignee:') + + expect(page).to have_css(js_dropdown_assignee, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb new file mode 100644 index 00000000000..60a86cc93d4 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -0,0 +1,154 @@ +require 'rails_helper' + +describe 'Dropdown author', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user, name: 'administrator', username: 'root') } + let!(:user_john) { create(:user, name: 'John', username: 'th0mas') } + let!(:user_jacob) { create(:user, name: 'Jacob', username: 'otter32') } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_author) { '#js-dropdown-author' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 5 + wait_for_ajax + end + end + + def dropdown_author_size + page.all('#js-dropdown-author .filter-dropdown .filter-dropdown-item').size + end + + def click_author(text) + find('#js-dropdown-author .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + project.team << [user_john, :master] + project.team << [user_jacob, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has author:' do + filtered_search.set('author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + + expect(page).to have_css(js_dropdown_author, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('author:') + + expect(page).to have_css('#js-dropdown-author .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('author:') + + expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading') + end + + it 'should load all the authors when opened' do + send_keys_to_filtered_search('author:') + + expect(dropdown_author_size).to eq(3) + end + end + + describe 'filtering' do + before do + filtered_search.set('author') + send_keys_to_filtered_search(':') + end + + it 'filters by name' do + send_keys_to_filtered_search('ja') + + expect(dropdown_author_size).to eq(1) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search('Ja') + + expect(dropdown_author_size).to eq(1) + end + + it 'filters by username with symbol' do + send_keys_to_filtered_search('@ot') + + expect(dropdown_author_size).to eq(2) + end + + it 'filters by username without symbol' do + send_keys_to_filtered_search('ot') + + expect(dropdown_author_size).to eq(2) + end + + it 'filters by case insensitive username without symbol' do + send_keys_to_filtered_search('OT') + + expect(dropdown_author_size).to eq(2) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('author') + send_keys_to_filtered_search(':') + end + + it 'fills in the author username when the author has not been filtered' do + click_author(user_jacob.name) + + expect(page).to have_css(js_dropdown_author, visible: false) + expect(filtered_search.value).to eq("author:@#{user_jacob.username}") + end + + it 'fills in the author username when the author has been filtered' do + click_author(user.name) + + expect(page).to have_css(js_dropdown_author, visible: false) + expect(filtered_search.value).to eq("author:@#{user.username}") + end + end + + describe 'input has existing content' do + it 'opens author dropdown with existing search term' do + filtered_search.set('searchTerm author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing assignee' do + filtered_search.set('assignee:@user author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing label' do + filtered_search.set('label:~bug author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + + it 'opens author dropdown with existing milestone' do + filtered_search.set('milestone:%v1.0 author:') + + expect(page).to have_css(js_dropdown_author, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb new file mode 100644 index 00000000000..04dd54ab459 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb @@ -0,0 +1,134 @@ +require 'rails_helper' + +describe 'Dropdown hint', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_hint) { '#js-dropdown-hint' } + + def dropdown_hint_size + page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size + end + + def click_hint(text) + find('#js-dropdown-hint .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + before do + expect(page).to have_css(js_dropdown_hint, visible: false) + filtered_search.click + end + + it 'opens when the search bar is first focused' do + expect(page).to have_css(js_dropdown_hint, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click + + expect(page).to have_css(js_dropdown_hint, visible: false) + end + end + + describe 'filtering' do + it 'does not filter `Keep typing and press Enter`' do + filtered_search.set('randomtext') + + expect(page).to have_css(js_dropdown_hint, text: 'Keep typing and press Enter', visible: false) + expect(dropdown_hint_size).to eq(0) + end + + it 'filters with text' do + filtered_search.set('a') + + expect(dropdown_hint_size).to eq(3) + end + end + + describe 'selecting from dropdown with no input' do + before do + filtered_search.click + end + + it 'opens the author dropdown when you click on author' do + click_hint('author') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-author', visible: true) + expect(filtered_search.value).to eq('author:') + end + + it 'opens the assignee dropdown when you click on assignee' do + click_hint('assignee') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-assignee', visible: true) + expect(filtered_search.value).to eq('assignee:') + end + + it 'opens the milestone dropdown when you click on milestone' do + click_hint('milestone') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-milestone', visible: true) + expect(filtered_search.value).to eq('milestone:') + end + + it 'opens the label dropdown when you click on label' do + click_hint('label') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-label', visible: true) + expect(filtered_search.value).to eq('label:') + end + end + + describe 'selecting from dropdown with some input' do + it 'opens the author dropdown when you click on author' do + filtered_search.set('auth') + click_hint('author') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-author', visible: true) + expect(filtered_search.value).to eq('author:') + end + + it 'opens the assignee dropdown when you click on assignee' do + filtered_search.set('assign') + click_hint('assignee') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-assignee', visible: true) + expect(filtered_search.value).to eq('assignee:') + end + + it 'opens the milestone dropdown when you click on milestone' do + filtered_search.set('mile') + click_hint('milestone') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-milestone', visible: true) + expect(filtered_search.value).to eq('milestone:') + end + + it 'opens the label dropdown when you click on label' do + filtered_search.set('lab') + click_hint('label') + + expect(page).to have_css(js_dropdown_hint, visible: false) + expect(page).to have_css('#js-dropdown-label', visible: true) + expect(filtered_search.value).to eq('label:') + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb new file mode 100644 index 00000000000..89c144141c9 --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -0,0 +1,242 @@ +require 'rails_helper' + +describe 'Dropdown label', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:uppercase_label) { create(:label, project: project, title: 'BUG') } + let!(:two_words_label) { create(:label, project: project, title: 'High Priority') } + let!(:wont_fix_label) { create(:label, project: project, title: 'Won"t Fix') } + let!(:wont_fix_single_label) { create(:label, project: project, title: 'Won\'t Fix') } + let!(:special_label) { create(:label, project: project, title: '!@#$%^+&*()')} + let!(:long_label) { create(:label, project: project, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title')} + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_label) { '#js-dropdown-label' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + sleep 3 + end + end + + def dropdown_label_size + page.all('#js-dropdown-label .filter-dropdown .filter-dropdown-item').size + end + + def click_label(text) + find('#js-dropdown-label .filter-dropdown .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has label:' do + filtered_search.set('label:') + + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + + expect(page).to have_css(js_dropdown_label, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('label:') + + expect(page).to have_css('#js-dropdown-label .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('label:') + + expect(page).not_to have_css('#js-dropdown-label .filter-dropdown-loading') + end + + it 'should load all the labels when opened' do + send_keys_to_filtered_search('label:') + + expect(dropdown_label_size).to be > 0 + end + end + + describe 'filtering' do + before do + filtered_search.set('label') + end + + it 'filters by name' do + send_keys_to_filtered_search(':b') + + expect(dropdown_label_size).to eq(2) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search(':B') + + expect(dropdown_label_size).to eq(2) + end + + it 'filters by name with symbol' do + send_keys_to_filtered_search(':~bu') + + expect(dropdown_label_size).to eq(2) + end + + it 'filters by case insensitive name with symbol' do + send_keys_to_filtered_search(':~BU') + + expect(dropdown_label_size).to eq(2) + end + + it 'filters by multiple words' do + send_keys_to_filtered_search(':Hig') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words with symbol' do + send_keys_to_filtered_search(':~Hig') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing single quotes' do + send_keys_to_filtered_search(':won\'t') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing single quotes with symbol' do + send_keys_to_filtered_search(':~won\'t') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing double quotes' do + send_keys_to_filtered_search(':won"t') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by multiple words containing double quotes with symbol' do + send_keys_to_filtered_search(':~won"t') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by special characters' do + send_keys_to_filtered_search(':^+') + + expect(dropdown_label_size).to eq(1) + end + + it 'filters by special characters with symbol' do + send_keys_to_filtered_search(':~^+') + + expect(dropdown_label_size).to eq(1) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('label:') + end + + it 'fills in the label name when the label has not been filled' do + click_label(bug_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{bug_label.title}") + end + + it 'fills in the label name when the label is partially filled' do + send_keys_to_filtered_search('bu') + click_label(bug_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{bug_label.title}") + end + + it 'fills in the label name that contains multiple words' do + click_label(two_words_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"") + end + + it 'fills in the label name that contains multiple words and is very long' do + click_label(long_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"") + end + + it 'fills in the label name that contains double quotes' do + click_label(wont_fix_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'") + end + + it 'fills in the label name with the correct capitalization' do + click_label(uppercase_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{uppercase_label.title}") + end + + it 'fills in the label name with special characters' do + click_label(special_label.title) + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:~#{special_label.title}") + end + + it 'selects `no label`' do + find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click + + expect(page).to have_css(js_dropdown_label, visible: false) + expect(filtered_search.value).to eq("label:none") + end + end + + describe 'input has existing content' do + it 'opens label dropdown with existing search term' do + filtered_search.set('searchTerm label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing author' do + filtered_search.set('author:@person label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing assignee' do + filtered_search.set('assignee:@person label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing label' do + filtered_search.set('label:~urgent label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + + it 'opens label dropdown with existing milestone' do + filtered_search.set('milestone:%v2.0 label:') + expect(page).to have_css(js_dropdown_label, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb new file mode 100644 index 00000000000..e5a271b663f --- /dev/null +++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb @@ -0,0 +1,222 @@ +require 'rails_helper' + +describe 'Dropdown milestone', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let!(:milestone) { create(:milestone, title: 'v1.0', project: project) } + let!(:uppercase_milestone) { create(:milestone, title: 'CAP_MILESTONE', project: project) } + let!(:two_words_milestone) { create(:milestone, title: 'Future Plan', project: project) } + let!(:wont_fix_milestone) { create(:milestone, title: 'Won"t Fix', project: project) } + let!(:special_milestone) { create(:milestone, title: '!@#$%^&*(+)', project: project) } + let!(:long_milestone) { create(:milestone, title: 'this is a very long title this is a very long title this is a very long title this is a very long title this is a very long title', project: project) } + + let(:filtered_search) { find('.filtered-search') } + let(:js_dropdown_milestone) { '#js-dropdown-milestone' } + + def send_keys_to_filtered_search(input) + input.split("").each do |i| + filtered_search.send_keys(i) + sleep 3 + wait_for_ajax + sleep 3 + end + end + + def dropdown_milestone_size + page.all('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item').size + end + + def click_milestone(text) + find('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item', text: text).click + end + + def click_static_milestone(text) + find('#js-dropdown-milestone .filter-dropdown-item', text: text).click + end + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'behavior' do + it 'opens when the search bar has milestone:' do + filtered_search.set('milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'closes when the search bar is unfocused' do + find('body').click() + + expect(page).to have_css(js_dropdown_milestone, visible: false) + end + + it 'should show loading indicator when opened' do + filtered_search.set('milestone:') + + expect(page).to have_css('#js-dropdown-milestone .filter-dropdown-loading', visible: true) + end + + it 'should hide loading indicator when loaded' do + send_keys_to_filtered_search('milestone:') + + expect(page).not_to have_css('#js-dropdown-milestone .filter-dropdown-loading') + end + + it 'should load all the milestones when opened' do + send_keys_to_filtered_search('milestone:') + + expect(dropdown_milestone_size).to be > 0 + end + end + + describe 'filtering' do + before do + filtered_search.set('milestone') + end + + it 'filters by name' do + send_keys_to_filtered_search(':v1') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by case insensitive name' do + send_keys_to_filtered_search(':V1') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by name with symbol' do + send_keys_to_filtered_search(':%v1') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by case insensitive name with symbol' do + send_keys_to_filtered_search(':%V1') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by special characters' do + send_keys_to_filtered_search(':(+') + + expect(dropdown_milestone_size).to eq(1) + end + + it 'filters by special characters with symbol' do + send_keys_to_filtered_search(':%(+') + + expect(dropdown_milestone_size).to eq(1) + end + end + + describe 'selecting from dropdown' do + before do + filtered_search.set('milestone:') + end + + it 'fills in the milestone name when the milestone has not been filled' do + click_milestone(milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + end + + it 'fills in the milestone name when the milestone is partially filled' do + send_keys_to_filtered_search('v') + click_milestone(milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{milestone.title}") + end + + it 'fills in the milestone name that contains multiple words' do + click_milestone(two_words_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"") + end + + it 'fills in the milestone name that contains multiple words and is very long' do + click_milestone(long_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"") + end + + it 'fills in the milestone name that contains double quotes' do + click_milestone(wont_fix_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'") + end + + it 'fills in the milestone name with the correct capitalization' do + click_milestone(uppercase_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}") + end + + it 'fills in the milestone name with special characters' do + click_milestone(special_milestone.title) + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}") + end + + it 'selects `no milestone`' do + click_static_milestone('No Milestone') + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:none") + end + + it 'selects `upcoming milestone`' do + click_static_milestone('Upcoming') + + expect(page).to have_css(js_dropdown_milestone, visible: false) + expect(filtered_search.value).to eq("milestone:upcoming") + end + end + + describe 'input has existing content' do + it 'opens milestone dropdown with existing search term' do + filtered_search.set('searchTerm milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing author' do + filtered_search.set('author:@john milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing assignee' do + filtered_search.set('assignee:@john milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing label' do + filtered_search.set('label:~important milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + + it 'opens milestone dropdown with existing milestone' do + filtered_search.set('milestone:%100 milestone:') + + expect(page).to have_css(js_dropdown_milestone, visible: true) + end + end +end diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb new file mode 100644 index 00000000000..ead43d6784a --- /dev/null +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -0,0 +1,759 @@ +require 'rails_helper' + +describe 'Filter issues', js: true, feature: true do + include WaitForAjax + + let!(:group) { create(:group) } + let!(:project) { create(:project, group: group) } + let!(:user) { create(:user) } + let!(:user2) { create(:user) } + let!(:milestone) { create(:milestone, project: project) } + let!(:label) { create(:label, project: project) } + let!(:wontfix) { create(:label, project: project, title: "Won't fix") } + + let!(:bug_label) { create(:label, project: project, title: 'bug') } + let!(:caps_sensitive_label) { create(:label, project: project, title: 'CAPS_sensitive') } + let!(:milestone) { create(:milestone, title: "8", project: project) } + let!(:multiple_words_label) { create(:label, project: project, title: "Two words") } + + let!(:closed_issue) { create(:issue, title: 'bug that is closed', project: project, state: :closed) } + let(:filtered_search) { find('.filtered-search') } + + def input_filtered_search(search_term) + filtered_search.set(search_term) + filtered_search.send_keys(:enter) + end + + def expect_filtered_search_input(input) + expect(find('.filtered-search').value).to eq(input) + end + + def expect_no_issues_list + page.within '.issues-list' do + expect(page).not_to have_selector('.issue') + end + end + + def expect_issues_list_count(open_count, closed_count = 0) + all_count = open_count + closed_count + + expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: open_count) + end + end + + before do + project.team << [user, :master] + project.team << [user2, :master] + group.add_developer(user) + group.add_developer(user2) + login_as(user) + create(:issue, project: project) + + create(:issue, title: "Bug report 1", project: project) + create(:issue, title: "Bug report 2", project: project) + create(:issue, title: "issue with 'single quotes'", project: project) + create(:issue, title: "issue with \"double quotes\"", project: project) + create(:issue, title: "issue with !@\#{$%^&*()-+", project: project) + create(:issue, title: "issue by assignee", project: project, milestone: milestone, author: user, assignee: user) + create(:issue, title: "issue by assignee with searchTerm", project: project, milestone: milestone, author: user, assignee: user) + + issue = create(:issue, + title: "Bug 2", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue.labels << bug_label + + issue_with_caps_label = create(:issue, + title: "issue by assignee with searchTerm and label", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_caps_label.labels << caps_sensitive_label + + issue_with_everything = create(:issue, + title: "Bug report with everything you thought was possible", + project: project, + milestone: milestone, + author: user, + assignee: user) + issue_with_everything.labels << bug_label + issue_with_everything.labels << caps_sensitive_label + + multiple_words_label_issue = create(:issue, title: "Issue with multiple words label", project: project) + multiple_words_label_issue.labels << multiple_words_label + + future_milestone = create(:milestone, title: "future", project: project, due_date: Time.now + 1.month) + + create(:issue, + title: "Issue with future milestone", + milestone: future_milestone, + project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + describe 'filter issues by author' do + context 'only author' do + it 'filters issues by searched author' do + input_filtered_search("author:@#{user.username}") + + expect_issues_list_count(5) + end + + it 'filters issues by invalid author' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by multiple authors' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + + context 'author with other filters' do + it 'filters issues by searched author and text' do + search = "author:@#{user.username} issue" + input_filtered_search(search) + + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee and text' do + search = "author:@#{user.username} assignee:@#{user.username} issue" + input_filtered_search(search) + + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee, label, and text' do + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} issue" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched author, assignee, label, milestone and text' do + search = "author:@#{user.username} assignee:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} issue" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + it 'sorting' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + + describe 'filter issues by assignee' do + context 'only assignee' do + it 'filters issues by searched assignee' do + search = "assignee:@#{user.username}" + input_filtered_search(search) + + expect_issues_list_count(5) + expect_filtered_search_input(search) + end + + it 'filters issues by no assignee' do + search = "assignee:none" + input_filtered_search(search) + + expect_issues_list_count(8, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by invalid assignee' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by multiple assignees' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + + context 'assignee with other filters' do + it 'filters issues by searched assignee and text' do + search = "assignee:@#{user.username} searchTerm" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author and text' do + search = "assignee:@#{user.username} author:@#{user.username} searchTerm" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author, label, text' do + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} searchTerm" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched assignee, author, label, milestone and text' do + search = "assignee:@#{user.username} author:@#{user.username} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} searchTerm" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'sorting' do + it 'sorts' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + end + + describe 'filter issues by label' do + context 'only label' do + it 'filters issues by searched label' do + search = "label:~#{bug_label.title}" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by no label' do + search = "label:none" + input_filtered_search(search) + + expect_issues_list_count(9, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by invalid label' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by multiple labels' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title}" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by label containing special characters' do + special_label = create(:label, project: project, title: '!@#{$%^&*()-+[]<>?/:{}|\}') + special_issue = create(:issue, title: "Issue with special character label", project: project) + special_issue.labels << special_label + + search = "label:~#{special_label.title}" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show issues' do + new_label = create(:label, project: project, title: "new_label") + + search = "label:~#{new_label.title}" + input_filtered_search(search) + + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'label with multiple words' do + it 'special characters' do + special_multiple_label = create(:label, project: project, title: "Utmost |mp0rt@nce") + special_multiple_issue = create(:issue, title: "Issue with special character multiple words label", project: project) + special_multiple_issue.labels << special_multiple_label + + search = "label:~'#{special_multiple_label.title}'" + input_filtered_search(search) + + expect_issues_list_count(1) + + # filtered search defaults quotations to double quotes + expect_filtered_search_input("label:~\"#{special_multiple_label.title}\"") + end + + it 'single quotes' do + search = "label:~'#{multiple_words_label.title}'" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input("label:~\"#{multiple_words_label.title}\"") + end + + it 'double quotes' do + search = "label:~\"#{multiple_words_label.title}\"" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'single quotes containing double quotes' do + double_quotes_label = create(:label, project: project, title: 'won"t fix') + double_quotes_label_issue = create(:issue, title: "Issue with double quotes label", project: project) + double_quotes_label_issue.labels << double_quotes_label + + search = "label:~'#{double_quotes_label.title}'" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'double quotes containing single quotes' do + single_quotes_label = create(:label, project: project, title: "won't fix") + single_quotes_label_issue = create(:issue, title: "Issue with single quotes label", project: project) + single_quotes_label_issue.labels << single_quotes_label + + search = "label:~\"#{single_quotes_label.title}\"" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'label with other filters' do + it 'filters issues by searched label and text' do + search = "label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author, assignee and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, author, assignee, milestone and text' do + search = "label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'multiple labels with other filters' do + it 'filters issues by searched label, label2, and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author, assignee and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched label, label2, author, assignee, milestone and text' do + search = "label:~#{bug_label.title} label:~#{caps_sensitive_label.title} author:@#{user.username} assignee:@#{user.username} milestone:%#{milestone.title} bug" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + end + + context 'issue label clicked' do + before do + find('.issues-list .issue .issue-info a .label', text: multiple_words_label.title).click + sleep 1 + end + + it 'filters' do + expect_issues_list_count(1) + end + + it 'displays in search bar' do + expect(find('.filtered-search').value).to eq("label:~\"#{multiple_words_label.title}\"") + end + end + + context 'sorting' do + it 'sorts' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + end + + describe 'filter issues by milestone' do + context 'only milestone' do + it 'filters issues by searched milestone' do + input_filtered_search("milestone:%#{milestone.title}") + + expect_issues_list_count(5) + end + + it 'filters issues by no milestone' do + input_filtered_search("milestone:none") + + expect_issues_list_count(7, 1) + end + + it 'filters issues by upcoming milestones' do + input_filtered_search("milestone:upcoming") + + expect_issues_list_count(1) + end + + it 'filters issues by invalid milestones' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by multiple milestones' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + + it 'filters issues by milestone containing special characters' do + special_milestone = create(:milestone, title: '!@\#{$%^&*()}', project: project) + create(:issue, title: "Issue with special character milestone", project: project, milestone: special_milestone) + + search = "milestone:%#{special_milestone.title}" + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show issues' do + new_milestone = create(:milestone, title: "new", project: project) + + search = "milestone:%#{new_milestone.title}" + input_filtered_search(search) + + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'milestone with other filters' do + it 'filters issues by searched milestone and text' do + search = "milestone:%#{milestone.title} bug" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author, assignee and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} bug" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + + it 'filters issues by searched milestone, author, assignee, label and text' do + search = "milestone:%#{milestone.title} author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug" + input_filtered_search(search) + + expect_issues_list_count(2) + expect_filtered_search_input(search) + end + end + + context 'sorting' do + it 'sorts' do + pending('to be tested, issue #26546') + expect(true).to be(false) + end + end + end + + describe 'filter issues by text' do + context 'only text' do + it 'filters issues by searched text' do + search = 'Bug' + input_filtered_search(search) + + expect_issues_list_count(4, 1) + expect_filtered_search_input(search) + end + + it 'filters issues by multiple searched text' do + search = 'Bug report' + input_filtered_search(search) + + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by case insensitive searched text' do + search = 'bug report' + input_filtered_search(search) + + expect_issues_list_count(3) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing single quotes' do + search = '\'single quotes\'' + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing double quotes' do + search = '"double quotes"' + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'filters issues by searched text containing special characters' do + search = '!@#{$%^&*()-+' + input_filtered_search(search) + + expect_issues_list_count(1) + expect_filtered_search_input(search) + end + + it 'does not show any issues' do + search = 'testing' + input_filtered_search(search) + + expect_no_issues_list() + expect_filtered_search_input(search) + end + end + + context 'searched text with other filters' do + it 'filters issues by searched text and author' do + input_filtered_search("bug author:@#{user.username}") + + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} bug") + end + + it 'filters issues by searched text, author and more text' do + input_filtered_search("bug author:@#{user.username} report") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} bug report") + end + + it 'filters issues by searched text, author and assignee' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username}") + + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug") + end + + it 'filters issues by searched text, author, more text and assignee' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username}") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report") + end + + it 'filters issues by searched text, author, more text, assignee and even more text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} bug report with") + end + + it 'filters issues by searched text, author, assignee and label' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title}") + + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} bug report with everything") + end + + it 'filters issues by searched text, author, assignee, label and milestone' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title}") + + expect_issues_list_count(2) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label, text, milestone and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything milestone:%#{milestone.title} you") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} milestone:%#{milestone.title} bug report with everything you") + end + + it 'filters issues by searched text, author, assignee, multiple labels and milestone' do + input_filtered_search("bug author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title}") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug") + end + + it 'filters issues by searched text, author, text, assignee, text, label1, text, label2, text, milestone and text' do + input_filtered_search("bug author:@#{user.username} report assignee:@#{user.username} with label:~#{bug_label.title} everything label:~#{caps_sensitive_label.title} you milestone:%#{milestone.title} thought") + + expect_issues_list_count(1) + expect_filtered_search_input("author:@#{user.username} assignee:@#{user.username} label:~#{bug_label.title} label:~#{caps_sensitive_label.title} milestone:%#{milestone.title} bug report with everything you thought") + end + end + + context 'sorting' do + it 'sorts by oldest updated' do + create(:issue, + title: '3 days ago', + project: project, + author: user, + created_at: 3.days.ago, + updated_at: 3.days.ago) + + old_issue = create(:issue, + title: '5 days ago', + project: project, + author: user, + created_at: 5.days.ago, + updated_at: 5.days.ago) + + input_filtered_search('days ago') + + expect_issues_list_count(2) + + sort_toggle = find('.filtered-search-container .dropdown-toggle') + sort_toggle.click + + find('.filtered-search-container .dropdown-menu li a', text: 'Oldest updated').click + wait_for_ajax + + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(old_issue.title) + end + end + end + + describe 'retains filter when switching issue states' do + before do + input_filtered_search('bug') + + # Wait for search results to load + sleep 2 + end + + it 'open state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + + find('.issues-state-filters a', text: 'Open').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 4) + end + + it 'closed state' do + find('.issues-state-filters a', text: 'Closed').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 1) + expect(find('.issues-list .issue:first-of-type .issue-title-text a')).to have_content(closed_issue.title) + end + + it 'all state' do + find('.issues-state-filters a', text: 'All').click + wait_for_ajax + + expect(page).to have_selector('.issues-list .issue', count: 5) + end + end + + describe 'RSS feeds' do + it 'updates atom feed link for project issues' do + visit namespace_project_issues_path(project.namespace, project, milestone_title: milestone.title, assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI.parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => [milestone.title]) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end + + it 'updates atom feed link for group issues' do + visit issues_group_path(group, milestone_title: milestone.title, assignee_id: user.id) + link = find('.nav-controls a', text: 'Subscribe') + params = CGI.parse(URI.parse(link[:href]).query) + auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) + auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query) + + expect(params).to include('private_token' => [user.private_token]) + expect(params).to include('milestone_title' => [milestone.title]) + expect(params).to include('assignee_id' => [user.id.to_s]) + expect(auto_discovery_params).to include('private_token' => [user.private_token]) + expect(auto_discovery_params).to include('milestone_title' => [milestone.title]) + expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) + end + end +end diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb new file mode 100644 index 00000000000..56b1d354eb0 --- /dev/null +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -0,0 +1,88 @@ +require 'rails_helper' + +describe 'Search bar', js: true, feature: true do + include WaitForAjax + + let!(:project) { create(:empty_project) } + let!(:user) { create(:user) } + let(:filtered_search) { find('.filtered-search') } + + before do + project.team << [user, :master] + login_as(user) + create(:issue, project: project) + + visit namespace_project_issues_path(project.namespace, project) + end + + def get_left_style(style) + left_style = /left:\s\d*[.]\d*px/.match(style) + left_style.to_s.gsub('left: ', '').to_f + end + + describe 'clear search button' do + it 'clears text' do + search_text = 'search_text' + filtered_search.set(search_text) + + expect(filtered_search.value).to eq(search_text) + find('.filtered-search-input-container .clear-search').click + + expect(filtered_search.value).to eq('') + end + + it 'hides by default' do + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides after clicked' do + filtered_search.set('a') + find('.filtered-search-input-container .clear-search').click + + expect(page).to have_css('.clear-search', visible: false) + end + + it 'hides when there is no text' do + filtered_search.set('a') + filtered_search.set('') + + expect(page).to have_css('.clear-search', visible: false) + end + + it 'shows when there is text' do + filtered_search.set('a') + + expect(page).to have_css('.clear-search', visible: true) + end + + it 'resets the dropdown hint filter' do + filtered_search.click + original_size = page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size + + filtered_search.set('author') + + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(1) + + find('.filtered-search-input-container .clear-search').click + filtered_search.click + + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(original_size) + end + + it 'resets the dropdown filters' do + filtered_search.set('a') + hint_style = page.find('#js-dropdown-hint')['style'] + hint_offset = get_left_style(hint_style) + + filtered_search.set('author:') + + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to eq(0) + + find('.filtered-search-input-container .clear-search').click + filtered_search.click + + expect(page.all('#js-dropdown-hint .filter-dropdown .filter-dropdown-item').size).to be > 0 + expect(get_left_style(page.find('#js-dropdown-hint')['style'])).to eq(hint_offset) + end + end +end diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb new file mode 100644 index 00000000000..c8c9c50396b --- /dev/null +++ b/spec/features/issues/markdown_toolbar_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +feature 'Issue markdown toolbar', feature: true, js: true do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + before do + login_as(user) + + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it "doesn't include first new line when adding bold" do + find('#note_note').native.send_keys('test') + find('#note_note').native.send_key(:enter) + find('#note_note').native.send_keys('bold') + + page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 9)') + + first('.toolbar-btn').click + + expect(find('#note_note')[:value]).to eq("test\n**bold**\n") + end + + it "doesn't include first new line when adding underline" do + find('#note_note').native.send_keys('test') + find('#note_note').native.send_key(:enter) + find('#note_note').native.send_keys('underline') + + page.evaluate_script('document.querySelectorAll(".js-main-target-form #note_note")[0].setSelectionRange(4, 50)') + + find('.toolbar-btn:nth-child(2)').click + + expect(find('#note_note')[:value]).to eq("test\n*underline*\n") + end +end diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb deleted file mode 100644 index c9a3ecf16ea..00000000000 --- a/spec/features/issues/reset_filters_spec.rb +++ /dev/null @@ -1,89 +0,0 @@ -require 'rails_helper' - -feature 'Issues filter reset button', feature: true, js: true do - include WaitForAjax - include IssueHelpers - - let!(:project) { create(:project, :public) } - let!(:user) { create(:user)} - let!(:milestone) { create(:milestone, project: project) } - let!(:bug) { create(:label, project: project, name: 'bug')} - let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')} - let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')} - - before do - project.team << [user, :developer] - end - - context 'when a milestone filter has been applied' do - it 'resets the milestone filter' do - visit_issues(project, milestone_title: milestone.title) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when a label filter has been applied' do - it 'resets the label filter' do - visit_issues(project, label_name: bug.name) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when a text search has been conducted' do - it 'resets the text search filter' do - visit_issues(project, search: 'Bug') - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when author filter has been applied' do - it 'resets the author filter' do - visit_issues(project, author_id: user.id) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when assignee filter has been applied' do - it 'resets the assignee filter' do - visit_issues(project, assignee_id: user.id) - expect(page).to have_css('.issue', count: 1) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when all filters have been applied' do - it 'resets all filters' do - visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') - expect(page).to have_css('.issue', count: 0) - - reset_filters - expect(page).to have_css('.issue', count: 2) - end - end - - context 'when no filters have been applied' do - it 'the reset link should not be visible' do - visit_issues(project) - expect(page).to have_css('.issue', count: 2) - expect(page).not_to have_css '.reset_filters' - end - end - - def reset_filters - find('.reset-filters').click - end -end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index b071fe480e6..394eb31aff8 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -619,14 +619,18 @@ describe 'Issues', feature: true do end it 'adds due date to issue' do + date = Date.today.at_beginning_of_month + 2.days + page.within '.due_date' do click_link 'Edit' page.within '.ui-datepicker-calendar' do - first('.ui-state-default').click + click_link date.day end - expect(page).to have_no_content 'None' + wait_for_ajax + + expect(find('.value').text).to have_content date.strftime('%b %-d, %Y') end end @@ -638,6 +642,8 @@ describe 'Issues', feature: true do first('.ui-state-default').click end + wait_for_ajax + expect(page).to have_no_content 'No due date' click_link 'remove due date' diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb index d5c9ed8a3b7..0952b17b63e 100644 --- a/spec/features/merge_requests/deleted_source_branch_spec.rb +++ b/spec/features/merge_requests/deleted_source_branch_spec.rb @@ -4,6 +4,8 @@ require 'spec_helper' # message to be shown by JavaScript when the source branch was deleted. # Please do not remove "js: true". describe 'Deleted source branch', feature: true, js: true do + include WaitForAjax + let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } @@ -13,7 +15,8 @@ describe 'Deleted source branch', feature: true, js: true do merge_request.update!(source_branch: 'this-branch-does-not-exist') visit namespace_project_merge_request_path( merge_request.project.namespace, - merge_request.project, merge_request + merge_request.project, + merge_request ) end @@ -23,11 +26,17 @@ describe 'Deleted source branch', feature: true, js: true do ) end - it 'hides Discussion, Commits and Changes tabs' do + it 'still contains Discussion, Commits and Changes tabs' do within '.merge-request-details' do - expect(page).to have_no_content('Discussion') - expect(page).to have_no_content('Commits') - expect(page).to have_no_content('Changes') + expect(page).to have_content('Discussion') + expect(page).to have_content('Commits') + expect(page).to have_content('Changes') end + + click_on 'Changes' + wait_for_ajax + + expect(page).to have_selector('.diffs.tab-pane .nothing-here-block') + expect(page).to have_content('Nothing to merge from this-branch-does-not-exist into feature') end end diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/merge_requests/filter_by_labels_spec.rb index 0253629f753..4c60329865c 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/merge_requests/filter_by_labels_spec.rb @@ -7,25 +7,27 @@ feature 'Issue filtering by Labels', feature: true, js: true do let!(:user) { create(:user) } let!(:label) { create(:label, project: project) } - before do - bug = create(:label, project: project, title: 'bug') - feature = create(:label, project: project, title: 'feature') - enhancement = create(:label, project: project, title: 'enhancement') + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:enhancement) { create(:label, project: project, title: 'enhancement') } + + let!(:mr1) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "bugfix1") } + let!(:mr2) { create(:merge_request, title: "Bugfix2", source_project: project, target_project: project, source_branch: "bugfix2") } + let!(:mr3) { create(:merge_request, title: "Feature1", source_project: project, target_project: project, source_branch: "feature1") } - issue1 = create(:issue, title: "Bugfix1", project: project) - issue1.labels << bug + before do + mr1.labels << bug - issue2 = create(:issue, title: "Bugfix2", project: project) - issue2.labels << bug - issue2.labels << enhancement + mr2.labels << bug + mr2.labels << enhancement - issue3 = create(:issue, title: "Feature1", project: project) - issue3.labels << feature + mr3.title = "Feature1" + mr3.labels << feature project.team << [user, :master] login_as(user) - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) end context 'filter by label bug' do diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 0d19563d628..4642b5a530d 100644 --- a/spec/features/issues/filter_issues_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -1,10 +1,10 @@ require 'rails_helper' -describe 'Filter issues', feature: true do +describe 'Filter merge requests', feature: true do include WaitForAjax + let!(:project) { create(:project) } let!(:group) { create(:group) } - let!(:project) { create(:project, group: group) } let!(:user) { create(:user)} let!(:milestone) { create(:milestone, project: project) } let!(:label) { create(:label, project: project) } @@ -14,12 +14,12 @@ describe 'Filter issues', feature: true do project.team << [user, :master] group.add_developer(user) login_as(user) - create(:issue, project: project) + create(:merge_request, source_project: project, target_project: project) end - describe 'for assignee from issues#index' do + describe 'for assignee from mr#index' do before do - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) find('.js-assignee-search').click @@ -47,9 +47,9 @@ describe 'Filter issues', feature: true do end end - describe 'for milestone from issues#index' do + describe 'for milestone from mr#index' do before do - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) find('.js-milestone-select').click @@ -77,9 +77,9 @@ describe 'Filter issues', feature: true do end end - describe 'for label from issues#index', js: true do + describe 'for label from mr#index', js: true do before do - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) find('.js-label-select').click wait_for_ajax end @@ -127,7 +127,7 @@ describe 'Filter issues', feature: true do expect(page).to have_content wontfix.title end - find('.dropdown-menu-close-icon').click + find('body').click expect(find('.filtered-labels')).to have_content(wontfix.title) @@ -135,7 +135,7 @@ describe 'Filter issues', feature: true do wait_for_ajax find('.dropdown-menu-labels a', text: label.title).click - find('.dropdown-menu-close-icon').click + find('body').click expect(find('.filtered-labels')).to have_content(wontfix.title) expect(find('.filtered-labels')).to have_content(label.title) @@ -150,21 +150,21 @@ describe 'Filter issues', feature: true do it "selects and unselects `won't fix`" do find('.dropdown-menu-labels a', text: wontfix.title).click find('.dropdown-menu-labels a', text: wontfix.title).click - - find('.dropdown-menu-close-icon').click + # Close label dropdown to load + find('body').click expect(page).not_to have_css('.filtered-labels') end end describe 'for assignee and label from issues#index' do before do - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) find('.js-assignee-search').click find('.dropdown-menu-user-link', text: user.username).click - expect(page).not_to have_selector('.issues-list .issue') + expect(page).not_to have_selector('.mr-list .merge-request') find('.js-label-select').click @@ -196,38 +196,40 @@ describe 'Filter issues', feature: true do end end - describe 'filter issues by text' do + describe 'filter merge requests by text' do before do - create(:issue, title: "Bug", project: project) + create(:merge_request, title: "Bug", source_project: project, target_project: project, source_branch: "bug") bug_label = create(:label, project: project, title: 'bug') milestone = create(:milestone, title: "8", project: project) - issue = create(:issue, - title: "Bug 2", - project: project, + mr = create(:merge_request, + title: "Bug 2", + source_project: project, + target_project: project, + source_branch: "bug2", milestone: milestone, author: user, assignee: user) - issue.labels << bug_label + mr.labels << bug_label - visit namespace_project_issues_path(project.namespace, project) + visit namespace_project_merge_requests_path(project.namespace, project) end context 'only text', js: true do - it 'filters issues by searched text' do + it 'filters merge requests by searched text' do fill_in 'issuable_search', with: 'Bug' - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end end - it 'does not show any issues' do + it 'does not show any merge requests' do fill_in 'issuable_search', with: 'testing' - page.within '.issues-list' do - expect(page).not_to have_selector('.issue') + page.within '.mr-list' do + expect(page).not_to have_selector('.merge-request') end end end @@ -237,8 +239,8 @@ describe 'Filter issues', feature: true do fill_in 'issuable_search', with: 'Bug' expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Label' @@ -248,8 +250,8 @@ describe 'Filter issues', feature: true do find('.dropdown-menu-close-icon').click expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) end end @@ -257,8 +259,8 @@ describe 'Filter issues', feature: true do fill_in 'issuable_search', with: 'Bug' expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Milestone' @@ -267,8 +269,8 @@ describe 'Filter issues', feature: true do end expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) end end @@ -276,8 +278,8 @@ describe 'Filter issues', feature: true do fill_in 'issuable_search', with: 'Bug' expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Assignee' @@ -286,8 +288,8 @@ describe 'Filter issues', feature: true do end expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) end end @@ -295,8 +297,8 @@ describe 'Filter issues', feature: true do fill_in 'issuable_search', with: 'Bug' expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Author' @@ -305,26 +307,27 @@ describe 'Filter issues', feature: true do end expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 1) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 1) end end end end - describe 'filter issues and sort', js: true do + describe 'filter merge requests and sort', js: true do before do bug_label = create(:label, project: project, title: 'bug') - bug_one = create(:issue, title: "Frontend", project: project) - bug_two = create(:issue, title: "Bug 2", project: project) - bug_one.labels << bug_label - bug_two.labels << bug_label + mr1 = create(:merge_request, title: "Frontend", source_project: project, target_project: project, source_branch: "Frontend") + mr2 = create(:merge_request, title: "Bug 2", source_project: project, target_project: project, source_branch: "bug2") - visit namespace_project_issues_path(project.namespace, project) + mr1.labels << bug_label + mr2.labels << bug_label + + visit namespace_project_merge_requests_path(project.namespace, project) end - it 'is able to filter and sort issues' do + it 'is able to filter and sort merge requests' do click_button 'Label' wait_for_ajax page.within '.labels-filter' do @@ -334,8 +337,8 @@ describe 'Filter issues', feature: true do wait_for_ajax expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: 2) + page.within '.mr-list' do + expect(page).to have_selector('.merge-request', count: 2) end click_button 'Last created' @@ -344,41 +347,9 @@ describe 'Filter issues', feature: true do end wait_for_ajax - page.within '.issues-list' do + page.within '.mr-list' do expect(page).to have_content('Frontend') end end end - - it 'updates atom feed link for project issues' do - visit namespace_project_issues_path(project.namespace, project, milestone_title: '', assignee_id: user.id) - - link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) - end - - it 'updates atom feed link for group issues' do - visit issues_group_path(group, milestone_title: '', assignee_id: user.id) - - link = find('.nav-controls a', text: 'Subscribe') - params = CGI::parse(URI.parse(link[:href]).query) - auto_discovery_link = find('link[type="application/atom+xml"]', visible: false) - auto_discovery_params = CGI::parse(URI.parse(auto_discovery_link[:href]).query) - - expect(params).to include('private_token' => [user.private_token]) - expect(params).to include('milestone_title' => ['']) - expect(params).to include('assignee_id' => [user.id.to_s]) - expect(auto_discovery_params).to include('private_token' => [user.private_token]) - expect(auto_discovery_params).to include('milestone_title' => ['']) - expect(auto_discovery_params).to include('assignee_id' => [user.id.to_s]) - end end diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb new file mode 100644 index 00000000000..3a7ece7e1d6 --- /dev/null +++ b/spec/features/merge_requests/reset_filters_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +feature 'Issues filter reset button', feature: true, js: true do + include WaitForAjax + include IssueHelpers + + let!(:project) { create(:project, :public) } + let!(:user) { create(:user)} + let!(:milestone) { create(:milestone, project: project) } + let!(:bug) { create(:label, project: project, name: 'bug')} + let!(:mr1) { create(:merge_request, title: "Feature", source_project: project, target_project: project, source_branch: "Feature", milestone: milestone, author: user, assignee: user) } + let!(:mr2) { create(:merge_request, title: "Bugfix1", source_project: project, target_project: project, source_branch: "Bugfix1") } + + let(:merge_request_css) { '.merge-request' } + + before do + mr2.labels << bug + project.team << [user, :developer] + end + + context 'when a milestone filter has been applied' do + it 'resets the milestone filter' do + visit_merge_requests(project, milestone_title: milestone.title) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when a label filter has been applied' do + it 'resets the label filter' do + visit_merge_requests(project, label_name: bug.name) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when a text search has been conducted' do + it 'resets the text search filter' do + visit_merge_requests(project, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when author filter has been applied' do + it 'resets the author filter' do + visit_merge_requests(project, author_id: user.id) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when assignee filter has been applied' do + it 'resets the assignee filter' do + visit_merge_requests(project, assignee_id: user.id) + expect(page).to have_css(merge_request_css, count: 1) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when all filters have been applied' do + it 'resets all filters' do + visit_merge_requests(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, search: 'Bug') + expect(page).to have_css(merge_request_css, count: 0) + + reset_filters + expect(page).to have_css(merge_request_css, count: 2) + end + end + + context 'when no filters have been applied' do + it 'the reset link should not be visible' do + visit_merge_requests(project) + expect(page).to have_css(merge_request_css, count: 2) + expect(page).not_to have_css '.reset_filters' + end + end + + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path project.namespace, project, opts + end + + def reset_filters + find('.reset-filters').click + end +end diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb index 1a71a03fbd9..8b302a6aa23 100644 --- a/spec/features/projects/group_links_spec.rb +++ b/spec/features/projects/group_links_spec.rb @@ -14,10 +14,10 @@ feature 'Project group links', feature: true, js: true do context 'setting an expiration date for a group link' do before do - visit namespace_project_group_links_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) select2 group.id, from: '#link_group_id' - fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') + fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d') page.find('body').click click_on 'Share' end diff --git a/spec/features/projects/members/anonymous_user_sees_members_spec.rb b/spec/features/projects/members/anonymous_user_sees_members_spec.rb index c5e3d143d91..d82cf53c690 100644 --- a/spec/features/projects/members/anonymous_user_sees_members_spec.rb +++ b/spec/features/projects/members/anonymous_user_sees_members_spec.rb @@ -11,10 +11,10 @@ feature 'Projects > Members > Anonymous user sees members', feature: true do end scenario "anonymous user visits the project's members page and sees the list of members" do - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) expect(current_path).to eq( - namespace_project_project_members_path(project.namespace, project)) + namespace_project_settings_members_path(project.namespace, project)) expect(page).to have_content(user.name) end end diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/group_links_spec.rb index 94995f7cf95..cffb935ad5a 100644 --- a/spec/features/projects/members/group_links_spec.rb +++ b/spec/features/projects/members/group_links_spec.rb @@ -12,7 +12,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t @group_link = create(:project_group_link, project: project, group: group) login_as(user) - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end it 'updates group access level' do @@ -24,7 +24,7 @@ feature 'Projects > Members > Anonymous user sees members', feature: true, js: t wait_for_ajax - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) expect(first('.group_member')).to have_content('Guest') end diff --git a/spec/features/projects/members/group_members_spec.rb b/spec/features/projects/members/group_members_spec.rb index 7d0065ee2c4..3385e5972ff 100644 --- a/spec/features/projects/members/group_members_spec.rb +++ b/spec/features/projects/members/group_members_spec.rb @@ -19,7 +19,7 @@ feature 'Projects members', feature: true do context 'with a group invitee' do before do group_invitee - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end scenario 'does not appear in the project members page' do @@ -33,7 +33,7 @@ feature 'Projects members', feature: true do before do group_invitee project_invitee - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end scenario 'shows the project invitee, the project developer, and the group owner' do @@ -54,7 +54,7 @@ feature 'Projects members', feature: true do context 'with a group requester' do before do group.request_access(group_requester) - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end scenario 'does not appear in the project members page' do @@ -68,7 +68,7 @@ feature 'Projects members', feature: true do before do group.request_access(group_requester) project.request_access(project_requester) - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) end scenario 'shows the project requester, the project developer, and the group owner' do diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index b7273021c95..f136d9ce0fa 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -14,15 +14,15 @@ feature 'Projects > Members > Master adds member with expiration date', feature: login_as(master) end - scenario 'expiration date is displayed in the members list' do + scenario 'expiration date is displayed in the members list', js: true do travel_to Time.zone.parse('2016-08-06 08:00') do - visit namespace_project_project_members_path(project.namespace, project) - + visit namespace_project_settings_members_path(project.namespace, project) page.within '.users-project-form' do select2(new_member.id, from: '#user_ids', multiple: true) fill_in 'expires_at', with: '2016-08-10' - click_on 'Add to project' end + find('.users-project-form').click + click_on 'Add to project' page.within "#project_member_#{new_member.project_members.first.id}" do expect(page).to have_content('Expires in 4 days') diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index 97c42bd7f01..0b4dcaa39c6 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -39,7 +39,7 @@ feature 'Projects > Members > User requests access', feature: true do open_project_settings_menu click_link 'Members' - visit namespace_project_project_members_path(project.namespace, project) + visit namespace_project_settings_members_path(project.namespace, project) page.within('.content') do expect(page).not_to have_content(user.name) end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index cef50f6f237..ca18ac073d8 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -1,267 +1,364 @@ require 'spec_helper' describe 'Pipelines', :feature, :js do - include GitlabRoutingHelper - include WaitForAjax + include WaitForVueResource let(:project) { create(:empty_project) } - let(:user) { create(:user) } - before do - login_as(user) - project.team << [user, :developer] - end - - describe 'GET /:project/pipelines' do - let!(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running') } - - [:all, :running, :branches].each do |scope| - context "displaying #{scope}" do - let(:project) { create(:project) } - - before { visit namespace_project_pipelines_path(project.namespace, project, scope: scope) } - - it { expect(page).to have_content(pipeline.short_sha) } - end - end - - context 'anonymous access' do - before { visit namespace_project_pipelines_path(project.namespace, project) } + context 'when user is logged in' do + let(:user) { create(:user) } - it { expect(page).to have_http_status(:success) } + before do + login_as(user) + project.team << [user, :developer] end - context 'cancelable pipeline' do - let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } - - before do - build.run - visit namespace_project_pipelines_path(project.namespace, project) + describe 'GET /:project/pipelines' do + let(:project) { create(:project) } + + let!(:pipeline) do + create( + :ci_empty_pipeline, + project: project, + ref: 'master', + status: 'running', + sha: project.commit.id, + ) end - it { expect(page).to have_link('Cancel') } - it { expect(page).to have_selector('.ci-running') } + [:all, :running, :branches].each do |scope| + context "when displaying #{scope}" do + before do + visit_project_pipelines(scope: scope) + end - context 'when canceling' do - before { click_link('Cancel') } - - it { expect(page).not_to have_link('Cancel') } - it { expect(page).to have_selector('.ci-canceled') } + it 'contains pipeline commit short SHA' do + expect(page).to have_content(pipeline.short_sha) + end + end end - end - context 'retryable pipelines' do - let!(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') } + context 'when pipeline is cancelable' do + let!(:build) do + create(:ci_build, pipeline: pipeline, + stage: 'test', + commands: 'test') + end - before do - build.drop - visit namespace_project_pipelines_path(project.namespace, project) - end + before do + build.run + visit_project_pipelines + end - it { expect(page).to have_link('Retry') } - it { expect(page).to have_selector('.ci-failed') } + it 'indicates that pipeline can be canceled' do + expect(page).to have_link('Cancel') + expect(page).to have_selector('.ci-running') + end - context 'when retrying' do - before { click_link('Retry') } + context 'when canceling' do + before { click_link('Cancel') } - it { expect(page).not_to have_link('Retry') } - it { expect(page).to have_selector('.ci-running') } + it 'indicated that pipelines was canceled' do + expect(page).not_to have_link('Cancel') + expect(page).to have_selector('.ci-canceled') + end + end end - end - context 'with manual actions' do - let!(:manual) do - create(:ci_build, :manual, pipeline: pipeline, - name: 'manual build', - stage: 'test', - commands: 'test') - end + context 'when pipeline is retryable' do + let!(:build) do + create(:ci_build, pipeline: pipeline, + stage: 'test', + commands: 'test') + end - before do - visit namespace_project_pipelines_path(project.namespace, project) - end + before do + build.drop + visit_project_pipelines + end - it 'has link to the manual action' do - find('.js-pipeline-dropdown-manual-actions').click + it 'indicates that pipeline can be retried' do + expect(page).to have_link('Retry') + expect(page).to have_selector('.ci-failed') + end - expect(page).to have_link('Manual build') - end + context 'when retrying' do + before { click_link('Retry') } - context 'when manual action was played' do - before do - find('.js-pipeline-dropdown-manual-actions').click - click_link('Manual build') + it 'shows running pipeline that is not retryable' do + expect(page).not_to have_link('Retry') + expect(page).to have_selector('.ci-running') + end end + end - it 'enqueues manual action job' do - expect(manual.reload).to be_pending + context 'when pipeline has configuration errors' do + let(:pipeline) do + create(:ci_pipeline, :invalid, project: project) end - end - end - context 'for generic statuses' do - context 'when running' do - let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } + before { visit_project_pipelines } - before do - visit namespace_project_pipelines_path(project.namespace, project) + it 'contains badge that indicates errors' do + expect(page).to have_content 'yaml invalid' end - it 'is cancelable' do - expect(page).to have_link('Cancel') + it 'contains badge with tooltip which contains error' do + expect(pipeline).to have_yaml_errors + expect(page).to have_selector( + %Q{span[data-original-title="#{pipeline.yaml_errors}"]}) end + end - it 'has pipeline running' do - expect(page).to have_selector('.ci-running') + context 'with manual actions' do + let!(:manual) do + create(:ci_build, :manual, + pipeline: pipeline, + name: 'manual build', + stage: 'test', + commands: 'test') end - context 'when canceling' do - before { click_link('Cancel') } + before { visit_project_pipelines } - it { expect(page).not_to have_link('Cancel') } - it { expect(page).to have_selector('.ci-canceled') } + it 'has a dropdown with play button' do + expect(page).to have_selector('.dropdown-toggle.btn.btn-default .icon-play') end - end - context 'when failed' do - let!(:status) { create(:generic_commit_status, :pending, pipeline: pipeline, stage: 'test') } + it 'has link to the manual action' do + find('.js-pipeline-dropdown-manual-actions').click - before do - status.drop - visit namespace_project_pipelines_path(project.namespace, project) + expect(page).to have_link('manual build') end - it 'is not retryable' do - expect(page).not_to have_link('Retry') - end + context 'when manual action was played' do + before do + find('.js-pipeline-dropdown-manual-actions').click + click_link('manual build') + end - it 'has failed pipeline' do - expect(page).to have_selector('.ci-failed') + it 'enqueues manual action job' do + expect(manual.reload).to be_pending + end end end - end - - context 'downloadable pipelines' do - context 'with artifacts' do - let!(:with_artifacts) { create(:ci_build, :artifacts, :success, pipeline: pipeline, name: 'rspec tests', stage: 'test') } - before { visit namespace_project_pipelines_path(project.namespace, project) } + context 'for generic statuses' do + context 'when running' do + let!(:running) do + create(:generic_commit_status, + status: 'running', + pipeline: pipeline, + stage: 'test') + end + + before { visit_project_pipelines } + + it 'is cancelable' do + expect(page).to have_link('Cancel') + end + + it 'has pipeline running' do + expect(page).to have_selector('.ci-running') + end + + context 'when canceling' do + before { click_link('Cancel') } + + it 'indicates that pipeline was canceled' do + expect(page).not_to have_link('Cancel') + expect(page).to have_selector('.ci-canceled') + end + end + end - it { expect(page).to have_selector('.build-artifacts') } - it do - find('.js-pipeline-dropdown-download').click - expect(page).to have_link(with_artifacts.name) + context 'when failed' do + let!(:status) do + create(:generic_commit_status, :pending, + pipeline: pipeline, + stage: 'test') + end + + before do + status.drop + visit_project_pipelines + end + + it 'is not retryable' do + expect(page).not_to have_link('Retry') + end + + it 'has failed pipeline' do + expect(page).to have_selector('.ci-failed') + end end end - context 'with artifacts expired' do - let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + context 'downloadable pipelines' do + context 'with artifacts' do + let!(:with_artifacts) do + create(:ci_build, :artifacts, :success, + pipeline: pipeline, + name: 'rspec tests', + stage: 'test') + end - before { visit namespace_project_pipelines_path(project.namespace, project) } + before { visit_project_pipelines } - it { expect(page).not_to have_selector('.build-artifacts') } - end + it 'has artifats' do + expect(page).to have_selector('.build-artifacts') + end - context 'without artifacts' do - let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + it 'has artifacts download dropdown' do + find('.js-pipeline-dropdown-download').click - before { visit namespace_project_pipelines_path(project.namespace, project) } + expect(page).to have_link(with_artifacts.name) + end + end - it { expect(page).not_to have_selector('.build-artifacts') } - end - end + context 'with artifacts expired' do + let!(:with_artifacts_expired) do + create(:ci_build, :artifacts_expired, :success, + pipeline: pipeline, + name: 'rspec', + stage: 'test') + end - context 'mini pipleine graph' do - let!(:build) do - create(:ci_build, pipeline: pipeline, stage: 'build', name: 'build') - end + before { visit_project_pipelines } - before do - visit namespace_project_pipelines_path(project.namespace, project) - end + it { expect(page).not_to have_selector('.build-artifacts') } + end + + context 'without artifacts' do + let!(:without_artifacts) do + create(:ci_build, :success, + pipeline: pipeline, + name: 'rspec', + stage: 'test') + end - it 'should render a mini pipeline graph' do - endpoint = stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: build.name) + before { visit_project_pipelines } - expect(page).to have_selector('.js-mini-pipeline-graph') - expect(page).to have_selector(".js-builds-dropdown-button[data-stage-endpoint='#{endpoint}']") + it { expect(page).not_to have_selector('.build-artifacts') } + end end - context 'when clicking a graph stage' do - it 'should open a dropdown' do - find('.js-builds-dropdown-button').trigger('click') + context 'mini pipeline graph' do + let!(:build) do + create(:ci_build, :pending, pipeline: pipeline, + stage: 'build', + name: 'build') + end - wait_for_ajax + before { visit_project_pipelines } - expect(page).to have_link build.name + it 'should render a mini pipeline graph' do + expect(page).to have_selector('.js-mini-pipeline-graph') + expect(page).to have_selector('.js-builds-dropdown-button') end - it 'should be possible to retry the failed build' do - find('.js-builds-dropdown-button').trigger('click') + context 'when clicking a stage badge' do + it 'should open a dropdown' do + find('.js-builds-dropdown-button').trigger('click') + + expect(page).to have_link build.name + end - wait_for_ajax + it 'should be possible to cancel pending build' do + find('.js-builds-dropdown-button').trigger('click') + find('a.js-ci-action-icon').trigger('click') - find('a.js-ci-action-icon').trigger('click') - expect(page).not_to have_content('Cancel running') + expect(page).to have_content('canceled') + expect(build.reload).to be_canceled + end end end end - end - describe 'POST /:project/pipelines' do - let(:project) { create(:project) } + describe 'POST /:project/pipelines' do + let(:project) { create(:project) } - before { visit new_namespace_project_pipeline_path(project.namespace, project) } + before do + visit new_namespace_project_pipeline_path(project.namespace, project) + end + + context 'for valid commit' do + before { fill_in('pipeline[ref]', with: 'master') } + + context 'with gitlab-ci.yml' do + before { stub_ci_pipeline_to_return_yaml_file } - context 'for valid commit' do - before { fill_in('pipeline[ref]', with: 'master') } + it 'creates a new pipeline' do + expect { click_on 'Create pipeline' } + .to change { Ci::Pipeline.count }.by(1) + end + end - context 'with gitlab-ci.yml' do - before { stub_ci_pipeline_to_return_yaml_file } + context 'without gitlab-ci.yml' do + before { click_on 'Create pipeline' } - it { expect{ click_on 'Create pipeline' }.to change{ Ci::Pipeline.count }.by(1) } + it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + end end - context 'without gitlab-ci.yml' do - before { click_on 'Create pipeline' } + context 'for invalid commit' do + before do + fill_in('pipeline[ref]', with: 'invalid-reference') + click_on 'Create pipeline' + end - it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + it { expect(page).to have_content('Reference not found') } end end - context 'for invalid commit' do + describe 'Create pipelines' do + let(:project) { create(:project) } + before do - fill_in('pipeline[ref]', with: 'invalid-reference') - click_on 'Create pipeline' + visit new_namespace_project_pipeline_path(project.namespace, project) + end + + describe 'new pipeline page' do + it 'has field to add a new pipeline' do + expect(page).to have_field('pipeline[ref]') + expect(page).to have_content('Create for') + end end - it { expect(page).to have_content('Reference not found') } + describe 'find pipelines' do + it 'shows filtered pipelines', js: true do + fill_in('pipeline[ref]', with: 'fix') + find('input#ref').native.send_keys(:keydown) + + within('.ui-autocomplete') do + expect(page).to have_selector('li', text: 'fix') + end + end + end end end - describe 'Create pipelines', feature: true do - let(:project) { create(:project) } - + context 'when user is not logged in' do before do - visit new_namespace_project_pipeline_path(project.namespace, project) + visit namespace_project_pipelines_path(project.namespace, project) end - describe 'new pipeline page' do - it 'has field to add a new pipeline' do - expect(page).to have_field('pipeline[ref]') - expect(page).to have_content('Create for') - end + context 'when project is public' do + let(:project) { create(:project, :public) } + + it { expect(page).to have_content 'No pipelines to show' } + it { expect(page).to have_http_status(:success) } end - describe 'find pipelines' do - it 'shows filtered pipelines', js: true do - fill_in('pipeline[ref]', with: 'fix') - find('input#ref').native.send_keys(:keydown) + context 'when project is private' do + let(:project) { create(:project, :private) } - within('.ui-autocomplete') do - expect(page).to have_selector('li', text: 'fix') - end - end + it { expect(page).to have_content 'You need to sign in' } end end + + def visit_project_pipelines(**query) + visit namespace_project_pipelines_path(project.namespace, project, query) + wait_for_vue_resource + end end diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 8de827447ff..86a07b2c679 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -33,10 +33,89 @@ feature 'Setup Mattermost slash commands', feature: true do expect(value).to eq(token) end - describe 'mattermost service is enabled' do - it 'shows the add to mattermost button' do - expect(page).to have_link 'Add to Mattermost' + it 'shows the add to mattermost button' do + expect(page).to have_link('Add to Mattermost') + end + + it 'shows an explanation if user is a member of no teams' do + stub_teams(count: 0) + + click_link 'Add to Mattermost' + + expect(page).to have_content('You aren’t a member of any team on the Mattermost instance') + expect(page).to have_link('join a team', href: "#{Gitlab.config.mattermost.host}/select_team") + end + + it 'shows an explanation if user is a member of 1 team' do + stub_teams(count: 1) + + click_link 'Add to Mattermost' + + expect(page).to have_content('The team where the slash commands will be used in') + expect(page).to have_content('This is the only available team.') + end + + it 'shows a disabled prefilled select if user is a member of 1 team' do + teams = stub_teams(count: 1) + + click_link 'Add to Mattermost' + + team_name = teams.first[1]['display_name'] + select_element = find('select#mattermost_team_id') + selected_option = select_element.find('option[selected]') + + expect(select_element['disabled']).to be(true) + expect(selected_option).to have_content(team_name.to_s) + end + + it 'has a hidden input for the prefilled value if user is a member of 1 team' do + teams = stub_teams(count: 1) + + click_link 'Add to Mattermost' + + expect(find('input#mattermost_team_id', visible: false).value).to eq(teams.first[0].to_s) + end + + it 'shows an explanation user is a member of multiple teams' do + stub_teams(count: 2) + + click_link 'Add to Mattermost' + + expect(page).to have_content('Select the team where the slash commands will be used in') + expect(page).to have_content('The list shows all available teams.') + end + + it 'shows a select with team options user is a member of multiple teams' do + stub_teams(count: 2) + + click_link 'Add to Mattermost' + + select_element = find('select#mattermost_team_id') + selected_option = select_element.find('option[selected]') + + expect(select_element['disabled']).to be(false) + expect(selected_option).to have_content('Select team...') + # The 'Select team...' placeholder is item `0`. + expect(select_element.all('option').count).to eq(3) + end + + def stub_teams(count: 0) + teams = create_teams(count) + + allow_any_instance_of(MattermostSlashCommandsService).to receive(:list_teams) { teams } + + teams + end + + def create_teams(count = 0) + teams = {} + + count.times do |i| + i += 1 + teams[i] = { id: i, display_name: i } end + + teams end describe 'mattermost service is not enabled' do diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index caecd027aaa..a05b83959fb 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -169,16 +169,16 @@ describe "Search", feature: true do find('.dropdown-menu').click_link 'Issues assigned to me' sleep 2 - expect(page).to have_selector('.issues-holder') - expect(find('.js-assignee-search .dropdown-toggle-text')).to have_content(user.name) + expect(page).to have_selector('.filtered-search') + expect(find('.filtered-search').value).to eq("assignee:@#{user.username}") end it 'takes user to her issues page when issues authored is clicked' do find('.dropdown-menu').click_link "Issues I've created" sleep 2 - expect(page).to have_selector('.issues-holder') - expect(find('.js-author-search .dropdown-toggle-text')).to have_content(user.name) + expect(page).to have_selector('.filtered-search') + expect(find('.filtered-search').value).to eq("author:@#{user.username}") end it 'takes user to her MR page when MR assigned is clicked' do diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 1897c8119d2..ecebabefff8 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -82,8 +82,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } end - describe "GET /:project_path/project_members" do - subject { namespace_project_project_members_path(project.namespace, project) } + describe "GET /:project_path/settings/members" do + subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index f52e23f9433..9bc59a7c4f9 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -82,8 +82,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_denied_for(:visitor) } end - describe "GET /:project_path/project_members" do - subject { namespace_project_project_members_path(project.namespace, project) } + describe "GET /:project_path/settings/members" do + subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index bed9e92fcb6..a8d43b3d581 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -82,8 +82,8 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for(:visitor) } end - describe "GET /:project_path/project_members" do - subject { namespace_project_project_members_path(project.namespace, project) } + describe "GET /:project_path/settings/members" do + subject { namespace_project_settings_members_path(project.namespace, project) } it { is_expected.to be_allowed_for(:admin) } it { is_expected.to be_allowed_for(:owner).of(project) } diff --git a/spec/features/snippets/create_snippet_spec.rb b/spec/features/snippets/create_snippet_spec.rb index cb95e7828db..5470276bf06 100644 --- a/spec/features/snippets/create_snippet_spec.rb +++ b/spec/features/snippets/create_snippet_spec.rb @@ -17,4 +17,18 @@ feature 'Create Snippet', feature: true do expect(page).to have_content('My Snippet Title') expect(page).to have_content('Hello World!') 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 + find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name' + find(:xpath, "//input[@id='personal_snippet_content']").set 'Hello World!' + end + + click_button 'Create snippet' + + expect(page).to have_content('My Snippet Title') + expect(page).to have_content('snippet+file+name') + expect(page).to have_content('Hello World!') + end end diff --git a/spec/fixtures/config/redis_config_with_env.yml b/spec/fixtures/config/redis_config_with_env.yml new file mode 100644 index 00000000000..f5860f37e47 --- /dev/null +++ b/spec/fixtures/config/redis_config_with_env.yml @@ -0,0 +1,2 @@ +test: + url: <%= ENV['TEST_GITLAB_REDIS_URL'] %> diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb index 837b0de9a4c..ad7f032d1e5 100644 --- a/spec/initializers/secret_token_spec.rb +++ b/spec/initializers/secret_token_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' require_relative '../../config/initializers/secret_token' describe 'create_tokens', lib: true do + include StubENV + let(:secrets) { ActiveSupport::OrderedOptions.new } before do - allow(ENV).to receive(:[]).and_call_original allow(File).to receive(:write) allow(File).to receive(:delete) allow(Rails).to receive_message_chain(:application, :secrets).and_return(secrets) @@ -17,7 +18,7 @@ describe 'create_tokens', lib: true do context 'setting secret_key_base and otp_key_base' do context 'when none of the secrets exist' do before do - allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return(nil) + stub_env('SECRET_KEY_BASE', nil) allow(File).to receive(:exist?).with('.secret').and_return(false) allow(File).to receive(:exist?).with('config/secrets.yml').and_return(false) allow(self).to receive(:warn_missing_secret) @@ -69,7 +70,7 @@ describe 'create_tokens', lib: true do context 'when secret_key_base exists in the environment and secrets.yml' do before do - allow(ENV).to receive(:[]).with('SECRET_KEY_BASE').and_return('env_key') + stub_env('SECRET_KEY_BASE', 'env_key') secrets.secret_key_base = 'secret_key_base' secrets.otp_key_base = 'otp_key_base' end diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index 7792acffac2..dcbcd014dc3 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -1,15 +1,28 @@ { - "plugins": ["jasmine"], "env": { "jasmine": true }, "extends": "plugin:jasmine/recommended", + "globals": { + "appendLoadFixtures": false, + "appendLoadStyleFixtures": false, + "appendSetFixtures": false, + "appendSetStyleFixtures": false, + "getJSONFixture": false, + "loadFixtures": false, + "loadJSONFixtures": false, + "loadStyleFixtures": false, + "preloadFixtures": false, + "preloadStyleFixtures": false, + "readFixtures": false, + "sandbox": false, + "setFixtures": false, + "setStyleFixtures": false, + "spyOnEvent": false + }, + "plugins": ["jasmine"], "rules": { "prefer-arrow-callback": 0, "func-names": 0 - }, - "globals": { - "fixture": false, - "spyOnEvent": false } } diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index 49e56249565..cf19aa05031 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -13,10 +13,10 @@ (index, element) => element.innerText.indexOf(searchText) > -1, ).first(); - fixture.preload(FIXTURE); + preloadFixtures(FIXTURE); beforeEach(function () { - fixture.load(FIXTURE); + loadFixtures(FIXTURE); this.abuseReports = new global.AbuseReports(); messages = $('.abuse-reports .message'); }); diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 192da4ee8d9..b3617a45bd4 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -7,7 +7,7 @@ (() => { window.gon || (window.gon = {}); - const fixtureTemplate = 'event_filter.html'; + const fixtureTemplate = 'static/event_filter.html.raw'; const filters = [ { id: 'all', @@ -35,7 +35,7 @@ describe('Activities', () => { beforeEach(() => { - fixture.load(fixtureTemplate); + loadFixtures(fixtureTemplate); new gl.Activities(); }); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 89201c8cb8b..faba2837d41 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -34,9 +34,9 @@ }; describe('AwardsHandler', function() { - fixture.preload('issues/open-issue.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); beforeEach(function() { - fixture.load('issues/open-issue.html.raw'); + loadFixtures('issues/open-issue.html.raw'); awardsHandler = new AwardsHandler; spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { return function(url, emoji, cb) { diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index b4573e53a4e..e77d732a32a 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -6,7 +6,7 @@ describe('Autosize behavior', function() { var load; beforeEach(function() { - return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>'); + return setFixtures('<textarea class="js-autosize" style="resize: vertical"></textarea>'); }); it('does not overwrite the resize property', function() { load(); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 0f61000bc37..1a1f34cfdc0 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -5,9 +5,9 @@ (function() { describe('Quick Submit behavior', function() { var keydownEvent; - fixture.preload('behaviors/quick_submit.html'); + preloadFixtures('static/behaviors/quick_submit.html.raw'); beforeEach(function() { - fixture.load('behaviors/quick_submit.html'); + loadFixtures('static/behaviors/quick_submit.html.raw'); $('form').submit(function(e) { // Prevent a form submit from moving us off the testing page return e.preventDefault(); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index c3f4c867d6a..1f62591c06d 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -4,9 +4,9 @@ (function() { describe('requiresInput', function() { - fixture.preload('behaviors/requires_input.html'); + preloadFixtures('static/behaviors/requires_input.html.raw'); beforeEach(function() { - return fixture.load('behaviors/requires_input.html'); + return loadFixtures('static/behaviors/requires_input.html.raw'); }); it('disables submit when any field is required', function() { $('.js-requires-input').requiresInput(); diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index 133712debab..ea953d0f5a5 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -2,10 +2,10 @@ (() => { describe('Linked Tabs', () => { - fixture.preload('linked_tabs'); + preloadFixtures('static/linked_tabs.html.raw'); beforeEach(() => { - fixture.load('linked_tabs'); + loadFixtures('static/linked_tabs.html.raw'); }); describe('when is initialized', () => { diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 3983cad4c13..0c556382980 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -17,10 +17,10 @@ describe('Build', () => { offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0, })); - fixture.preload('builds/build-with-artifacts.html.raw'); + preloadFixtures('builds/build-with-artifacts.html.raw'); beforeEach(() => { - fixture.load('builds/build-with-artifacts.html.raw'); + loadFixtures('builds/build-with-artifacts.html.raw'); spyOn($, 'ajax'); }); diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index aadf6f518a8..3f6b328348d 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -7,7 +7,7 @@ ((global) => { describe('Dashboard', () => { - const fixtureTemplate = 'dashboard.html'; + const fixtureTemplate = 'static/dashboard.html.raw'; function todosCountText() { return $('.js-todos-count').text(); @@ -17,9 +17,9 @@ $(document).trigger('todo:toggle', newCount); } - fixture.preload(fixtureTemplate); + preloadFixtures(fixtureTemplate); beforeEach(() => { - fixture.load(fixtureTemplate); + loadFixtures(fixtureTemplate); new global.Sidebar(); }); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 4bae3f30bb5..056e4d41e93 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -2,10 +2,10 @@ //= require environments/components/environment_actions describe('Actions Component', () => { - fixture.preload('environments/element.html'); + preloadFixtures('static/environments/element.html.raw'); beforeEach(() => { - fixture.load('environments/element.html'); + loadFixtures('static/environments/element.html.raw'); }); it('should render a dropdown with the provided actions', () => { diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 9f82567c35b..950a5d53fad 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -2,9 +2,9 @@ //= require environments/components/environment_external_url describe('External URL Component', () => { - fixture.preload('environments/element.html'); + preloadFixtures('static/environments/element.html.raw'); beforeEach(() => { - fixture.load('environments/element.html'); + loadFixtures('static/environments/element.html.raw'); }); it('should link to the provided externalUrl prop', () => { diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 5d7c6b2411d..c178b9cc1ec 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -3,9 +3,9 @@ //= require environments/components/environment_item describe('Environment item', () => { - fixture.preload('environments/table.html'); + preloadFixtures('static/environments/table.html.raw'); beforeEach(() => { - fixture.load('environments/table.html'); + loadFixtures('static/environments/table.html.raw'); }); describe('When item is folder', () => { diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 77ba0ab38ec..21241116e29 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,12 +1,12 @@ //= require vue //= require environments/components/environment_rollback describe('Rollback Component', () => { - fixture.preload('environments/element.html'); + preloadFixtures('static/environments/element.html.raw'); const retryURL = 'https://gitlab.com/retry'; beforeEach(() => { - fixture.load('environments/element.html'); + loadFixtures('static/environments/element.html.raw'); }); it('Should link to the provided retryUrl', () => { diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index 84a41b2bf46..bb998a32f32 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,13 +1,13 @@ //= require vue //= require environments/components/environment_stop describe('Stop Component', () => { - fixture.preload('environments/element.html'); + preloadFixtures('static/environments/element.html.raw'); let stopURL; let component; beforeEach(() => { - fixture.load('environments/element.html'); + loadFixtures('static/environments/element.html.raw'); stopURL = '/stop'; component = new window.gl.environmentsList.StopComponent({ diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 76309930f27..91846bb9143 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -6,7 +6,7 @@ describe('jQuery extensions', function() { describe('disable', function() { beforeEach(function() { - return fixture.set('<input type="text" />'); + return setFixtures('<input type="text" />'); }); it('adds the disabled attribute', function() { var $input; @@ -23,7 +23,7 @@ }); return describe('enable', function() { beforeEach(function() { - return fixture.set('<input type="text" disabled="disabled" class="disabled" />'); + return setFixtures('<input type="text" disabled="disabled" class="disabled" />'); }); it('removes the disabled attribute', function() { var $input; diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 new file mode 100644 index 00000000000..ce61b73aa8a --- /dev/null +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -0,0 +1,107 @@ +//= require extensions/array +//= require filtered_search/dropdown_utils +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown_manager + +(() => { + describe('Dropdown Utils', () => { + describe('getEscapedText', () => { + it('should return same word when it has no space', () => { + const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); + expect(escaped).toBe('textWithoutSpace'); + }); + + it('should escape with double quotes', () => { + let escaped = gl.DropdownUtils.getEscapedText('text with space'); + expect(escaped).toBe('"text with space"'); + + escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); + expect(escaped).toBe('"won\'t fix"'); + }); + + it('should escape with single quotes', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t fix'); + expect(escaped).toBe('\'won"t fix\''); + }); + + it('should escape with single quotes by default', () => { + const escaped = gl.DropdownUtils.getEscapedText('won"t\' fix'); + expect(escaped).toBe('\'won"t\' fix\''); + }); + }); + + describe('filterWithSymbol', () => { + const item = { + title: '@root', + }; + + it('should filter without symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':roo'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should filter with symbol', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':@roo'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + + it('should filter with colon', () => { + const updatedItem = gl.DropdownUtils.filterWithSymbol('@', item, ':'); + expect(updatedItem.droplab_hidden).toBe(false); + }); + }); + + describe('filterHint', () => { + it('should filter', () => { + let updatedItem = gl.DropdownUtils.filterHint({ + hint: 'label', + }, 'l'); + expect(updatedItem.droplab_hidden).toBe(false); + + updatedItem = gl.DropdownUtils.filterHint({ + hint: 'label', + }, 'o'); + expect(updatedItem.droplab_hidden).toBe(true); + }); + + it('should return droplab_hidden false when item has no hint', () => { + const updatedItem = gl.DropdownUtils.filterHint({}, ''); + expect(updatedItem.droplab_hidden).toBe(false); + }); + }); + + describe('setDataValueIfSelected', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') + .and.callFake(() => {}); + }); + + it('calls addWordToInput when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + }); + + it('returns true when dataValue exists', () => { + const selected = { + getAttribute: () => 'value', + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(result).toBe(true); + }); + + it('returns false when dataValue does not exist', () => { + const selected = { + getAttribute: () => null, + }; + + const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + expect(result).toBe(false); + }); + }); + }); +})(); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 new file mode 100644 index 00000000000..d0d27ceb4a6 --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -0,0 +1,59 @@ +//= require extensions/array +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown_manager + +(() => { + describe('Filtered Search Dropdown Manager', () => { + describe('addWordToInput', () => { + function getInputValue() { + return document.querySelector('.filtered-search').value; + } + + function setInputValue(value) { + document.querySelector('.filtered-search').value = value; + } + + beforeEach(() => { + const input = document.createElement('input'); + input.classList.add('filtered-search'); + document.body.appendChild(input); + }); + + afterEach(() => { + document.querySelector('.filtered-search').outerHTML = ''; + }); + + describe('input has no existing value', () => { + it('should add just tokenName', () => { + gl.FilteredSearchDropdownManager.addWordToInput('milestone'); + expect(getInputValue()).toBe('milestone:'); + }); + + it('should add tokenName and tokenValue', () => { + gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); + expect(getInputValue()).toBe('label:none'); + }); + }); + + describe('input has existing value', () => { + it('should be able to just add tokenName', () => { + setInputValue('a'); + gl.FilteredSearchDropdownManager.addWordToInput('author'); + expect(getInputValue()).toBe('author:'); + }); + + it('should replace tokenValue', () => { + setInputValue('author:roo'); + gl.FilteredSearchDropdownManager.addWordToInput('author', '@root'); + expect(getInputValue()).toBe('author:@root'); + }); + + it('should add tokenValues containing spaces', () => { + setInputValue('label:~"test'); + gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); + expect(getInputValue()).toBe('label:~\'"test me"\''); + }); + }); + }); + }); +})(); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 new file mode 100644 index 00000000000..6df7c0e44ef --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 @@ -0,0 +1,104 @@ +//= require extensions/array +//= require filtered_search/filtered_search_token_keys + +(() => { + describe('Filtered Search Token Keys', () => { + describe('get', () => { + let tokenKeys; + + beforeEach(() => { + tokenKeys = gl.FilteredSearchTokenKeys.get(); + }); + + it('should return tokenKeys', () => { + expect(tokenKeys !== null).toBe(true); + }); + + it('should return tokenKeys as an array', () => { + expect(tokenKeys instanceof Array).toBe(true); + }); + }); + + describe('getConditions', () => { + let conditions; + + beforeEach(() => { + conditions = gl.FilteredSearchTokenKeys.getConditions(); + }); + + it('should return conditions', () => { + expect(conditions !== null).toBe(true); + }); + + it('should return conditions as an array', () => { + expect(conditions instanceof Array).toBe(true); + }); + }); + + describe('searchByKey', () => { + it('should return null when key not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKey('notakey'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchBySymbol', () => { + it('should return null when symbol not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by symbol', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByKeyParam', () => { + it('should return null when key param not found', () => { + const tokenKey = gl.FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + expect(tokenKey === null).toBe(true); + }); + + it('should return tokenKey when found by key param', () => { + const tokenKeys = gl.FilteredSearchTokenKeys.get(); + const result = gl.FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + expect(result).toEqual(tokenKeys[0]); + }); + }); + + describe('searchByConditionUrl', () => { + it('should return null when condition url not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by url', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + expect(result).toBe(conditions[0]); + }); + }); + + describe('searchByConditionKeyValue', () => { + it('should return null when condition tokenKey and value not found', () => { + const condition = gl.FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + expect(condition === null).toBe(true); + }); + + it('should return condition when found by tokenKey and value', () => { + const conditions = gl.FilteredSearchTokenKeys.getConditions(); + const result = gl.FilteredSearchTokenKeys + .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); + expect(result).toEqual(conditions[0]); + }); + }); + }); +})(); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 new file mode 100644 index 00000000000..ac7f8e9cbcd --- /dev/null +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -0,0 +1,104 @@ +//= require extensions/array +//= require filtered_search/filtered_search_token_keys +//= require filtered_search/filtered_search_tokenizer + +(() => { + describe('Filtered Search Tokenizer', () => { + describe('processTokens', () => { + it('returns for input containing only search value', () => { + const results = gl.FilteredSearchTokenizer.processTokens('searchTerm'); + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(0); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing only tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none'); + expect(results.searchToken).toBe(''); + expect(results.tokens.length).toBe(4); + expect(results.tokens[3]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('"Very Important"'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('v1.0'); + expect(results.tokens[2].symbol).toBe('%'); + + expect(results.tokens[3].key).toBe('assignee'); + expect(results.tokens[3].value).toBe('none'); + expect(results.tokens[3].symbol).toBe(''); + }); + + it('returns for input starting with search value and ending with tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('searchTerm anotherSearchTerm milestone:none'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0]).toBe(results.lastToken); + expect(results.tokens[0].key).toBe('milestone'); + expect(results.tokens[0].value).toBe('none'); + expect(results.tokens[0].symbol).toBe(''); + }); + + it('returns for input starting with tokens and ending with search value', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('assignee:@user searchTerm'); + + expect(results.searchToken).toBe('searchTerm'); + expect(results.tokens.length).toBe(1); + expect(results.tokens[0].key).toBe('assignee'); + expect(results.tokens[0].value).toBe('user'); + expect(results.tokens[0].symbol).toBe('@'); + expect(results.lastToken).toBe(results.searchToken); + }); + + it('returns for input containing search value wrapped between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none'); + + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('label'); + expect(results.tokens[1].value).toBe('"Won\'t fix"'); + expect(results.tokens[1].symbol).toBe('~'); + + expect(results.tokens[2].key).toBe('milestone'); + expect(results.tokens[2].value).toBe('none'); + expect(results.tokens[2].symbol).toBe(''); + }); + + it('returns for input containing search value in between tokens', () => { + const results = gl.FilteredSearchTokenizer + .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing'); + expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); + expect(results.tokens.length).toBe(3); + expect(results.tokens[2]).toBe(results.lastToken); + + expect(results.tokens[0].key).toBe('author'); + expect(results.tokens[0].value).toBe('root'); + expect(results.tokens[0].symbol).toBe('@'); + + expect(results.tokens[1].key).toBe('assignee'); + expect(results.tokens[1].value).toBe('none'); + expect(results.tokens[1].symbol).toBe(''); + + expect(results.tokens[2].key).toBe('label'); + expect(results.tokens[2].value).toBe('Doing'); + expect(results.tokens[2].symbol).toBe('~'); + }); + }); + }); +})(); diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json index 4919d77e5a4..4ce7f5c601a 100644 --- a/spec/javascripts/fixtures/projects.json +++ b/spec/javascripts/fixtures/projects.json @@ -1 +1,445 @@ -[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}] +[{ + "id": 9, + "description": "", + "default_branch": null, + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:root/test.git", + "http_url_to_repo": "http://localhost:3000/root/test.git", + "web_url": "http://localhost:3000/root/test", + "owner": { + "name": "Administrator", + "username": "root", + "id": 1, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon", + "web_url": "http://localhost:3000/u/root" + }, + "name": "test", + "name_with_namespace": "Administrator / test", + "path": "test", + "path_with_namespace": "root/test", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-14T19:08:05.364Z", + "last_activity_at": "2016-01-14T19:08:07.418Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 1, + "name": "root", + "path": "root", + "owner_id": 1, + "created_at": "2016-01-13T20:19:44.439Z", + "updated_at": "2016-01-13T20:19:44.439Z", + "description": "", + "avatar": null + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 0, + "permissions": { + "project_access": null, + "group_access": null + } +}, { + "id": 8, + "description": "Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:h5bp/html5-boilerplate.git", + "http_url_to_repo": "http://localhost:3000/h5bp/html5-boilerplate.git", + "web_url": "http://localhost:3000/h5bp/html5-boilerplate", + "name": "Html5 Boilerplate", + "name_with_namespace": "H5bp / Html5 Boilerplate", + "path": "html5-boilerplate", + "path_with_namespace": "h5bp/html5-boilerplate", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:57.525Z", + "last_activity_at": "2016-01-13T20:27:57.280Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 5, + "name": "H5bp", + "path": "h5bp", + "owner_id": null, + "created_at": "2016-01-13T20:19:57.239Z", + "updated_at": "2016-01-13T20:19:57.239Z", + "description": "Tempore accusantium possimus aut libero.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 10, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 7, + "description": "Modi odio mollitia dolorem qui.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:twitter/typeahead-js.git", + "http_url_to_repo": "http://localhost:3000/twitter/typeahead-js.git", + "web_url": "http://localhost:3000/twitter/typeahead-js", + "name": "Typeahead.Js", + "name_with_namespace": "Twitter / Typeahead.Js", + "path": "typeahead-js", + "path_with_namespace": "twitter/typeahead-js", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:56.212Z", + "last_activity_at": "2016-01-13T20:27:51.496Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-01-13T20:19:54.480Z", + "updated_at": "2016-01-13T20:19:54.480Z", + "description": "Id voluptatem ipsa maiores omnis repudiandae et et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": true, + "open_issues_count": 4, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 10, + "notification_level": 3 + } + } +}, { + "id": 6, + "description": "Omnis asperiores ipsa et beatae quidem necessitatibus quia.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:twitter/flight.git", + "http_url_to_repo": "http://localhost:3000/twitter/flight.git", + "web_url": "http://localhost:3000/twitter/flight", + "name": "Flight", + "name_with_namespace": "Twitter / Flight", + "path": "flight", + "path_with_namespace": "twitter/flight", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:54.754Z", + "last_activity_at": "2016-01-13T20:27:50.502Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 4, + "name": "Twitter", + "path": "twitter", + "owner_id": null, + "created_at": "2016-01-13T20:19:54.480Z", + "updated_at": "2016-01-13T20:19:54.480Z", + "description": "Id voluptatem ipsa maiores omnis repudiandae et et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": true, + "open_issues_count": 4, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 10, + "notification_level": 3 + } + } +}, { + "id": 5, + "description": "Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-test.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-test.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-test", + "name": "Gitlab Test", + "name_with_namespace": "Gitlab Org / Gitlab Test", + "path": "gitlab-test", + "path_with_namespace": "gitlab-org/gitlab-test", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:53.202Z", + "last_activity_at": "2016-01-13T20:27:41.626Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 4, + "description": "Aut molestias quas est ut aperiam officia quod libero.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-shell.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-shell.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-shell", + "name": "Gitlab Shell", + "name_with_namespace": "Gitlab Org / Gitlab Shell", + "path": "gitlab-shell", + "path_with_namespace": "gitlab-org/gitlab-shell", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:51.882Z", + "last_activity_at": "2016-01-13T20:27:35.678Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 20, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 3, + "description": "Excepturi molestiae quia repellendus omnis est illo illum eligendi.", + "default_branch": "master", + "tag_list": [], + "public": true, + "archived": false, + "visibility_level": 20, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ci.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ci.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-ci", + "name": "Gitlab Ci", + "name_with_namespace": "Gitlab Org / Gitlab Ci", + "path": "gitlab-ci", + "path_with_namespace": "gitlab-org/gitlab-ci", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:50.346Z", + "last_activity_at": "2016-01-13T20:27:30.115Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 3, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 2, + "description": "Adipisci quaerat dignissimos enim sed ipsam dolorem quia.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 10, + "ssh_url_to_repo": "phil@localhost:gitlab-org/gitlab-ce.git", + "http_url_to_repo": "http://localhost:3000/gitlab-org/gitlab-ce.git", + "web_url": "http://localhost:3000/gitlab-org/gitlab-ce", + "name": "Gitlab Ce", + "name_with_namespace": "Gitlab Org / Gitlab Ce", + "path": "gitlab-ce", + "path_with_namespace": "gitlab-org/gitlab-ce", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:49.065Z", + "last_activity_at": "2016-01-13T20:26:58.454Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 3, + "name": "Gitlab Org", + "path": "gitlab-org", + "owner_id": null, + "created_at": "2016-01-13T20:19:48.851Z", + "updated_at": "2016-01-13T20:19:48.851Z", + "description": "Magni mollitia quod quidem soluta nesciunt impedit.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": { + "access_level": 30, + "notification_level": 3 + }, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}, { + "id": 1, + "description": "Vel voluptatem maxime saepe ex quia.", + "default_branch": "master", + "tag_list": [], + "public": false, + "archived": false, + "visibility_level": 0, + "ssh_url_to_repo": "phil@localhost:documentcloud/underscore.git", + "http_url_to_repo": "http://localhost:3000/documentcloud/underscore.git", + "web_url": "http://localhost:3000/documentcloud/underscore", + "name": "Underscore", + "name_with_namespace": "Documentcloud / Underscore", + "path": "underscore", + "path_with_namespace": "documentcloud/underscore", + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "builds_enabled": true, + "snippets_enabled": false, + "created_at": "2016-01-13T20:19:45.862Z", + "last_activity_at": "2016-01-13T20:25:03.106Z", + "shared_runners_enabled": true, + "creator_id": 1, + "namespace": { + "id": 2, + "name": "Documentcloud", + "path": "documentcloud", + "owner_id": null, + "created_at": "2016-01-13T20:19:44.464Z", + "updated_at": "2016-01-13T20:19:44.464Z", + "description": "Aut impedit perferendis fuga et ipsa repellat cupiditate et.", + "avatar": { + "url": null + } + }, + "avatar_url": null, + "star_count": 0, + "forks_count": 0, + "only_allow_merge_if_build_succeeds": false, + "open_issues_count": 5, + "permissions": { + "project_access": null, + "group_access": { + "access_level": 50, + "notification_level": 3 + } + } +}] diff --git a/spec/javascripts/fixtures/static_fixtures.rb b/spec/javascripts/fixtures/static_fixtures.rb new file mode 100644 index 00000000000..4569f16f0ca --- /dev/null +++ b/spec/javascripts/fixtures/static_fixtures.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe ApplicationController, '(Static JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + before(:all) do + clean_frontend_fixtures('static/') + end + + fixtures_path = File.expand_path(JavaScriptFixturesHelpers::FIXTURE_PATH, Rails.root) + haml_fixtures = Dir.glob(File.expand_path('**/*.haml', fixtures_path)).map do |file_path| + file_path.sub(/\A#{fixtures_path}#{File::SEPARATOR}/, '') + end + + haml_fixtures.each do |template_file_name| + it "static/#{template_file_name.sub(/\.haml\z/, '.raw')}" do |example| + fixture_file_name = example.description + rendered = render_template(template_file_name) + store_frontend_fixture(rendered, fixture_file_name) + end + end + + private + + def render_template(template_file_name) + fixture_path = JavaScriptFixturesHelpers::FIXTURE_PATH + controller = ApplicationController.new + controller.prepend_view_path(fixture_path) + controller.render_to_string(template: template_file_name, layout: false) + end +end diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb new file mode 100644 index 00000000000..c9c0b891237 --- /dev/null +++ b/spec/javascripts/fixtures/u2f.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +context 'U2F' do + include JavaScriptFixturesHelpers + + let(:user) { create(:user, :two_factor_via_u2f) } + + before(:all) do + clean_frontend_fixtures('u2f/') + end + + describe SessionsController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + @request.env['devise.mapping'] = Devise.mappings[:user] + end + + it 'u2f/authenticate.html.raw' do |example| + allow(controller).to receive(:find_user).and_return(user) + + post :create, user: { login: user.username, password: user.password } + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end + + describe Profiles::TwoFactorAuthsController, '(JavaScript fixtures)', type: :controller do + render_views + + before do + sign_in(user) + end + + it 'u2f/register.html.raw' do |example| + get :show + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + end +end diff --git a/spec/javascripts/fixtures/u2f/authenticate.html.haml b/spec/javascripts/fixtures/u2f/authenticate.html.haml deleted file mode 100644 index 779d6429a5f..00000000000 --- a/spec/javascripts/fixtures/u2f/authenticate.html.haml +++ /dev/null @@ -1 +0,0 @@ -= render partial: "u2f/authenticate", locals: { new_user_session_path: "/users/sign_in", params: {}, resource_name: "user" } diff --git a/spec/javascripts/fixtures/u2f/register.html.haml b/spec/javascripts/fixtures/u2f/register.html.haml deleted file mode 100644 index 5ed51be689c..00000000000 --- a/spec/javascripts/fixtures/u2f/register.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -- user = FactoryGirl.build(:user, :two_factor_via_otp) -= render partial: "u2f/register", locals: { create_u2f_profile_two_factor_auth_path: '/profile/two_factor_auth/create_u2f', current_user: user } diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index bfaf90e2aee..ce96571bd52 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -43,8 +43,7 @@ } describe('Dropdown', function describeDropdown() { - fixture.preload('gl_dropdown.html'); - fixture.preload('projects.json'); + preloadFixtures('static/gl_dropdown.html.raw'); function initDropDown(hasRemote, isFilterable) { this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ @@ -61,10 +60,10 @@ } beforeEach(() => { - fixture.load('gl_dropdown.html'); + loadFixtures('static/gl_dropdown.html.raw'); this.dropdownContainerElement = $('.dropdown.inline'); this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement); - this.projectsData = fixture.load('projects.json')[0]; + this.projectsData = getJSONFixture('projects.json'); }); afterEach(() => { diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 5018e87ad6c..e5d934540af 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -4,11 +4,11 @@ //= require gl_field_errors ((global) => { - fixture.preload('gl_field_errors.html'); + preloadFixtures('static/gl_field_errors.html.raw'); describe('GL Style Field Errors', function() { beforeEach(function() { - fixture.load('gl_field_errors.html'); + loadFixtures('static/gl_field_errors.html.raw'); const $form = this.$form = $('form.gl-show-field-errors'); this.fieldErrors = new global.GlFieldErrors($form); }); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index d2bcbc37b64..b5262afa1cf 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -7,7 +7,7 @@ describe('Header', function() { var todosPendingCount = '.todos-pending-count'; - var fixtureTemplate = 'header.html'; + var fixtureTemplate = 'static/header.html.raw'; function isTodosCountHidden() { return $(todosPendingCount).hasClass('hidden'); @@ -17,9 +17,9 @@ $(document).trigger('todo:toggle', newCount); } - fixture.preload(fixtureTemplate); + preloadFixtures(fixtureTemplate); beforeEach(function() { - fixture.load(fixtureTemplate); + loadFixtures(fixtureTemplate); }); it('should update todos-pending-count after receiving the todo:toggle event', function() { diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index d61601ee4fb..917a6267b92 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -21,10 +21,10 @@ } describe('Issuable', () => { - fixture.preload('issuable_filter'); + preloadFixtures('static/issuable_filter.html.raw'); beforeEach(() => { - fixture.load('issuable_filter'); + loadFixtures('static/issuable_filter.html.raw'); Issuable.init(); }); @@ -37,7 +37,7 @@ beforeEach(() => { $filtersForm = $('.js-filter-form'); - fixture.load('issuable_filter'); + loadFixtures('static/issuable_filter.html.raw'); resetForm($filtersForm); }); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index faab5ae00c2..eb07421826c 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -8,9 +8,9 @@ var INVALID_URL = 'http://goesnowhere.nothing/whereami'; var $boxClosed, $boxOpen, $btnClose, $btnReopen; - fixture.preload('issues/closed-issue.html'); - fixture.preload('issues/issue-with-task-list.html'); - fixture.preload('issues/open-issue.html'); + preloadFixtures('issues/closed-issue.html.raw'); + preloadFixtures('issues/issue-with-task-list.html.raw'); + preloadFixtures('issues/open-issue.html.raw'); function expectErrorMessage() { var $flashMessage = $('div.flash-alert'); @@ -61,8 +61,8 @@ describe('Issue', function() { describe('task lists', function() { - fixture.load('issues/issue-with-task-list.html'); beforeEach(function() { + loadFixtures('issues/issue-with-task-list.html.raw'); this.issue = new Issue(); }); @@ -86,7 +86,7 @@ describe('close issue', function() { beforeEach(function() { - fixture.load('issues/open-issue.html'); + loadFixtures('issues/open-issue.html.raw'); findElements(); this.issue = new Issue(); @@ -140,7 +140,7 @@ describe('reopen issue', function() { beforeEach(function() { - fixture.load('issues/closed-issue.html'); + loadFixtures('issues/closed-issue.html.raw'); findElements(); this.issue = new Issue(); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 0c48d04776f..e3146559a4a 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -17,10 +17,10 @@ (() => { let saveLabelCount = 0; describe('Issue dropdown sidebar', () => { - fixture.preload('issue_sidebar_label.html'); + preloadFixtures('static/issue_sidebar_label.html.raw'); beforeEach(() => { - fixture.load('issue_sidebar_label.html'); + loadFixtures('static/issue_sidebar_label.html.raw'); new IssuableContext('{"id":1,"name":"Administrator","username":"root"}'); new LabelsSelect(); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index ef75f600898..031f9ca03c9 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -15,6 +15,7 @@ expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); }); }); + describe('gl.utils.parseUrlPathname', () => { beforeEach(() => { spyOn(gl.utils, 'parseUrl').and.callFake(url => ({ @@ -28,5 +29,28 @@ expect(gl.utils.parseUrlPathname('some/relative/url')).toEqual('/some/relative/url'); }); }); + + describe('gl.utils.getUrlParamsArray', () => { + it('should return params array', () => { + expect(gl.utils.getUrlParamsArray() instanceof Array).toBe(true); + }); + + it('should remove the question mark from the search params', () => { + const paramsArray = gl.utils.getUrlParamsArray(); + expect(paramsArray[0][0] !== '?').toBe(true); + }); + }); + + describe('gl.utils.getParameterByName', () => { + it('should return valid parameter', () => { + const value = gl.utils.getParameterByName('reporter'); + expect(value).toBe('Console'); + }); + + it('should return invalid parameter', () => { + const value = gl.utils.getParameterByName('fakeParameter'); + expect(value).toBe(null); + }); + }); }); })(); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 new file mode 100644 index 00000000000..e97356b65d5 --- /dev/null +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -0,0 +1,25 @@ +//= require lib/utils/text_utility + +(() => { + describe('text_utility', () => { + describe('gl.text.getTextWidth', () => { + it('returns zero width when no text is passed', () => { + expect(gl.text.getTextWidth('')).toBe(0); + }); + + it('returns zero width when no text is passed and font is passed', () => { + expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0); + }); + + it('returns width when text is passed', () => { + expect(gl.text.getTextWidth('foo') > 0).toBe(true); + }); + + it('returns bigger width when font is larger', () => { + const largeFont = gl.text.getTextWidth('foo', '100px sans-serif'); + const regular = gl.text.getTextWidth('foo', '10px sans-serif'); + expect(largeFont > regular).toBe(true); + }); + }); + }); +})(); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index decdf583410..31f516b41bf 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -6,7 +6,7 @@ (function() { describe('LineHighlighter', function() { var clickLine; - fixture.preload('line_highlighter.html'); + preloadFixtures('static/line_highlighter.html.raw'); clickLine = function(number, eventData) { var e; if (eventData == null) { @@ -20,7 +20,7 @@ } }; beforeEach(function() { - fixture.load('line_highlighter.html'); + loadFixtures('static/line_highlighter.html.raw'); this["class"] = new LineHighlighter(); this.css = this["class"].highlightClass; return this.spies = { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 4cf1693af1b..9b232617fe5 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -6,9 +6,9 @@ (function() { describe('MergeRequest', function() { return describe('task lists', function() { - fixture.preload('merge_requests_show.html'); + preloadFixtures('static/merge_requests_show.html.raw'); beforeEach(function() { - fixture.load('merge_requests_show.html'); + loadFixtures('static/merge_requests_show.html.raw'); return this.merge = new MergeRequest(); }); it('modifies the Markdown field', function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 130d391bfab..98201fb98ed 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -16,7 +16,7 @@ }; $.extend(stubLocation, defaults, stubs || {}); }; - fixture.preload('merge_request_tabs.html'); + preloadFixtures('static/merge_request_tabs.html.raw'); beforeEach(function () { this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); @@ -30,7 +30,7 @@ describe('#activateTab', function () { beforeEach(function () { spyOn($, 'ajax').and.callFake(function () {}); - fixture.load('merge_request_tabs.html'); + loadFixtures('static/merge_request_tabs.html.raw'); this.subject = this.class.activateTab; }); it('shows the first tab when action is show', function () { diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index d1793e9308e..a1c2fe3df37 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -5,10 +5,10 @@ (() => { describe('Mini Pipeline Graph Dropdown', () => { - fixture.preload('mini_dropdown_graph'); + preloadFixtures('static/mini_dropdown_graph.html.raw'); beforeEach(() => { - fixture.load('mini_dropdown_graph'); + loadFixtures('static/mini_dropdown_graph.html.raw'); }); describe('When is initialized', () => { diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index a6cb9e47744..e0dc549a9f4 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -8,7 +8,7 @@ describe('Branch', function() { return describe('create a new branch', function() { var expectToHaveError, fillNameWith; - fixture.preload('new_branch.html'); + preloadFixtures('static/new_branch.html.raw'); fillNameWith = function(value) { return $('.js-branch-name').val(value).trigger('blur'); }; @@ -16,7 +16,7 @@ return expect($('.js-branch-name-error span').text()).toEqual(error); }; beforeEach(function() { - fixture.load('new_branch.html'); + loadFixtures('static/new_branch.html.raw'); $('form').on('submit', function(e) { return e.preventDefault(); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index bb13af7ac0c..9cdb0a5d5aa 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -12,11 +12,11 @@ gl.utils = gl.utils || {}; describe('Notes', function() { - var commentsTemplate = 'issues/issue_with_comment.raw'; - fixture.preload(commentsTemplate); + var commentsTemplate = 'issues/issue_with_comment.html.raw'; + preloadFixtures(commentsTemplate); beforeEach(function () { - fixture.load(commentsTemplate); + loadFixtures(commentsTemplate); gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; $('body').data('page', 'projects:issues:show'); diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index 85c9cf4b4f1..f0f9ad7430d 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -2,10 +2,10 @@ (() => { describe('Pipelines', () => { - fixture.preload('pipeline_graph'); + preloadFixtures('static/pipeline_graph.html.raw'); beforeEach(() => { - fixture.load('pipeline_graph'); + loadFixtures('static/pipeline_graph.html.raw'); }); it('should be defined', () => { diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 216b77f37c0..27b071f266d 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -16,10 +16,9 @@ window.gon.api_version = 'v3'; describe('Project Title', function() { - fixture.preload('project_title.html'); - fixture.preload('projects.json'); + preloadFixtures('static/project_title.html.raw'); beforeEach(function() { - fixture.load('project_title.html'); + loadFixtures('static/project_title.html.raw'); return this.project = new Project(); }); return describe('project list', function() { @@ -34,7 +33,7 @@ beforeEach((function(_this) { return function() { - _this.projects_data = fixture.load('projects.json')[0]; + _this.projects_data = getJSONFixture('projects.json'); return spyOn(jQuery, 'ajax').and.callFake(fakeAjaxResponse.bind(_this)); }; })(this)); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index a083dbf033a..0177d8e4e79 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -36,9 +36,9 @@ describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; - fixture.preload(fixtureName); + preloadFixtures(fixtureName); beforeEach(function() { - fixture.load(fixtureName); + loadFixtures(fixtureName); this.sidebar = new Sidebar; $aside = $('.right-sidebar'); $page = $('.page-with-sidebar'); @@ -65,9 +65,10 @@ }); it('should broadcast todo:toggle event when add todo clicked', function() { + var todos = getJSONFixture('todos.json'); spyOn(jQuery, 'ajax').and.callFake(function() { var d = $.Deferred(); - var response = fixture.load('todos.json'); + var response = todos; d.resolve(response); return d.promise(); }); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 1b7f642d59e..2d3f44e7980 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -11,6 +11,7 @@ (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; + var userName = 'root'; widget = null; @@ -19,6 +20,7 @@ window.gon || (window.gon = {}); window.gon.current_user_id = userId; + window.gon.current_username = userName; dashboardIssuesPath = '/dashboard/issues'; @@ -93,8 +95,8 @@ assertLinks = function(list, issuesPath, mrsPath) { var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; - issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId; - issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId; + issuesAssignedToMeLink = issuesPath + "/?assignee_username=" + userName; + issuesIHaveCreatedLink = issuesPath + "/?author_username=" + userName; mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; a1 = "a[href='" + issuesAssignedToMeLink + "']"; @@ -112,9 +114,9 @@ }; describe('Search autocomplete dropdown', function() { - fixture.preload('search_autocomplete.html'); + preloadFixtures('static/search_autocomplete.html.raw'); beforeEach(function() { - fixture.load('search_autocomplete.html'); + loadFixtures('static/search_autocomplete.html.raw'); return widget = new gl.SearchAutocomplete; }); it('should show Dashboard specific dropdown menu', function() { diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 7bc898aed5d..ae5d639ad9c 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -6,9 +6,9 @@ (function() { describe('ShortcutsIssuable', function() { var fixtureName = 'issues/open-issue.html.raw'; - fixture.preload(fixtureName); + preloadFixtures(fixtureName); beforeEach(function() { - fixture.load(fixtureName); + loadFixtures(fixtureName); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); return this.shortcut = new ShortcutsIssuable(); }); diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 index 9a9fb22255b..c274b9c45f4 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -2,7 +2,7 @@ ((global) => { describe('SigninTabsMemoizer', () => { - const fixtureTemplate = 'signin_tabs.html'; + const fixtureTemplate = 'static/signin_tabs.html.raw'; const tabSelector = 'ul.nav-tabs'; const currentTabKey = 'current_signin_tab'; let memo; @@ -15,10 +15,10 @@ return memo; } - fixture.preload(fixtureTemplate); + preloadFixtures(fixtureTemplate); beforeEach(() => { - fixture.load(fixtureTemplate); + loadFixtures(fixtureTemplate); }); it('does nothing if no tab was previously selected', () => { diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 1b7ca97cde4..39d236986b9 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -103,7 +103,7 @@ describe('DOM Events', function () { beforeEach(function () { // This ensures DOM and DOM events are initialized for these specs. - fixture.set('<div></div>'); + setFixtures('<div></div>'); this.smartInterval = createDefaultSmartInterval(); }); diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index 831dfada952..f8e3aca29fa 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -37,12 +37,12 @@ // file as a manifest. // For more information: http://github.com/modeset/teaspoon -(function() { - - -}).call(this); +// set our fixtures path +jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures'; +jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures'; // defined in ActionDispatch::TestRequest // see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7 window.gl = window.gl || {}; -gl.TEST_HOST = 'http://test.host'; +window.gl.TEST_HOST = 'http://test.host'; +window.gon = window.gon || {}; diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index ac411f6c306..5984ce8ffd4 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -13,7 +13,7 @@ }; describe('on a js-syntax-highlight element', function() { beforeEach(function() { - return fixture.set('<div class="js-syntax-highlight"></div>'); + return setFixtures('<div class="js-syntax-highlight"></div>'); }); return it('applies syntax highlighting', function() { stubUserColorScheme('monokai'); @@ -23,7 +23,7 @@ }); return describe('on a parent element', function() { beforeEach(function() { - return fixture.set("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); + return setFixtures("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); }); it('applies highlighting to all applicable children', function() { stubUserColorScheme('monokai'); @@ -33,7 +33,7 @@ }); return it('prevents an infinite loop when no matches exist', function() { var highlight; - fixture.set('<div></div>'); + setFixtures('<div></div>'); highlight = function() { return $('div').syntaxHighlight(); }; diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 064d18519ea..dc2f4967985 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -10,8 +10,10 @@ (function() { describe('U2FAuthenticate', function() { - fixture.load('u2f/authenticate'); + preloadFixtures('u2f/authenticate.html.raw'); + beforeEach(function() { + loadFixtures('u2f/authenticate.html.raw'); this.u2fDevice = new MockU2FDevice; this.container = $("#js-authenticate-u2f"); this.component = new window.gl.U2FAuthenticate( diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 189592ea87a..ab4c5edd044 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -10,8 +10,10 @@ (function() { describe('U2FRegister', function() { - fixture.load('u2f/register'); + preloadFixtures('u2f/register.html.raw'); + beforeEach(function() { + loadFixtures('u2f/register.html.raw'); this.u2fDevice = new MockU2FDevice; this.container = $("#js-register-u2f"); this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token"); diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index 26dfdb94aae..d6c6f786fb1 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -5,7 +5,7 @@ describe('Commit component', () => { let component; it('should render a code-fork icon if it does not represent a tag', () => { - fixture.set('<div class="test-commit-container"></div>'); + setFixtures('<div class="test-commit-container"></div>'); component = new window.gl.CommitComponent({ el: document.querySelector('.test-commit-container'), propsData: { @@ -30,7 +30,7 @@ describe('Commit component', () => { describe('Given all the props', () => { beforeEach(() => { - fixture.set('<div class="test-commit-container"></div>'); + setFixtures('<div class="test-commit-container"></div>'); props = { tag: true, @@ -105,7 +105,7 @@ describe('Commit component', () => { describe('When commit title is not provided', () => { it('should render default message', () => { - fixture.set('<div class="test-commit-container"></div>'); + setFixtures('<div class="test-commit-container"></div>'); props = { tag: false, commitRef: { diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 new file mode 100644 index 00000000000..1a7f2bb5fb8 --- /dev/null +++ b/spec/javascripts/vue_pagination/pagination_spec.js.es6 @@ -0,0 +1,168 @@ +//= require vue +//= require lib/utils/common_utils +//= require vue_pagination/index +/* global fixture, gl */ + +describe('Pagination component', () => { + let component; + + const changeChanges = { + one: '', + two: '', + }; + + const change = (one, two) => { + changeChanges.one = one; + changeChanges.two = two; + }; + + it('should render and start at page 1', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 2, + previousPage: '', + }, + change, + }, + }); + + expect(component.$el.classList).toContain('gl-pagination'); + + component.changePage({ target: { innerText: '1' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the previous page', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 3, + previousPage: 1, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Prev' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the next page', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Next' } }); + + expect(changeChanges.one).toEqual(5); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the last page', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Last >>' } }); + + expect(changeChanges.one).toEqual(10); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the first page', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: '<< First' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should do nothing', () => { + fixture.set('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 2, + previousPage: '', + }, + change, + }, + }); + + component.changePage({ target: { innerText: '...' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); +}); + +describe('paramHelper', () => { + it('can parse url parameters correctly', () => { + window.history.pushState({}, null, '?scope=all&p=2'); + + const scope = gl.utils.getParameterByName('scope'); + const p = gl.utils.getParameterByName('p'); + + expect(scope).toEqual('all'); + expect(p).toEqual('2'); + }); + + it('returns null if param not in url', () => { + window.history.pushState({}, null, '?p=2'); + + const scope = gl.utils.getParameterByName('scope'); + const p = gl.utils.getParameterByName('p'); + + expect(scope).toEqual(null); + expect(p).toEqual('2'); + }); +}); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 5b4d007c8f7..f1c2edcc55c 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -10,9 +10,9 @@ describe('ZenMode', function() { var fixtureName = 'issues/open-issue.html.raw'; - fixture.preload(fixtureName); + preloadFixtures(fixtureName); beforeEach(function() { - fixture.load(fixtureName); + loadFixtures(fixtureName); spyOn(Dropzone, 'forElement').and.callFake(function() { return { enable: function() { diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb new file mode 100644 index 00000000000..267318faed4 --- /dev/null +++ b/spec/lib/api/helpers/pagination_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe API::Helpers::Pagination do + let(:resource) { Project.all } + + subject do + Class.new.include(described_class).new + end + + describe '#paginate' do + let(:value) { spy('return value') } + + before do + allow(value).to receive(:to_query).and_return(value) + + allow(subject).to receive(:header).and_return(value) + allow(subject).to receive(:params).and_return(value) + allow(subject).to receive(:request).and_return(value) + end + + describe 'required instance methods' do + let(:return_spy) { spy } + + it 'requires some instance methods' do + expect_message(:header) + expect_message(:params) + expect_message(:request) + + subject.paginate(resource) + end + end + + context 'when resource can be paginated' do + before do + create_list(:empty_project, 3) + end + + describe 'first page' do + before do + allow(subject).to receive(:params) + .and_return({ page: 1, per_page: 2 }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 2 + end + + it 'adds appropriate headers' do + expect_header('X-Total', '3') + expect_header('X-Total-Pages', '2') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '1') + expect_header('X-Next-Page', '2') + expect_header('X-Prev-Page', '') + expect_header('Link', any_args) + + subject.paginate(resource) + end + end + + describe 'second page' do + before do + allow(subject).to receive(:params) + .and_return({ page: 2, per_page: 2 }) + end + + it 'returns appropriate amount of resources' do + expect(subject.paginate(resource).count).to eq 1 + end + + it 'adds appropriate headers' do + expect_header('X-Total', '3') + expect_header('X-Total-Pages', '2') + expect_header('X-Per-Page', '2') + expect_header('X-Page', '2') + expect_header('X-Next-Page', '') + expect_header('X-Prev-Page', '1') + expect_header('Link', any_args) + + subject.paginate(resource) + end + end + end + + def expect_header(name, value) + expect(subject).to receive(:header).with(name, value) + end + + def expect_message(method) + expect(subject).to receive(method) + .at_least(:once).and_return(value) + end + end +end diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb index 898f1e84ab0..0762fd7e56a 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/ci/ansi2html_spec.rb @@ -136,6 +136,14 @@ describe Ci::Ansi2html, lib: true do expect(subject.convert("<")[:html]).to eq('<') end + it "replaces newlines with line break tags" do + expect(subject.convert("\n")[:html]).to eq('<br>') + end + + it "groups carriage returns with newlines" do + expect(subject.convert("\r\n")[:html]).to eq('<br>') + end + describe "incremental update" do shared_examples 'stateable converter' do let(:pass1) { subject.convert(pre_text) } diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 62d68721574..f824e2e1efe 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -769,6 +769,19 @@ module Ci expect(builds.first[:environment]).to eq(environment[:name]) expect(builds.first[:options]).to include(environment: environment) end + + context 'the url has a port as variable' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com:$PORT' } + end + + it 'allows a variable for the port' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + end end context 'when no environment is specified' do diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index f3843ca64ff..ba199917f5c 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -8,6 +8,10 @@ module Gitlab let(:html) { 'H<sub>2</sub>O' } context "without project" do + before do + allow_any_instance_of(ApplicationSetting).to receive(:current).and_return(::ApplicationSetting.create_from_defaults) + end + it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, diff --git a/spec/lib/gitlab/backup/manager_spec.rb b/spec/lib/gitlab/backup/manager_spec.rb index 1b749d1bd39..f84782ab440 100644 --- a/spec/lib/gitlab/backup/manager_spec.rb +++ b/spec/lib/gitlab/backup/manager_spec.rb @@ -1,9 +1,27 @@ require 'spec_helper' describe Backup::Manager, lib: true do - describe '#remove_old' do - let(:progress) { StringIO.new } + include StubENV + + let(:progress) { StringIO.new } + + 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 + + @old_progress = $progress # rubocop:disable Style/GlobalVars + $progress = progress # rubocop:disable Style/GlobalVars + end + + after do + $progress = @old_progress # rubocop:disable Style/GlobalVars + end + describe '#remove_old' do let(:files) do [ '1451606400_2016_01_01_gitlab_backup.tar', @@ -20,20 +38,6 @@ describe Backup::Manager, lib: true do allow(Dir).to receive(:glob).and_return(files) allow(FileUtils).to receive(:rm) allow(Time).to receive(:now).and_return(Time.utc(2016)) - - allow(progress).to receive(:puts) - allow(progress).to receive(:print) - - allow_any_instance_of(String).to receive(:color) do |string, _color| - string - end - - @old_progress = $progress # rubocop:disable Style/GlobalVars - $progress = progress # rubocop:disable Style/GlobalVars - end - - after do - $progress = @old_progress # rubocop:disable Style/GlobalVars end context 'when keep_time is zero' do @@ -124,4 +128,82 @@ describe Backup::Manager, lib: true do end end end + + describe '#unpack' do + before do + allow(Dir).to receive(:chdir) + end + + context 'when there are no backup files in the directory' do + before do + allow(Dir).to receive(:glob).and_return([]) + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('No backups found')) + end + end + + context 'when there are two backup files in the directory and BACKUP variable is not set' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar', + '1451520000_2015_12_31_gitlab_backup.tar', + ] + ) + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(progress).to have_received(:puts) + .with(a_string_matching('Found more than one backup')) + end + end + + context 'when BACKUP variable is set to a non-existing file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(false) + + stub_env('BACKUP', 'wrong') + end + + it 'fails the operation and prints an error' do + expect { subject.unpack }.to raise_error SystemExit + expect(File).to have_received(:exist?).with('wrong_gitlab_backup.tar') + expect(progress).to have_received(:puts) + .with(a_string_matching('The backup file wrong_gitlab_backup.tar does not exist')) + end + end + + context 'when BACKUP variable is set to a correct file' do + before do + allow(Dir).to receive(:glob).and_return( + [ + '1451606400_2016_01_01_gitlab_backup.tar' + ] + ) + allow(File).to receive(:exist?).and_return(true) + allow(Kernel).to receive(:system).and_return(true) + allow(YAML).to receive(:load_file).and_return(gitlab_version: Gitlab::VERSION) + + stub_env('BACKUP', '1451606400_2016_01_01') + end + + it 'unpacks the file' do + subject.unpack + + expect(Kernel).to have_received(:system) + .with("tar", "-xf", "1451606400_2016_01_01_gitlab_backup.tar") + expect(progress).to have_received(:puts).with(a_string_matching('done')) + end + end + end end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 39069b49978..98effecdbbc 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -56,7 +56,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do it 'returns an error if the user is not allowed to do forced pushes to protected branches' do expect(Gitlab::Checks::ForcePush).to receive(:force_push?).and_return(true) - expect(user_access).to receive(:can_do_action?).with(:force_push_code_to_protected_branches).and_return(false) expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to force push code to a protected branch on this project.') @@ -88,8 +87,6 @@ describe Gitlab::Checks::ChangeAccess, lib: true do end it 'returns an error if the user is not allowed to delete protected branches' do - expect(user_access).to receive(:can_do_action?).with(:remove_protected_branches).and_return(false) - expect(subject.status).to be(false) expect(subject.message).to eq('You are not allowed to delete protected branches from this project.') end diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb index d97806295fb..2adbed2154f 100644 --- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb @@ -196,22 +196,5 @@ describe Gitlab::Ci::Config::Entry::Environment do end end end - - context 'when invalid URL is used' do - let(:config) { { name: 'test', url: 'invalid-example.gitlab.com' } } - - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end - end - - describe '#errors?' do - it 'contains error about invalid URL' do - expect(entry.errors) - .to include "environment url must be a valid url" - end - end - end end end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index ac26c831fd0..d88a141b458 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -248,6 +248,7 @@ DeployKey: - fingerprint - public - can_push +- last_used_at Service: - id - type diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index 534bcbf39fe..b9d12c3c24c 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -15,9 +15,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'should block user in GitLab' do + expect(access).to receive(:block_user).with(user, 'does not exist anymore') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -34,9 +34,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'blocks user in GitLab' do + expect(access).to receive(:block_user).with(user, 'is disabled in Active Directory') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -53,7 +53,10 @@ describe Gitlab::LDAP::Access, lib: true do end it 'does not unblock user in GitLab' do + expect(access).not_to receive(:unblock_user) + access.allowed? + expect(user).to be_blocked expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic end @@ -65,8 +68,9 @@ describe Gitlab::LDAP::Access, lib: true do end it 'unblocks user in GitLab' do + expect(access).to receive(:unblock_user).with(user, 'is not disabled anymore') + access.allowed? - expect(user).not_to be_blocked end end end @@ -87,9 +91,9 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } it 'blocks user in GitLab' do + expect(access).to receive(:block_user).with(user, 'does not exist anymore') + access.allowed? - expect(user).to be_blocked - expect(user).to be_ldap_blocked end end @@ -99,11 +103,54 @@ describe Gitlab::LDAP::Access, lib: true do end it 'unblocks the user if it exists' do + expect(access).to receive(:unblock_user).with(user, 'is available again') + access.allowed? - expect(user).not_to be_blocked end end end end end + + describe '#block_user' do + before do + user.activate + allow(Gitlab::AppLogger).to receive(:info) + + access.block_user user, 'reason' + end + + it 'blocks the user' do + expect(user).to be_blocked + expect(user).to be_ldap_blocked + end + + it 'logs the reason' do + expect(Gitlab::AppLogger).to have_received(:info).with( + "LDAP account \"123456\" reason, " \ + "blocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end + end + + describe '#unblock_user' do + before do + user.ldap_block + allow(Gitlab::AppLogger).to receive(:info) + + access.unblock_user user, 'reason' + end + + it 'activates the user' do + expect(user).not_to be_blocked + expect(user).not_to be_ldap_blocked + end + + it 'logs the reason' do + Gitlab::AppLogger.info( + "LDAP account \"123456\" reason, " \ + "unblocking Gitlab user \"#{user.name}\" (#{user.email})" + ) + end + end end diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index 1a6803e01c3..cab2e9908ff 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -129,4 +129,27 @@ describe Gitlab::LDAP::Config, lib: true do expect(config.has_auth?).to be_falsey end end + + describe '#attributes' do + it 'uses default attributes when no custom attributes are configured' do + expect(config.attributes).to eq(config.default_attributes) + end + + it 'merges the configuration attributes with default attributes' do + stub_ldap_config( + options: { + 'attributes' => { + 'username' => %w(sAMAccountName), + 'email' => %w(userPrincipalName) + } + } + ) + + expect(config.attributes).to include({ + 'username' => %w(sAMAccountName), + 'email' => %w(userPrincipalName), + 'name' => 'cn' + }) + end + end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 60afe046788..9a556cde5d5 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -7,9 +7,11 @@ describe Gitlab::LDAP::Person do before do stub_ldap_config( - attributes: { - name: 'cn', - email: %w(mail email userPrincipalName) + options: { + 'attributes' => { + 'name' => 'cn', + 'email' => %w(mail email userPrincipalName) + } } ) end @@ -30,7 +32,7 @@ describe Gitlab::LDAP::Person do entry['mail'] = mail person = Gitlab::LDAP::Person.new(entry, 'ldapmain') - expect(person.email).to eq(mail) + expect(person.email).to eq([mail]) end it 'returns the value of userPrincipalName, if mail and email are not present' do @@ -38,7 +40,7 @@ describe Gitlab::LDAP::Person do entry['userPrincipalName'] = user_principal_name person = Gitlab::LDAP::Person.new(entry, 'ldapmain') - expect(person.email).to eq(user_principal_name) + expect(person.email).to eq([user_principal_name]) end end end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index 7371b578a48..fb470ea7568 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -126,5 +126,16 @@ describe Gitlab::Metrics::RackMiddleware do expect(transaction.action).to eq('Grape#GET /projects/:id/archive') end + + it 'does not tag a transaction if route infos are missing' do + endpoint = double(:endpoint) + allow(endpoint).to receive(:route).and_raise + + env['api.endpoint'] = endpoint + + middleware.tag_endpoint(transaction, env) + + expect(transaction.action).to be_nil + end end end diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb index e5406fb2d33..917c5c46db1 100644 --- a/spec/lib/gitlab/redis_spec.rb +++ b/spec/lib/gitlab/redis_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Redis do - let(:redis_config) { Rails.root.join('config', 'resque.yml').to_s } + include StubENV before(:each) { clear_raw_config } after(:each) { clear_raw_config } @@ -72,6 +72,20 @@ describe Gitlab::Redis do expect(url2).not_to end_with('foobar') end + + context 'when yml file with env variable' do + let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_config_with_env.yml') } + + before do + stub_env('TEST_GITLAB_REDIS_URL', 'redis://redishost:6379') + end + + it 'reads redis url from env variable' do + stub_const("#{described_class}::CONFIG_FILE", redis_config) + + expect(described_class.url).to eq 'redis://redishost:6379' + end + end end describe '._raw_config' do diff --git a/spec/migrations/fill_authorized_projects_spec.rb b/spec/migrations/fill_authorized_projects_spec.rb new file mode 100644 index 00000000000..99dc4195818 --- /dev/null +++ b/spec/migrations/fill_authorized_projects_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170106142508_fill_authorized_projects.rb') + +describe FillAuthorizedProjects do + describe '#up' do + it 'schedules the jobs in batches' do + user1 = create(:user) + user2 = create(:user) + + expect(Sidekiq::Client).to receive(:push_bulk).with( + 'class' => 'AuthorizedProjectsWorker', + 'args' => [[user1.id], [user2.id]] + ) + + described_class.new.up + end + end +end diff --git a/spec/migrations/remove_dot_git_from_usernames.rb b/spec/migrations/remove_dot_git_from_usernames.rb deleted file mode 100644 index 1b1d2adc463..00000000000 --- a/spec/migrations/remove_dot_git_from_usernames.rb +++ /dev/null @@ -1,29 +0,0 @@ -# encoding: utf-8 - -require 'spec_helper' -require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_usernames.rb') - -describe RemoveDotGitFromUsernames do - let(:user) { create(:user) } - - describe '#up' do - let(:migration) { described_class.new } - - before do - namespace = user.namespace - namespace.path = 'test.git' - namespace.save!(validate: false) - - user.username = 'test.git' - user.save!(validate: false) - end - - it 'renames user with .git in username' do - migration.up - - expect(user.reload.username).to eq('test_git') - expect(user.namespace.reload.path).to eq('test_git') - expect(user.namespace.route.path).to eq('test_git') - end - end -end diff --git a/spec/migrations/remove_dot_git_from_usernames_spec.rb b/spec/migrations/remove_dot_git_from_usernames_spec.rb new file mode 100644 index 00000000000..8737e00eaeb --- /dev/null +++ b/spec/migrations/remove_dot_git_from_usernames_spec.rb @@ -0,0 +1,57 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20161226122833_remove_dot_git_from_usernames.rb') + +describe RemoveDotGitFromUsernames do + let(:user) { create(:user) } + let(:migration) { described_class.new } + + describe '#up' do + before do + update_namespace(user, 'test.git') + end + + it 'renames user with .git in username' do + migration.up + + expect(user.reload.username).to eq('test_git') + expect(user.namespace.reload.path).to eq('test_git') + expect(user.namespace.route.path).to eq('test_git') + end + end + + context 'when new path exists already' do + describe '#up' do + let(:user2) { create(:user) } + + before do + update_namespace(user, 'test.git') + update_namespace(user2, 'test_git') + + storages = { 'default' => 'tmp/tests/custom_repositories' } + + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(migration).to receive(:route_exists?).with('test_git').and_return(true) + allow(migration).to receive(:route_exists?).with('test_git1').and_return(false) + end + + it 'renames user with .git in username' do + migration.up + + expect(user.reload.username).to eq('test_git1') + expect(user.namespace.reload.path).to eq('test_git1') + expect(user.namespace.route.path).to eq('test_git1') + end + end + end + + def update_namespace(user, path) + namespace = user.namespace + namespace.path = path + namespace.save!(validate: false) + + user.username = path + user.save!(validate: false) + end +end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb deleted file mode 100644 index cd3b6d51545..00000000000 --- a/spec/models/build_spec.rb +++ /dev/null @@ -1,1338 +0,0 @@ -require 'spec_helper' - -describe Ci::Build, models: true do - let(:project) { create(:project) } - - let(:pipeline) do - create(:ci_pipeline, project: project, - sha: project.commit.id, - ref: project.default_branch, - status: 'success') - end - - let(:build) { create(:ci_build, pipeline: pipeline) } - - it { is_expected.to validate_presence_of :ref } - - it { is_expected.to respond_to :trace_html } - - describe '#first_pending' do - let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } - let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } - subject { Ci::Build.first_pending } - - it { is_expected.to be_a(Ci::Build) } - it('returns with the first pending build') { is_expected.to eq(first) } - end - - describe '#create_from' do - before do - build.status = 'success' - build.save - end - let(:create_from_build) { Ci::Build.create_from build } - - it 'exists a pending task' do - expect(Ci::Build.pending.count(:all)).to eq 0 - create_from_build - expect(Ci::Build.pending.count(:all)).to be > 0 - end - end - - describe '#failed_but_allowed?' do - subject { build.failed_but_allowed? } - - context 'when build is not allowed to fail' do - before do - build.allow_failure = false - end - - context 'and build.status is success' do - before do - build.status = 'success' - end - - it { is_expected.to be_falsey } - end - - context 'and build.status is failed' do - before do - build.status = 'failed' - end - - it { is_expected.to be_falsey } - end - end - - context 'when build is allowed to fail' do - before do - build.allow_failure = true - end - - context 'and build.status is success' do - before do - build.status = 'success' - end - - it { is_expected.to be_falsey } - end - - context 'and build.status is failed' do - before do - build.status = 'failed' - end - - it { is_expected.to be_truthy } - end - end - end - - describe '#persisted_environment' do - before do - @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") - end - - subject { build.persisted_environment } - - context 'referenced literally' do - let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } - - it { is_expected.to eq(@environment) } - end - - context 'referenced with a variable' do - let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } - - it { is_expected.to eq(@environment) } - end - end - - describe '#trace' do - it { expect(build.trace).to be_nil } - - context 'when build.trace contains text' do - let(:text) { 'example output' } - before do - build.trace = text - end - - it { expect(build.trace).to eq(text) } - end - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.update(trace: token) - build.project.update(runners_token: token) - end - - it { expect(build.trace).not_to include(token) } - it { expect(build.raw_trace).to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(trace: token) - build.update(token: token) - end - - it { expect(build.trace).not_to include(token) } - it { expect(build.raw_trace).to include(token) } - end - end - - describe '#raw_trace' do - subject { build.raw_trace } - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.project.update(runners_token: token) - build.update(trace: token) - end - - it { is_expected.not_to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(token: token) - build.update(trace: token) - end - - it { is_expected.not_to include(token) } - end - end - - context '#append_trace' do - subject { build.trace_html } - - context 'when build.trace hides runners token' do - let(:token) { 'my_secret_token' } - - before do - build.project.update(runners_token: token) - build.append_trace(token, 0) - end - - it { is_expected.not_to include(token) } - end - - context 'when build.trace hides build token' do - let(:token) { 'my_secret_token' } - - before do - build.update(token: token) - build.append_trace(token, 0) - end - - it { is_expected.not_to include(token) } - end - end - - # TODO: build timeout - # describe :timeout do - # subject { build.timeout } - # - # it { is_expected.to eq(pipeline.project.timeout) } - # end - - describe '#options' do - let(:options) do - { - image: "ruby:2.1", - services: [ - "postgres" - ] - } - end - - subject { build.options } - it { is_expected.to eq(options) } - end - - # TODO: allow_git_fetch - # describe :allow_git_fetch do - # subject { build.allow_git_fetch } - # - # it { is_expected.to eq(project.allow_git_fetch) } - # end - - describe '#project' do - subject { build.project } - - it { is_expected.to eq(pipeline.project) } - end - - describe '#project_id' do - subject { build.project_id } - - it { is_expected.to eq(pipeline.project_id) } - end - - describe '#project_name' do - subject { build.project_name } - - it { is_expected.to eq(project.name) } - end - - describe '#extract_coverage' do - context 'valid content & regex' do - subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } - - it { is_expected.to eq(98.29) } - end - - context 'valid content & bad regex' do - subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') } - - it { is_expected.to be_nil } - end - - context 'no coverage content & regex' do - subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') } - - it { is_expected.to be_nil } - end - - context 'multiple results in content & regex' do - subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') } - - it { is_expected.to eq(98.29) } - end - - context 'using a regex capture' do - subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') } - - it { is_expected.to eq(65) } - end - end - - describe '#ref_slug' do - { - 'master' => 'master', - '1-foo' => '1-foo', - 'fix/1-foo' => 'fix-1-foo', - 'fix-1-foo' => 'fix-1-foo', - 'a' * 63 => 'a' * 63, - 'a' * 64 => 'a' * 63, - 'FOO' => 'foo', - }.each do |ref, slug| - it "transforms #{ref} to #{slug}" do - build.ref = ref - - expect(build.ref_slug).to eq(slug) - end - end - end - - describe '#variables' do - let(:container_registry_enabled) { false } - let(:predefined_variables) do - [ - { key: 'CI', value: 'true', public: true }, - { key: 'GITLAB_CI', value: 'true', public: true }, - { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, - { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, - { key: 'CI_BUILD_REF', value: build.sha, public: true }, - { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, - { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, - { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true }, - { key: 'CI_BUILD_NAME', value: 'test', public: true }, - { key: 'CI_BUILD_STAGE', value: 'test', public: true }, - { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, - { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, - { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, - { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, - { key: 'CI_PROJECT_NAME', value: project.path, public: true }, - { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, - { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, - { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, - { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } - ] - end - - before do - stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com') - end - - subject { build.variables } - - context 'returns variables' do - before do - build.yaml_variables = [] - end - - it { is_expected.to eq(predefined_variables) } - end - - context 'when build has user' do - let(:user) { create(:user, username: 'starter') } - let(:user_variables) do - [ - { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, - { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } - ] - end - - before do - build.update_attributes(user: user) - end - - it { user_variables.each { |v| is_expected.to include(v) } } - end - - context 'when build has an environment' do - before do - build.update(environment: 'production') - create(:environment, project: build.project, name: 'production', slug: 'prod-slug') - end - - let(:environment_variables) do - [ - { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, - { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true } - ] - end - - it { environment_variables.each { |v| is_expected.to include(v) } } - end - - context 'when build started manually' do - before do - build.update_attributes(when: :manual) - end - - let(:manual_variable) do - { key: 'CI_BUILD_MANUAL', value: 'true', public: true } - end - - it { is_expected.to include(manual_variable) } - end - - context 'when build is for tag' do - let(:tag_variable) do - { key: 'CI_BUILD_TAG', value: 'master', public: true } - end - - before do - build.update_attributes(tag: true) - end - - it { is_expected.to include(tag_variable) } - end - - context 'when secure variable is defined' do - let(:secure_variable) do - { key: 'SECRET_KEY', value: 'secret_value', public: false } - end - - before do - build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') - end - - it { is_expected.to include(secure_variable) } - end - - context 'when build is for triggers' do - let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } - let(:user_trigger_variable) do - { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } - end - let(:predefined_trigger_variable) do - { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } - end - - before do - build.trigger_request = trigger_request - end - - it { is_expected.to include(user_trigger_variable) } - it { is_expected.to include(predefined_trigger_variable) } - end - - context 'when yaml_variables are undefined' do - before do - build.yaml_variables = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq(predefined_variables) } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq(predefined_variables) } - end - - context 'when config has variables' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - variables: { - KEY: 'value' - } - } - }) - end - let(:variables) do - [{ key: 'KEY', value: 'value', public: true }] - end - - it { is_expected.to eq(predefined_variables + variables) } - end - end - end - - context 'when container registry is enabled' do - let(:container_registry_enabled) { true } - let(:ci_registry) do - { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } - end - let(:ci_registry_image) do - { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } - end - - context 'and is disabled for project' do - before do - project.update(container_registry_enabled: false) - end - - it { is_expected.to include(ci_registry) } - it { is_expected.not_to include(ci_registry_image) } - end - - context 'and is enabled for project' do - before do - project.update(container_registry_enabled: true) - end - - it { is_expected.to include(ci_registry) } - it { is_expected.to include(ci_registry_image) } - end - end - - context 'when runner is assigned to build' do - let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) } - - before do - build.update(runner: runner) - end - - it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) } - it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) } - it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } - end - - context 'when build is for a deployment' do - let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } } - - before do - build.environment = 'production' - allow(project).to receive(:deployment_variables).and_return([deployment_variable]) - end - - it { is_expected.to include(deployment_variable) } - end - - context 'returns variables in valid order' do - before do - allow(build).to receive(:predefined_variables) { ['predefined'] } - allow(project).to receive(:predefined_variables) { ['project'] } - allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } - allow(build).to receive(:yaml_variables) { ['yaml'] } - allow(project).to receive(:secret_variables) { ['secret'] } - end - - it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } - end - end - - describe '#has_tags?' do - context 'when build has tags' do - subject { create(:ci_build, tag_list: ['tag']) } - it { is_expected.to have_tags } - end - - context 'when build does not have tags' do - subject { create(:ci_build, tag_list: []) } - it { is_expected.not_to have_tags } - end - end - - describe '#any_runners_online?' do - subject { build.any_runners_online? } - - context 'when no runners' do - it { is_expected.to be_falsey } - end - - context 'when there are runners' do - let(:runner) { create(:ci_runner) } - - before do - build.project.runners << runner - runner.update_attributes(contacted_at: 1.second.ago) - end - - it { is_expected.to be_truthy } - - it 'that is inactive' do - runner.update_attributes(active: false) - is_expected.to be_falsey - end - - it 'that is not online' do - runner.update_attributes(contacted_at: nil) - is_expected.to be_falsey - end - - it 'that cannot handle build' do - expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) - is_expected.to be_falsey - end - end - end - - describe '#stuck?' do - subject { build.stuck? } - - context "when commit_status.status is pending" do - before do - build.status = 'pending' - end - - it { is_expected.to be_truthy } - - context "and there are specific runner" do - let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) } - - before do - build.project.runners << runner - runner.save - end - - it { is_expected.to be_falsey } - end - end - - %w[success failed canceled running].each do |state| - context "when commit_status.status is #{state}" do - before do - build.status = state - end - - it { is_expected.to be_falsey } - end - end - end - - describe '#artifacts?' do - subject { build.artifacts? } - - context 'artifacts archive does not exist' do - before do - build.update_attributes(artifacts_file: nil) - end - - it { is_expected.to be_falsy } - end - - context 'artifacts archive exists' do - let(:build) { create(:ci_build, :artifacts) } - it { is_expected.to be_truthy } - - context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } - it { is_expected.to be_falsy } - end - - context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } - it { is_expected.to be_truthy } - end - end - end - - describe '#artifacts_expired?' do - subject { build.artifacts_expired? } - - context 'is expired' do - before { build.update(artifacts_expire_at: Time.now - 7.days) } - - it { is_expected.to be_truthy } - end - - context 'is not expired' do - before { build.update(artifacts_expire_at: Time.now + 7.days) } - - it { is_expected.to be_falsey } - end - end - - describe '#artifacts_metadata?' do - subject { build.artifacts_metadata? } - context 'artifacts metadata does not exist' do - it { is_expected.to be_falsy } - end - - context 'artifacts archive is a zip file and metadata exists' do - let(:build) { create(:ci_build, :artifacts) } - it { is_expected.to be_truthy } - end - end - describe '#repo_url' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - - subject { build.repo_url } - - it { is_expected.to be_a(String) } - it { is_expected.to end_with(".git") } - it { is_expected.to start_with(project.web_url[0..6]) } - it { is_expected.to include(build.token) } - it { is_expected.to include('gitlab-ci-token') } - it { is_expected.to include(project.web_url[7..-1]) } - end - - describe '#artifacts_expire_in' do - subject { build.artifacts_expire_in } - it { is_expected.to be_nil } - - context 'when artifacts_expire_at is specified' do - let(:expire_at) { Time.now + 7.days } - - before { build.artifacts_expire_at = expire_at } - - it { is_expected.to be_within(5).of(expire_at - Time.now) } - end - end - - describe '#artifacts_expire_in=' do - subject { build.artifacts_expire_in } - - it 'when assigning valid duration' do - build.artifacts_expire_in = '7 days' - - is_expected.to be_within(10).of(7.days.to_i) - end - - it 'when assigning invalid duration' do - expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError) - is_expected.to be_nil - end - - it 'when resseting value' do - build.artifacts_expire_in = nil - - is_expected.to be_nil - end - end - - describe '#keep_artifacts!' do - let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) } - - it 'to reset expire_at' do - build.keep_artifacts! - - expect(build.artifacts_expire_at).to be_nil - end - end - - describe '#depends_on_builds' do - let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } - let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } - let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } - let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } - - it 'expects to have no dependents if this is first build' do - expect(build.depends_on_builds).to be_empty - end - - it 'expects to have one dependent if this is test' do - expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) - end - - it 'expects to have all builds from build and test stage if this is last' do - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) - end - - it 'expects to have retried builds instead the original ones' do - retried_rspec = Ci::Build.retry(rspec_test) - expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) - end - end - - def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) - create(factory, source_project_id: pipeline.gl_project_id, - target_project_id: pipeline.gl_project_id, - source_branch: build.ref, - created_at: created_at) - end - - describe '#merge_request' do - context 'when a MR has a reference to the pipeline' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request) - - commits = [double(id: pipeline.sha)] - allow(@merge_request).to receive(:commits).and_return(commits) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) - end - - it 'returns the single associated MR' do - expect(build.merge_request.id).to eq(@merge_request.id) - end - end - - context 'when there is not a MR referencing the pipeline' do - it 'returns nil' do - expect(build.merge_request).to be_nil - end - end - - context 'when more than one MR have a reference to the pipeline' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request) - @merge_request.close! - @merge_request2 = create_mr(build, pipeline, factory: :merge_request) - - commits = [double(id: pipeline.sha)] - allow(@merge_request).to receive(:commits).and_return(commits) - allow(@merge_request2).to receive(:commits).and_return(commits) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) - end - - it 'returns the first MR' do - expect(build.merge_request.id).to eq(@merge_request.id) - end - end - - context 'when a Build is created after the MR' do - before do - @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs) - pipeline2 = create(:ci_pipeline, project: project) - @build2 = create(:ci_build, pipeline: pipeline2) - - allow(@merge_request).to receive(:commits_sha). - and_return([pipeline.sha, pipeline2.sha]) - allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) - end - - it 'returns the current MR' do - expect(@build2.merge_request.id).to eq(@merge_request.id) - end - end - end - - describe 'build erasable' do - shared_examples 'erasable' do - it 'removes artifact file' do - expect(build.artifacts_file.exists?).to be_falsy - end - - it 'removes artifact metadata file' do - expect(build.artifacts_metadata.exists?).to be_falsy - end - - it 'erases build trace in trace file' do - expect(build.trace).to be_empty - end - - it 'sets erased to true' do - expect(build.erased?).to be true - end - - it 'sets erase date' do - expect(build.erased_at).not_to be_falsy - end - end - - context 'build is not erasable' do - let!(:build) { create(:ci_build) } - - describe '#erase' do - subject { build.erase } - - it { is_expected.to be false } - end - - describe '#erasable?' do - subject { build.erasable? } - it { is_expected.to eq false } - end - end - - context 'build is erasable' do - let!(:build) { create(:ci_build, :trace, :success, :artifacts) } - - describe '#erase' do - before do - build.erase(erased_by: user) - end - - context 'erased by user' do - let!(:user) { create(:user, username: 'eraser') } - - include_examples 'erasable' - - it 'records user who erased a build' do - expect(build.erased_by).to eq user - end - end - - context 'erased by system' do - let(:user) { nil } - - include_examples 'erasable' - - it 'does not set user who erased a build' do - expect(build.erased_by).to be_nil - end - end - end - - describe '#erasable?' do - subject { build.erasable? } - it { is_expected.to be_truthy } - end - - describe '#erased?' do - let!(:build) { create(:ci_build, :trace, :success, :artifacts) } - subject { build.erased? } - - context 'build has not been erased' do - it { is_expected.to be_falsey } - end - - context 'build has been erased' do - before do - build.erase - end - - it { is_expected.to be_truthy } - end - end - - context 'metadata and build trace are not available' do - let!(:build) { create(:ci_build, :success, :artifacts) } - - before do - build.remove_artifacts_metadata! - end - - describe '#erase' do - it 'does not raise error' do - expect { build.erase }.not_to raise_error - end - end - end - end - end - - describe '#commit' do - it 'returns commit pipeline has been created for' do - expect(build.commit).to eq project.commit - end - end - - describe '#when' do - subject { build.when } - - context 'when `when` is undefined' do - before do - build.when = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq('on_success') } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq('on_success') } - end - - context 'when config has `when`' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - when: 'always' - } - }) - end - - it { is_expected.to eq('always') } - end - end - end - end - - describe '#cancelable?' do - subject { build } - - context 'when build is cancelable' do - context 'when build is pending' do - it { is_expected.to be_cancelable } - end - - context 'when build is running' do - before do - build.run! - end - - it { is_expected.to be_cancelable } - end - end - - context 'when build is not cancelable' do - context 'when build is successful' do - before do - build.success! - end - - it { is_expected.not_to be_cancelable } - end - - context 'when build is failed' do - before do - build.drop! - end - - it { is_expected.not_to be_cancelable } - end - end - end - - describe '#retryable?' do - subject { build } - - context 'when build is retryable' do - context 'when build is successful' do - before do - build.success! - end - - it { is_expected.to be_retryable } - end - - context 'when build is failed' do - before do - build.drop! - end - - it { is_expected.to be_retryable } - end - - context 'when build is canceled' do - before do - build.cancel! - end - - it { is_expected.to be_retryable } - end - end - - context 'when build is not retryable' do - context 'when build is running' do - before do - build.run! - end - - it { is_expected.not_to be_retryable } - end - - context 'when build is skipped' do - before do - build.skip! - end - - it { is_expected.not_to be_retryable } - end - end - end - - describe '#manual?' do - before do - build.update(when: value) - end - - subject { build.manual? } - - context 'when is set to manual' do - let(:value) { 'manual' } - - it { is_expected.to be_truthy } - end - - context 'when set to something else' do - let(:value) { 'something else' } - - it { is_expected.to be_falsey } - end - end - - describe '#other_actions' do - let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } - - subject { build.other_actions } - - it 'returns other actions' do - is_expected.to contain_exactly(other_build) - end - - context 'when build is retried' do - let!(:new_build) { Ci::Build.retry(build) } - - it 'does not return any of them' do - is_expected.not_to include(build, new_build) - end - end - - context 'when other build is retried' do - let!(:retried_build) { Ci::Build.retry(other_build) } - - it 'returns a retried build' do - is_expected.to contain_exactly(retried_build) - end - end - end - - describe '#play' do - let(:build) { create(:ci_build, :manual, pipeline: pipeline) } - - subject { build.play } - - it 'enqueues a build' do - is_expected.to be_pending - is_expected.to eq(build) - end - - context 'for successful build' do - before do - build.update(status: 'success') - end - - it 'creates a new build' do - is_expected.to be_pending - is_expected.not_to eq(build) - end - end - end - - describe '#when' do - subject { build.when } - - context 'when `when` is undefined' do - before do - build.when = nil - end - - context 'use from gitlab-ci.yml' do - before do - stub_ci_pipeline_yaml_file(config) - end - - context 'when config is not found' do - let(:config) { nil } - - it { is_expected.to eq('on_success') } - end - - context 'when config does not have a questioned job' do - let(:config) do - YAML.dump({ - test_other: { - script: 'Hello World' - } - }) - end - - it { is_expected.to eq('on_success') } - end - - context 'when config has when' do - let(:config) do - YAML.dump({ - test: { - script: 'Hello World', - when: 'always' - } - }) - end - - it { is_expected.to eq('always') } - end - end - end - end - - describe '#retryable?' do - context 'when build is running' do - before { build.run! } - - it 'returns false' do - expect(build).not_to be_retryable - end - end - - context 'when build is finished' do - before do - build.success! - end - - it 'returns true' do - expect(build).to be_retryable - end - end - end - - describe '#has_environment?' do - subject { build.has_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - it { is_expected.to be_truthy } - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#starts_environment?' do - subject { build.starts_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - context 'no action is defined' do - it { is_expected.to be_truthy } - end - - context 'and start action is defined' do - before do - build.update(options: { environment: { action: 'start' } } ) - end - - it { is_expected.to be_truthy } - end - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#stops_environment?' do - subject { build.stops_environment? } - - context 'when environment is defined' do - before do - build.update(environment: 'review') - end - - context 'no action is defined' do - it { is_expected.to be_falsey } - end - - context 'and stop action is defined' do - before do - build.update(options: { environment: { action: 'stop' } } ) - end - - it { is_expected.to be_truthy } - end - end - - context 'when environment is not defined' do - before do - build.update(environment: nil) - end - - it { is_expected.to be_falsey } - end - end - - describe '#last_deployment' do - subject { build.last_deployment } - - context 'when multiple deployments are created' do - let!(:deployment1) { create(:deployment, deployable: build) } - let!(:deployment2) { create(:deployment, deployable: build) } - - it 'returns the latest one' do - is_expected.to eq(deployment2) - end - end - end - - describe '#outdated_deployment?' do - subject { build.outdated_deployment? } - - context 'when build succeeded' do - let(:build) { create(:ci_build, :success) } - let!(:deployment) { create(:deployment, deployable: build) } - - context 'current deployment is latest' do - it { is_expected.to be_falsey } - end - - context 'current deployment is not latest on environment' do - let!(:deployment2) { create(:deployment, environment: deployment.environment) } - - it { is_expected.to be_truthy } - end - end - - context 'when build failed' do - let(:build) { create(:ci_build, :failed) } - - it { is_expected.to be_falsey } - end - end - - describe '#expanded_environment_name' do - subject { build.expanded_environment_name } - - context 'when environment uses $CI_BUILD_REF_NAME' do - let(:build) do - create(:ci_build, - ref: 'master', - environment: 'review/$CI_BUILD_REF_NAME') - end - - it { is_expected.to eq('review/master') } - end - - context 'when environment uses yaml_variables containing symbol keys' do - let(:build) do - create(:ci_build, - yaml_variables: [{ key: :APP_HOST, value: 'host' }], - environment: 'review/$APP_HOST') - end - - it { is_expected.to eq('review/host') } - end - end - - describe '#detailed_status' do - let(:user) { create(:user) } - - it 'returns a detailed status' do - expect(build.detailed_status(user)) - .to be_a Gitlab::Ci::Status::Build::Cancelable - end - end -end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7e1d1126b97..af0f6a31eda 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,14 +1,962 @@ require 'spec_helper' -describe Ci::Build, models: true do - let(:build) { create(:ci_build) } +describe Ci::Build, :models do + let(:project) { create(:project) } + let(:build) { create(:ci_build, pipeline: pipeline) } let(:test_trace) { 'This is a test' } + let(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.id, + ref: project.default_branch, + status: 'success') + end + it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } - it { is_expected.to have_many(:deployments) } + it { is_expected.to validate_presence_of :ref } + it { is_expected.to respond_to :trace_html } + + describe '#any_runners_online?' do + subject { build.any_runners_online? } + + context 'when no runners' do + it { is_expected.to be_falsey } + end + + context 'when there are runners' do + let(:runner) { create(:ci_runner) } + + before do + build.project.runners << runner + runner.update_attributes(contacted_at: 1.second.ago) + end + + it { is_expected.to be_truthy } + + it 'that is inactive' do + runner.update_attributes(active: false) + is_expected.to be_falsey + end + + it 'that is not online' do + runner.update_attributes(contacted_at: nil) + is_expected.to be_falsey + end + + it 'that cannot handle build' do + expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) + is_expected.to be_falsey + end + end + end + + describe '#append_trace' do + subject { build.trace_html } + + context 'when build.trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update(runners_token: token) + build.append_trace(token, 0) + end + + it { is_expected.not_to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(token: token) + build.append_trace(token, 0) + end + + it { is_expected.not_to include(token) } + end + end + + describe '#artifacts?' do + subject { build.artifacts? } + + context 'artifacts archive does not exist' do + before do + build.update_attributes(artifacts_file: nil) + end + + it { is_expected.to be_falsy } + end + + context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } + + context 'is expired' do + before { build.update(artifacts_expire_at: Time.now - 7.days) } + it { is_expected.to be_falsy } + end + + context 'is not expired' do + before { build.update(artifacts_expire_at: Time.now + 7.days) } + it { is_expected.to be_truthy } + end + end + end + + describe '#artifacts_expired?' do + subject { build.artifacts_expired? } + + context 'is expired' do + before { build.update(artifacts_expire_at: Time.now - 7.days) } + + it { is_expected.to be_truthy } + end + + context 'is not expired' do + before { build.update(artifacts_expire_at: Time.now + 7.days) } + + it { is_expected.to be_falsey } + end + end + + describe '#artifacts_metadata?' do + subject { build.artifacts_metadata? } + context 'artifacts metadata does not exist' do + it { is_expected.to be_falsy } + end + + context 'artifacts archive is a zip file and metadata exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } + end + end + + describe '#artifacts_expire_in' do + subject { build.artifacts_expire_in } + it { is_expected.to be_nil } + + context 'when artifacts_expire_at is specified' do + let(:expire_at) { Time.now + 7.days } + + before { build.artifacts_expire_at = expire_at } + + it { is_expected.to be_within(5).of(expire_at - Time.now) } + end + end + + describe '#artifacts_expire_in=' do + subject { build.artifacts_expire_in } + + it 'when assigning valid duration' do + build.artifacts_expire_in = '7 days' + + is_expected.to be_within(10).of(7.days.to_i) + end + + it 'when assigning invalid duration' do + expect { build.artifacts_expire_in = '7 elephants' }.to raise_error(ChronicDuration::DurationParseError) + is_expected.to be_nil + end + + it 'when resseting value' do + build.artifacts_expire_in = nil + + is_expected.to be_nil + end + end + + describe '#commit' do + it 'returns commit pipeline has been created for' do + expect(build.commit).to eq project.commit + end + end + + describe '#create_from' do + before do + build.status = 'success' + build.save + end + let(:create_from_build) { Ci::Build.create_from build } + + it 'exists a pending task' do + expect(Ci::Build.pending.count(:all)).to eq 0 + create_from_build + expect(Ci::Build.pending.count(:all)).to be > 0 + end + end + + describe '#depends_on_builds' do + let!(:build) { create(:ci_build, pipeline: pipeline, name: 'build', stage_idx: 0, stage: 'build') } + let!(:rspec_test) { create(:ci_build, pipeline: pipeline, name: 'rspec', stage_idx: 1, stage: 'test') } + let!(:rubocop_test) { create(:ci_build, pipeline: pipeline, name: 'rubocop', stage_idx: 1, stage: 'test') } + let!(:staging) { create(:ci_build, pipeline: pipeline, name: 'staging', stage_idx: 2, stage: 'deploy') } + + it 'expects to have no dependents if this is first build' do + expect(build.depends_on_builds).to be_empty + end + + it 'expects to have one dependent if this is test' do + expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) + end + + it 'expects to have all builds from build and test stage if this is last' do + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) + end + + it 'expects to have retried builds instead the original ones' do + retried_rspec = Ci::Build.retry(rspec_test) + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + end + end + + describe '#detailed_status' do + let(:user) { create(:user) } + + it 'returns a detailed status' do + expect(build.detailed_status(user)) + .to be_a Gitlab::Ci::Status::Build::Cancelable + end + end + + describe 'deployment' do + describe '#last_deployment' do + subject { build.last_deployment } + + context 'when multiple deployments are created' do + let!(:deployment1) { create(:deployment, deployable: build) } + let!(:deployment2) { create(:deployment, deployable: build) } + + it 'returns the latest one' do + is_expected.to eq(deployment2) + end + end + end + + describe '#outdated_deployment?' do + subject { build.outdated_deployment? } + + context 'when build succeeded' do + let(:build) { create(:ci_build, :success) } + let!(:deployment) { create(:deployment, deployable: build) } + + context 'current deployment is latest' do + it { is_expected.to be_falsey } + end + + context 'current deployment is not latest on environment' do + let!(:deployment2) { create(:deployment, environment: deployment.environment) } + + it { is_expected.to be_truthy } + end + end + + context 'when build failed' do + let(:build) { create(:ci_build, :failed) } + + it { is_expected.to be_falsey } + end + end + end + + describe 'environment' do + describe '#has_environment?' do + subject { build.has_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + it { is_expected.to be_truthy } + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#expanded_environment_name' do + subject { build.expanded_environment_name } + + context 'when environment uses $CI_BUILD_REF_NAME' do + let(:build) do + create(:ci_build, + ref: 'master', + environment: 'review/$CI_BUILD_REF_NAME') + end + + it { is_expected.to eq('review/master') } + end + + context 'when environment uses yaml_variables containing symbol keys' do + let(:build) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + environment: 'review/$APP_HOST') + end + + it { is_expected.to eq('review/host') } + end + end + + describe '#starts_environment?' do + subject { build.starts_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_truthy } + end + + context 'and start action is defined' do + before do + build.update(options: { environment: { action: 'start' } } ) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#stops_environment?' do + subject { build.stops_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_falsey } + end + + context 'and stop action is defined' do + before do + build.update(options: { environment: { action: 'stop' } } ) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + end + + describe 'erasable build' do + shared_examples 'erasable' do + it 'removes artifact file' do + expect(build.artifacts_file.exists?).to be_falsy + end + + it 'removes artifact metadata file' do + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'erases build trace in trace file' do + expect(build.trace).to be_empty + end + + it 'sets erased to true' do + expect(build.erased?).to be true + end + + it 'sets erase date' do + expect(build.erased_at).not_to be_falsy + end + end + + context 'build is not erasable' do + let!(:build) { create(:ci_build) } + + describe '#erase' do + subject { build.erase } + + it { is_expected.to be false } + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to eq false } + end + end + + context 'build is erasable' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + + describe '#erase' do + before do + build.erase(erased_by: user) + end + + context 'erased by user' do + let!(:user) { create(:user, username: 'eraser') } + + include_examples 'erasable' + + it 'records user who erased a build' do + expect(build.erased_by).to eq user + end + end + + context 'erased by system' do + let(:user) { nil } + + include_examples 'erasable' + + it 'does not set user who erased a build' do + expect(build.erased_by).to be_nil + end + end + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to be_truthy } + end + + describe '#erased?' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + subject { build.erased? } + + context 'build has not been erased' do + it { is_expected.to be_falsey } + end + + context 'build has been erased' do + before do + build.erase + end + + it { is_expected.to be_truthy } + end + end + + context 'metadata and build trace are not available' do + let!(:build) { create(:ci_build, :success, :artifacts) } + + before do + build.remove_artifacts_metadata! + end + + describe '#erase' do + it 'does not raise error' do + expect { build.erase }.not_to raise_error + end + end + end + end + end + + describe '#extract_coverage' do + context 'valid content & regex' do + subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } + + it { is_expected.to eq(98.29) } + end + + context 'valid content & bad regex' do + subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') } + + it { is_expected.to be_nil } + end + + context 'no coverage content & regex' do + subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') } + + it { is_expected.to be_nil } + end + + context 'multiple results in content & regex' do + subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') } + + it { is_expected.to eq(98.29) } + end + + context 'using a regex capture' do + subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') } + + it { is_expected.to eq(65) } + end + end + + describe '#first_pending' do + let!(:first) { create(:ci_build, pipeline: pipeline, status: 'pending', created_at: Date.yesterday) } + let!(:second) { create(:ci_build, pipeline: pipeline, status: 'pending') } + subject { Ci::Build.first_pending } + + it { is_expected.to be_a(Ci::Build) } + it('returns with the first pending build') { is_expected.to eq(first) } + end + + describe '#failed_but_allowed?' do + subject { build.failed_but_allowed? } + + context 'when build is not allowed to fail' do + before do + build.allow_failure = false + end + + context 'and build.status is success' do + before do + build.status = 'success' + end + + it { is_expected.to be_falsey } + end + + context 'and build.status is failed' do + before do + build.status = 'failed' + end + + it { is_expected.to be_falsey } + end + end + + context 'when build is allowed to fail' do + before do + build.allow_failure = true + end + + context 'and build.status is success' do + before do + build.status = 'success' + end + + it { is_expected.to be_falsey } + end + + context 'and build.status is failed' do + before do + build.status = 'failed' + end + + it { is_expected.to be_truthy } + end + end + end + + describe 'flags' do + describe '#cancelable?' do + subject { build } + + context 'when build is cancelable' do + context 'when build is pending' do + it { is_expected.to be_cancelable } + end + + context 'when build is running' do + before do + build.run! + end + + it { is_expected.to be_cancelable } + end + end + + context 'when build is not cancelable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.not_to be_cancelable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.not_to be_cancelable } + end + end + end + + describe '#retryable?' do + subject { build } + + context 'when build is retryable' do + context 'when build is successful' do + before do + build.success! + end + + it { is_expected.to be_retryable } + end + + context 'when build is failed' do + before do + build.drop! + end + + it { is_expected.to be_retryable } + end + + context 'when build is canceled' do + before do + build.cancel! + end + + it { is_expected.to be_retryable } + end + end + + context 'when build is not retryable' do + context 'when build is running' do + before do + build.run! + end + + it { is_expected.not_to be_retryable } + end + + context 'when build is skipped' do + before do + build.skip! + end + + it { is_expected.not_to be_retryable } + end + end + end + + describe '#manual?' do + before do + build.update(when: value) + end + + subject { build.manual? } + + context 'when is set to manual' do + let(:value) { 'manual' } + + it { is_expected.to be_truthy } + end + + context 'when set to something else' do + let(:value) { 'something else' } + + it { is_expected.to be_falsey } + end + end + end + + describe '#has_tags?' do + context 'when build has tags' do + subject { create(:ci_build, tag_list: ['tag']) } + it { is_expected.to have_tags } + end + + context 'when build does not have tags' do + subject { create(:ci_build, tag_list: []) } + it { is_expected.not_to have_tags } + end + end + + describe '#keep_artifacts!' do + let(:build) { create(:ci_build, artifacts_expire_at: Time.now + 7.days) } + + it 'to reset expire_at' do + build.keep_artifacts! + + expect(build.artifacts_expire_at).to be_nil + end + end + + describe '#merge_request' do + def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now) + create(factory, source_project_id: pipeline.gl_project_id, + target_project_id: pipeline.gl_project_id, + source_branch: build.ref, + created_at: created_at) + end + + context 'when a MR has a reference to the pipeline' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request) + + commits = [double(id: pipeline.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) + end + + it 'returns the single associated MR' do + expect(build.merge_request.id).to eq(@merge_request.id) + end + end + + context 'when there is not a MR referencing the pipeline' do + it 'returns nil' do + expect(build.merge_request).to be_nil + end + end + + context 'when more than one MR have a reference to the pipeline' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request) + @merge_request.close! + @merge_request2 = create_mr(build, pipeline, factory: :merge_request) + + commits = [double(id: pipeline.sha)] + allow(@merge_request).to receive(:commits).and_return(commits) + allow(@merge_request2).to receive(:commits).and_return(commits) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request, @merge_request2]) + end + + it 'returns the first MR' do + expect(build.merge_request.id).to eq(@merge_request.id) + end + end + + context 'when a Build is created after the MR' do + before do + @merge_request = create_mr(build, pipeline, factory: :merge_request_with_diffs) + pipeline2 = create(:ci_pipeline, project: project) + @build2 = create(:ci_build, pipeline: pipeline2) + + allow(@merge_request).to receive(:commits_sha). + and_return([pipeline.sha, pipeline2.sha]) + allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request]) + end + + it 'returns the current MR' do + expect(@build2.merge_request.id).to eq(@merge_request.id) + end + end + end + + describe '#options' do + let(:options) do + { + image: "ruby:2.1", + services: [ + "postgres" + ] + } + end + + it 'contains options' do + expect(build.options).to eq(options) + end + end + + describe '#other_actions' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } + + subject { build.other_actions } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + + context 'when build is retried' do + let!(:new_build) { Ci::Build.retry(build) } + + it 'does not return any of them' do + is_expected.not_to include(build, new_build) + end + end + + context 'when other build is retried' do + let!(:retried_build) { Ci::Build.retry(other_build) } + + it 'returns a retried build' do + is_expected.to contain_exactly(retried_build) + end + end + end + + describe '#persisted_environment' do + before do + @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") + end + + subject { build.persisted_environment } + + context 'referenced literally' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } + + it { is_expected.to eq(@environment) } + end + + context 'referenced with a variable' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } + + it { is_expected.to eq(@environment) } + end + end + + describe '#play' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + + subject { build.play } + + it 'enqueues a build' do + is_expected.to be_pending + is_expected.to eq(build) + end + + context 'for successful build' do + before do + build.update(status: 'success') + end + + it 'creates a new build' do + is_expected.to be_pending + is_expected.not_to eq(build) + end + end + end + + describe 'project settings' do + describe '#timeout' do + it 'returns project timeout configuration' do + expect(build.timeout).to eq(project.build_timeout) + end + end + + describe '#allow_git_fetch' do + it 'return project allow_git_fetch configuration' do + expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch) + end + end + end + + describe '#project' do + subject { build.project } + + it { is_expected.to eq(pipeline.project) } + end + + describe '#project_id' do + subject { build.project_id } + + it { is_expected.to eq(pipeline.project_id) } + end + + describe '#project_name' do + subject { build.project_name } + + it { is_expected.to eq(project.name) } + end + + describe '#raw_trace' do + subject { build.raw_trace } + + context 'when build.trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.project.update(runners_token: token) + build.update(trace: token) + end + + it { is_expected.not_to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(token: token) + build.update(trace: token) + end + + it { is_expected.not_to include(token) } + end + end + + describe '#ref_slug' do + { + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + }.each do |ref, slug| + it "transforms #{ref} to #{slug}" do + build.ref = ref + + expect(build.ref_slug).to eq(slug) + end + end + end + + describe '#repo_url' do + let(:build) { create(:ci_build) } + let(:project) { build.project } + + subject { build.repo_url } + + it { is_expected.to be_a(String) } + it { is_expected.to end_with(".git") } + it { is_expected.to start_with(project.web_url[0..6]) } + it { is_expected.to include(build.token) } + it { is_expected.to include('gitlab-ci-token') } + it { is_expected.to include(project.web_url[7..-1]) } + end + + describe '#stuck?' do + subject { build.stuck? } + + context "when commit_status.status is pending" do + before do + build.status = 'pending' + end + + it { is_expected.to be_truthy } + + context "and there are specific runner" do + let(:runner) { create(:ci_runner, contacted_at: 1.second.ago) } + + before do + build.project.runners << runner + runner.save + end + + it { is_expected.to be_falsey } + end + end + + %w[success failed canceled running].each do |state| + context "when commit_status.status is #{state}" do + before do + build.status = state + end + + it { is_expected.to be_falsey } + end + end + end describe '#trace' do it 'obfuscates project runners token' do @@ -24,6 +972,45 @@ describe Ci::Build, models: true do expect(build.trace).to eq(test_trace) end + + context 'when build does not have trace' do + it 'is is empty' do + expect(build.trace).to be_nil + end + end + + context 'when trace contains text' do + let(:text) { 'example output' } + before do + build.trace = text + end + + it { expect(build.trace).to eq(text) } + end + + context 'when trace hides runners token' do + let(:token) { 'my_secret_token' } + + before do + build.update(trace: token) + build.project.update(runners_token: token) + end + + it { expect(build.trace).not_to include(token) } + it { expect(build.raw_trace).to include(token) } + end + + context 'when build.trace hides build token' do + let(:token) { 'my_secret_token' } + + before do + build.update(trace: token) + build.update(token: token) + end + + it { expect(build.trace).not_to include(token) } + it { expect(build.raw_trace).to include(token) } + end end describe '#has_trace_file?' do @@ -111,4 +1098,289 @@ describe Ci::Build, models: true do build.destroy end end + + describe '#when' do + subject { build.when } + + context 'when `when` is undefined' do + before do + build.when = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'when config is not found' do + let(:config) { nil } + + it { is_expected.to eq('on_success') } + end + + context 'when config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq('on_success') } + end + + context 'when config has `when`' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + when: 'always' + } + }) + end + + it { is_expected.to eq('always') } + end + end + end + end + + describe '#variables' do + let(:container_registry_enabled) { false } + let(:predefined_variables) do + [ + { key: 'CI', value: 'true', public: true }, + { key: 'GITLAB_CI', value: 'true', public: true }, + { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, + { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, + { key: 'CI_BUILD_REF', value: build.sha, public: true }, + { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, + { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, + { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true }, + { key: 'CI_BUILD_NAME', value: 'test', public: true }, + { key: 'CI_BUILD_STAGE', value: 'test', public: true }, + { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, + { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, + { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, + { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, + { key: 'CI_PROJECT_NAME', value: project.path, public: true }, + { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, + { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, + { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, + { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } + ] + end + + before do + stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com') + end + + subject { build.variables } + + context 'returns variables' do + before do + build.yaml_variables = [] + end + + it { is_expected.to eq(predefined_variables) } + end + + context 'when build has user' do + let(:user) { create(:user, username: 'starter') } + let(:user_variables) do + [ + { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true }, + { key: 'GITLAB_USER_EMAIL', value: user.email, public: true } + ] + end + + before do + build.update_attributes(user: user) + end + + it { user_variables.each { |v| is_expected.to include(v) } } + end + + context 'when build has an environment' do + before do + build.update(environment: 'production') + create(:environment, project: build.project, name: 'production', slug: 'prod-slug') + end + + let(:environment_variables) do + [ + { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true } + ] + end + + it { environment_variables.each { |v| is_expected.to include(v) } } + end + + context 'when build started manually' do + before do + build.update_attributes(when: :manual) + end + + let(:manual_variable) do + { key: 'CI_BUILD_MANUAL', value: 'true', public: true } + end + + it { is_expected.to include(manual_variable) } + end + + context 'when build is for tag' do + let(:tag_variable) do + { key: 'CI_BUILD_TAG', value: 'master', public: true } + end + + before do + build.update_attributes(tag: true) + end + + it { is_expected.to include(tag_variable) } + end + + context 'when secure variable is defined' do + let(:secure_variable) do + { key: 'SECRET_KEY', value: 'secret_value', public: false } + end + + before do + build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + end + + it { is_expected.to include(secure_variable) } + end + + context 'when build is for triggers' do + let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } + let(:user_trigger_variable) do + { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } + end + let(:predefined_trigger_variable) do + { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } + end + + before do + build.trigger_request = trigger_request + end + + it { is_expected.to include(user_trigger_variable) } + it { is_expected.to include(predefined_trigger_variable) } + end + + context 'when yaml_variables are undefined' do + before do + build.yaml_variables = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'when config is not found' do + let(:config) { nil } + + it { is_expected.to eq(predefined_variables) } + end + + context 'when config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq(predefined_variables) } + end + + context 'when config has variables' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + variables: { + KEY: 'value' + } + } + }) + end + let(:variables) do + [{ key: 'KEY', value: 'value', public: true }] + end + + it { is_expected.to eq(predefined_variables + variables) } + end + end + end + + context 'when container registry is enabled' do + let(:container_registry_enabled) { true } + let(:ci_registry) do + { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } + end + let(:ci_registry_image) do + { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } + end + + context 'and is disabled for project' do + before do + project.update(container_registry_enabled: false) + end + + it { is_expected.to include(ci_registry) } + it { is_expected.not_to include(ci_registry_image) } + end + + context 'and is enabled for project' do + before do + project.update(container_registry_enabled: true) + end + + it { is_expected.to include(ci_registry) } + it { is_expected.to include(ci_registry_image) } + end + end + + context 'when runner is assigned to build' do + let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) } + + before do + build.update(runner: runner) + end + + it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) } + it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) } + it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } + end + + context 'when build is for a deployment' do + let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } } + + before do + build.environment = 'production' + allow(project).to receive(:deployment_variables).and_return([deployment_variable]) + end + + it { is_expected.to include(deployment_variable) } + end + + context 'returns variables in valid order' do + before do + allow(build).to receive(:predefined_variables) { ['predefined'] } + allow(project).to receive(:predefined_variables) { ['project'] } + allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } + allow(build).to receive(:yaml_variables) { ['yaml'] } + allow(project).to receive(:secret_variables) { ['secret'] } + end + + it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index cebaa157ef3..d1aee27057a 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -888,6 +888,48 @@ describe Ci::Pipeline, models: true do end end + describe '#stuck?' do + before do + create(:ci_build, :pending, pipeline: pipeline) + end + + context 'when pipeline is stuck' do + it 'is stuck' do + expect(pipeline).to be_stuck + end + end + + context 'when pipeline is not stuck' do + before { create(:ci_runner, :shared, :online) } + + it 'is not stuck' do + expect(pipeline).not_to be_stuck + end + end + end + + describe '#has_yaml_errors?' do + context 'when pipeline has errors' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: nil }) + end + + it 'contains yaml errors' do + expect(pipeline).to have_yaml_errors + end + end + + context 'when pipeline does not have errors' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: { script: 'rake test' } }) + end + + it 'does not containyaml errors' do + expect(pipeline).not_to have_yaml_errors + end + end + end + describe 'notifications when pipeline success or failed' do let(:project) { create(:project) } diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 701f3323c0f..64ea607eb95 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -243,4 +243,23 @@ describe CommitStatus, models: true do .to be_a Gitlab::Ci::Status::Success end end + + describe '#sortable_name' do + tests = { + 'karma' => ['karma'], + 'karma 0 20' => ['karma ', 0, ' ', 20], + 'karma 10 20' => ['karma ', 10, ' ', 20], + 'karma 50:100' => ['karma ', 50, ':', 100], + 'karma 1.10' => ['karma ', 1, '.', 10], + 'karma 1.5.1' => ['karma ', 1, '.', 5, '.', 1], + 'karma 1 a' => ['karma ', 1, ' a'] + } + + tests.each do |name, sortable_name| + it "'#{name}' sorts as '#{sortable_name}'" do + commit_status.name = name + expect(commit_status.sortable_name).to eq(sortable_name) + end + end + end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 93eb402e060..96efe1696c3 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -63,6 +63,23 @@ describe Environment, models: true do end end + describe '#update_merge_request_metrics?' do + { 'production' => true, + 'production/eu' => true, + 'production/www.gitlab.com' => true, + 'productioneu' => false, + 'Production' => false, + 'Production/eu' => false, + 'test-production' => false + }.each do |name, expected_value| + it "returns #{expected_value} for #{name}" do + env = create(:environment, name: name) + + expect(env.update_merge_request_metrics?).to eq(expected_value) + end + end + end + describe '#first_deployment_for' do let(:project) { create(:project) } let!(:deployment) { create(:deployment, environment: environment, ref: commit.parent.id) } diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb index dd033480527..d87684fd49e 100644 --- a/spec/models/global_milestone_spec.rb +++ b/spec/models/global_milestone_spec.rb @@ -7,26 +7,72 @@ describe GlobalMilestone, models: true do let(:project1) { create(:project, group: group) } let(:project2) { create(:project, path: 'gitlab-ci', group: group) } let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) } - let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } - let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } - let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) } - let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) } - let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) } - let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) } describe '.build_collection' do + let(:milestone1_due_date) { 2.weeks.from_now.to_date } + + let!(:milestone1_project1) do + create( + :milestone, + title: "Milestone v1.2", + project: project1, + due_date: milestone1_due_date + ) + end + + let!(:milestone1_project2) do + create( + :milestone, + title: "Milestone v1.2", + project: project2, + due_date: milestone1_due_date + ) + end + + let!(:milestone1_project3) do + create( + :milestone, + title: "Milestone v1.2", + project: project3, + due_date: milestone1_due_date + ) + end + + let!(:milestone2_project1) do + create( + :milestone, + title: "VD-123", + project: project1, + due_date: nil + ) + end + + let!(:milestone2_project2) do + create( + :milestone, + title: "VD-123", + project: project2, + due_date: nil + ) + end + + let!(:milestone2_project3) do + create( + :milestone, + title: "VD-123", + project: project3, + due_date: nil + ) + end + before do - milestones = - [ - milestone1_project1, - milestone1_project2, - milestone1_project3, - milestone2_project1, - milestone2_project2, - milestone2_project3 - ] + projects = [ + project1, + project2, + project3 + ] - @global_milestones = GlobalMilestone.build_collection(milestones) + @global_milestones = GlobalMilestone.build_collection(projects, {}) end it 'has all project milestones' do @@ -40,9 +86,17 @@ describe GlobalMilestone, models: true do it 'has all project milestones' do expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6) end + + it 'sorts collection by due date' do + expect(@global_milestones.map(&:due_date)).to eq [nil, milestone1_due_date] + end end describe '#initialize' do + let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) } + let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) } + let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) } + before do milestones = [ diff --git a/spec/models/group_milestone_spec.rb b/spec/models/group_milestone_spec.rb new file mode 100644 index 00000000000..601167c3bd3 --- /dev/null +++ b/spec/models/group_milestone_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe GroupMilestone, models: true do + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:project_milestone) do + create(:milestone, title: "Milestone v1.2", project: project) + end + + describe '.build' do + it 'returns milestone with group assigned' do + milestone = GroupMilestone.build( + group, + [project], + project_milestone.title + ) + + expect(milestone.group).to eq group + end + end + + describe '.build_collection' do + before do + project_milestone + end + + it 'returns array of milestones, each with group assigned' do + milestones = GroupMilestone.build_collection(group, [project], {}) + expect(milestones).to all(have_attributes(group: group)) + end + end +end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 7758b7ffa97..5eaddd822be 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -28,6 +28,15 @@ describe Key, models: true do expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})") end end + + describe "#update_last_used_at" do + it "enqueues a UseKeyWorker job" do + key = create(:key) + + expect(UseKeyWorker).to receive(:perform_async).with(key.id) + key.update_last_used_at + end + end end context "validation of uniqueness (based on fingerprint uniqueness)" do diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 0c163659a71..a9139f7d4ab 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -31,12 +31,14 @@ describe Label, models: true do it 'validates title' do is_expected.not_to allow_value('G,ITLAB').for(:title) is_expected.not_to allow_value('').for(:title) + is_expected.not_to allow_value('s' * 256).for(:title) is_expected.to allow_value('GITLAB').for(:title) is_expected.to allow_value('gitlab').for(:title) is_expected.to allow_value('G?ITLAB').for(:title) is_expected.to allow_value('G&ITLAB').for(:title) is_expected.to allow_value("customer's request").for(:title) + is_expected.to allow_value('s' * 255).for(:title) end end diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb index d7e1a4e3b6c..497a626a418 100644 --- a/spec/models/project_services/bamboo_service_spec.rb +++ b/spec/models/project_services/bamboo_service_spec.rb @@ -1,14 +1,28 @@ require 'spec_helper' -describe BambooService, models: true do +describe BambooService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:bamboo_url) { 'http://gitlab.com/bamboo' } + + subject(:service) do + described_class.create( + project: create(:empty_project), + properties: { + bamboo_url: bamboo_url, + username: 'mic', + password: 'password', + build_key: 'foo' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end describe 'Validations' do - subject { service } - context 'when service is active' do before { subject.active = true } @@ -103,90 +117,103 @@ describe BambooService, models: true do end describe '#build_page' do - it 'returns a specific URL when status is 500' do - stub_request(status: 500) + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo') + expect(service.build_page('sha', 'ref')).to eq('foo') end + end - it 'returns a specific URL when response has no results' do - stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/foo') + expect(service.commit_status('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when bamboo_url has no trailing slash' do - stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + describe '#calculate_reactive_cache' do + context '#build_page' do + subject { service.calculate_reactive_cache('123', 'unused')[:build_page] } - expect(service(bamboo_url: 'http://gitlab.com/bamboo').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42') - end + it 'returns a specific URL when status is 500' do + stub_request(status: 500) - it 'returns a build URL when bamboo_url has a trailing slash' do - stub_request(body: %Q({"results":{"results":{"result":{"planResultKey":{"key":"42"}}}}})) + is_expected.to eq('http://gitlab.com/bamboo/browse/foo') + end - expect(service(bamboo_url: 'http://gitlab.com/bamboo/').build_page('123', 'unused')).to eq('http://gitlab.com/bamboo/browse/42') - end - end + it 'returns a specific URL when response has no results' do + stub_request(body: bamboo_response(size: 0)) - describe '#commit_status' do - it 'sets commit status to :error when status is 500' do - stub_request(status: 500) + is_expected.to eq('http://gitlab.com/bamboo/browse/foo') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end + it 'returns a build URL when bamboo_url has no trailing slash' do + stub_request(body: bamboo_response) - it 'sets commit status to "pending" when status is 404' do - stub_request(status: 404) + is_expected.to eq('http://gitlab.com/bamboo/browse/42') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + context 'bamboo_url has trailing slash' do + let(:bamboo_url) { 'http://gitlab.com/bamboo/' } - it 'sets commit status to "pending" when response has no results' do - stub_request(body: %Q({"results":{"results":{"size":"0"}}})) + it 'returns a build URL' do + stub_request(body: bamboo_response) - expect(service.commit_status('123', 'unused')).to eq('pending') + is_expected.to eq('http://gitlab.com/bamboo/browse/42') + end + end end - it 'sets commit status to "success" when build state contains Success' do - stub_request(build_state: 'YAY Success!') + context '#commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } - expect(service.commit_status('123', 'unused')).to eq('success') - end + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) - it 'sets commit status to "failed" when build state contains Failed' do - stub_request(build_state: 'NO Failed!') + is_expected.to eq(:error) + end - expect(service.commit_status('123', 'unused')).to eq('failed') - end + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) - it 'sets commit status to "pending" when build state contains Pending' do - stub_request(build_state: 'NO Pending!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + it 'sets commit status to "pending" when response has no results' do + stub_request(body: %Q({"results":{"results":{"size":"0"}}})) - it 'sets commit status to :error when build state is unknown' do - stub_request(build_state: 'FOO BAR!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end - end + it 'sets commit status to "success" when build state contains Success' do + stub_request(body: bamboo_response(build_state: 'YAY Success!')) - def service(bamboo_url: 'http://gitlab.com/bamboo') - described_class.create( - project: create(:empty_project), - properties: { - bamboo_url: bamboo_url, - username: 'mic', - password: 'password', - build_key: 'foo' - } - ) + is_expected.to eq('success') + end + + it 'sets commit status to "failed" when build state contains Failed' do + stub_request(body: bamboo_response(build_state: 'NO Failed!')) + + is_expected.to eq('failed') + end + + it 'sets commit status to "pending" when build state contains Pending' do + stub_request(body: bamboo_response(build_state: 'NO Pending!')) + + is_expected.to eq('pending') + end + + it 'sets commit status to :error when build state is unknown' do + stub_request(body: bamboo_response(build_state: 'FOO BAR!')) + + is_expected.to eq(:error) + end + end end - def stub_request(status: 200, body: nil, build_state: 'success') + def stub_request(status: 200, body: nil) bamboo_full_url = 'http://mic:password@gitlab.com/bamboo/rest/api/latest/result?label=123&os_authType=basic' - body ||= %Q({"results":{"results":{"result":{"buildState":"#{build_state}"}}}}) WebMock.stub_request(:get, bamboo_full_url).to_return( status: status, @@ -194,4 +221,8 @@ describe BambooService, models: true do body: body ) end + + def bamboo_response(result_key: 42, build_state: 'success', size: 1) + %Q({"results":{"results":{"size":"#{size}","result":{"buildState":"#{build_state}","planResultKey":{"key":"#{result_key}"}}}}}) + end end diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb index 6f65beb79d0..dbd23ff5491 100644 --- a/spec/models/project_services/buildkite_service_spec.rb +++ b/spec/models/project_services/buildkite_service_spec.rb @@ -1,6 +1,21 @@ require 'spec_helper' -describe BuildkiteService, models: true do +describe BuildkiteService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:project) { create(:empty_project) } + + subject(:service) do + described_class.create( + project: project, + properties: { + service_hook: true, + project_url: 'https://buildkite.com/account-name/example-project', + token: 'secret-sauce-webhook-token:secret-sauce-status-token' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -25,21 +40,12 @@ describe BuildkiteService, models: true do describe 'commits methods' do before do - @project = Project.new - allow(@project).to receive(:default_branch).and_return('default-brancho') - - @service = BuildkiteService.new - allow(@service).to receive_messages( - project: @project, - service_hook: true, - project_url: 'https://buildkite.com/account-name/example-project', - token: 'secret-sauce-webhook-token:secret-sauce-status-token' - ) + allow(project).to receive(:default_branch).and_return('default-brancho') end describe '#webhook_url' do it 'returns the webhook url' do - expect(@service.webhook_url).to eq( + expect(service.webhook_url).to eq( 'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token' ) end @@ -47,7 +53,7 @@ describe BuildkiteService, models: true do describe '#commit_status_path' do it 'returns the correct status page' do - expect(@service.commit_status_path('2ab7834c')).to eq( + expect(service.commit_status_path('2ab7834c')).to eq( 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c' ) end @@ -55,10 +61,53 @@ describe BuildkiteService, models: true do describe '#build_page' do it 'returns the correct build page' do - expect(@service.build_page('2ab7834c', nil)).to eq( + expect(service.build_page('2ab7834c', nil)).to eq( 'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c' ) end end + + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') + + expect(service.commit_status('sha', 'ref')).to eq('foo') + end + end + + describe '#calculate_reactive_cache' do + context '#commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } + + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + is_expected.to eq(:error) + end + + it 'sets commit status to :error when status is 404' do + stub_request(status: 404) + + is_expected.to eq(:error) + end + + it 'passes through build status untouched when status is 200' do + stub_request(body: %Q({"status":"Great Success"})) + + is_expected.to eq('Great Success') + end + end + end + end + + def stub_request(status: 200, body: nil) + body ||= %Q({"status":"success"}) + buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123' + + WebMock.stub_request(:get, buildkite_full_url).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) end end diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb index f13bb1e8adf..42c2ed668bc 100644 --- a/spec/models/project_services/drone_ci_service_spec.rb +++ b/spec/models/project_services/drone_ci_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' -describe DroneCiService, models: true do +describe DroneCiService, models: true, caching: true do + include ReactiveCachingHelpers + describe 'associations' do it { is_expected.to belong_to(:project) } it { is_expected.to have_one(:service_hook) } @@ -33,6 +35,10 @@ describe DroneCiService, models: true do let(:token) { 'secret' } let(:iid) { rand(1..9999) } + # URL's + let(:build_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } + let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } + before(:each) do allow(drone).to receive_messages( project_id: project.id, @@ -42,22 +48,66 @@ describe DroneCiService, models: true do token: token ) end + + def stub_request(status: 200, body: nil) + body ||= %Q({"status":"success"}) + + WebMock.stub_request(:get, commit_status_path).to_return( + status: status, + headers: { 'Content-Type' => 'application/json' }, + body: body + ) + end end describe "service page/path methods" do include_context :drone_ci_service - # URL's - let(:commit_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" } - let(:merge_request_page) { "#{drone_url}/gitlab/#{path}/redirect/pulls/#{iid}" } - let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" } - let(:merge_request_status_path) { "#{drone_url}/gitlab/#{path}/pulls/#{iid}?access_token=#{token}" } - - it { expect(drone.build_page(sha, branch)).to eq(commit_page) } - it { expect(drone.commit_page(sha, branch)).to eq(commit_page) } - it { expect(drone.merge_request_page(iid, sha, branch)).to eq(merge_request_page) } + it { expect(drone.build_page(sha, branch)).to eq(build_page) } it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) } - it { expect(drone.merge_request_status_path(iid, sha, branch)).to eq(merge_request_status_path) } + end + + describe '#commit_status' do + include_context :drone_ci_service + + it 'returns the contents of the reactive cache' do + stub_reactive_cache(drone, { commit_status: 'foo' }, 'sha', 'ref') + + expect(drone.commit_status('sha', 'ref')).to eq('foo') + end + end + + describe '#calculate_reactive_cache' do + include_context :drone_ci_service + + context '#commit_status' do + subject { drone.calculate_reactive_cache(sha, branch)[:commit_status] } + + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) + + is_expected.to eq(:error) + end + + it 'sets commit status to :error when status is 404' do + stub_request(status: 404) + + is_expected.to eq(:error) + end + + { "killed" => :canceled, + "failure" => :failed, + "error" => :failed, + "success" => "success", + }.each do |drone_status, our_status| + + it "sets commit status to #{our_status.inspect} when returned status is #{drone_status.inspect}" do + stub_request(body: %Q({"status":"#{drone_status}"})) + + is_expected.to eq(our_status) + end + end + end end describe "execute" do diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb index f7e878844dc..a1edd083aa1 100644 --- a/spec/models/project_services/teamcity_service_spec.rb +++ b/spec/models/project_services/teamcity_service_spec.rb @@ -1,14 +1,28 @@ require 'spec_helper' -describe TeamcityService, models: true do +describe TeamcityService, models: true, caching: true do + include ReactiveCachingHelpers + + let(:teamcity_url) { 'http://gitlab.com/teamcity' } + + subject(:service) do + described_class.create( + project: create(:empty_project), + properties: { + teamcity_url: teamcity_url, + username: 'mic', + password: 'password', + build_type: 'foo' + } + ) + end + describe 'Associations' do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } end describe 'Validations' do - subject { service } - context 'when service is active' do before { subject.active = true } @@ -103,73 +117,87 @@ describe TeamcityService, models: true do end describe '#build_page' do - it 'returns a specific URL when status is 500' do - stub_request(status: 500) + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { build_page: 'foo' }, 'sha', 'ref') - expect(service.build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo') + expect(service.build_page('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when teamcity_url has no trailing slash' do - stub_request(body: %Q({"build":{"id":"666"}})) + describe '#commit_status' do + it 'returns the contents of the reactive cache' do + stub_reactive_cache(service, { commit_status: 'foo' }, 'sha', 'ref') - expect(service(teamcity_url: 'http://gitlab.com/teamcity').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + expect(service.commit_status('sha', 'ref')).to eq('foo') end + end - it 'returns a build URL when teamcity_url has a trailing slash' do - stub_request(body: %Q({"build":{"id":"666"}})) + describe '#calculate_reactive_cache' do + context 'build_page' do + subject { service.calculate_reactive_cache('123', 'unused')[:build_page] } - expect(service(teamcity_url: 'http://gitlab.com/teamcity/').build_page('123', 'unused')).to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') - end - end + it 'returns a specific URL when status is 500' do + stub_request(status: 500) - describe '#commit_status' do - it 'sets commit status to :error when status is 500' do - stub_request(status: 500) + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildTypeId=foo') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end + it 'returns a build URL when teamcity_url has no trailing slash' do + stub_request(body: %Q({"build":{"id":"666"}})) - it 'sets commit status to "pending" when status is 404' do - stub_request(status: 404) + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + context 'teamcity_url has trailing slash' do + let(:teamcity_url) { 'http://gitlab.com/teamcity/' } - it 'sets commit status to "success" when build status contains SUCCESS' do - stub_request(build_status: 'YAY SUCCESS!') + it 'returns a build URL' do + stub_request(body: %Q({"build":{"id":"666"}})) - expect(service.commit_status('123', 'unused')).to eq('success') + is_expected.to eq('http://gitlab.com/teamcity/viewLog.html?buildId=666&buildTypeId=foo') + end + end end - it 'sets commit status to "failed" when build status contains FAILURE' do - stub_request(build_status: 'NO FAILURE!') + context 'commit_status' do + subject { service.calculate_reactive_cache('123', 'unused')[:commit_status] } - expect(service.commit_status('123', 'unused')).to eq('failed') - end + it 'sets commit status to :error when status is 500' do + stub_request(status: 500) - it 'sets commit status to "pending" when build status contains Pending' do - stub_request(build_status: 'NO Pending!') + is_expected.to eq(:error) + end - expect(service.commit_status('123', 'unused')).to eq('pending') - end + it 'sets commit status to "pending" when status is 404' do + stub_request(status: 404) - it 'sets commit status to :error when build status is unknown' do - stub_request(build_status: 'FOO BAR!') + is_expected.to eq('pending') + end - expect(service.commit_status('123', 'unused')).to eq(:error) - end - end + it 'sets commit status to "success" when build status contains SUCCESS' do + stub_request(build_status: 'YAY SUCCESS!') - def service(teamcity_url: 'http://gitlab.com/teamcity') - described_class.create( - project: create(:empty_project), - properties: { - teamcity_url: teamcity_url, - username: 'mic', - password: 'password', - build_type: 'foo' - } - ) + is_expected.to eq('success') + end + + it 'sets commit status to "failed" when build status contains FAILURE' do + stub_request(build_status: 'NO FAILURE!') + + is_expected.to eq('failed') + end + + it 'sets commit status to "pending" when build status contains Pending' do + stub_request(build_status: 'NO Pending!') + + is_expected.to eq('pending') + end + + it 'sets commit status to :error when build status is unknown' do + stub_request(build_status: 'FOO BAR!') + + is_expected.to eq(:error) + end + end end def stub_request(status: 200, body: nil, build_status: 'success') diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3ec7bb46686..e93a4e62244 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -190,34 +190,54 @@ describe Project, models: true do end it 'does not allow an invalid URI as import_url' do - project2 = build(:project, import_url: 'invalid://') + project2 = build(:empty_project, import_url: 'invalid://') expect(project2).not_to be_valid end it 'does allow a valid URI as import_url' do - project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git') + project2 = build(:empty_project, import_url: 'ssh://test@gitlab.com/project.git') expect(project2).to be_valid end it 'allows an empty URI' do - project2 = build(:project, import_url: '') + project2 = build(:empty_project, import_url: '') expect(project2).to be_valid end it 'does not produce import data on an empty URI' do - project2 = build(:project, import_url: '') + project2 = build(:empty_project, import_url: '') expect(project2.import_data).to be_nil end it 'does not produce import data on an invalid URI' do - project2 = build(:project, import_url: 'test://') + project2 = build(:empty_project, import_url: 'test://') expect(project2.import_data).to be_nil end + + describe 'project pending deletion' do + let!(:project_pending_deletion) do + create(:empty_project, + pending_delete: true) + end + let(:new_project) do + build(:empty_project, + name: project_pending_deletion.name, + namespace: project_pending_deletion.namespace) + end + + before do + new_project.validate + end + + it 'contains errors related to the project being deleted' do + expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.') + end + end end describe 'default_scope' do @@ -1525,11 +1545,13 @@ describe Project, models: true do end end - describe 'change_head' do + describe '#change_head' do let(:project) { create(:project) } - it 'calls the before_change_head method' do + it 'calls the before_change_head and after_change_head methods' do expect(project.repository).to receive(:before_change_head) + expect(project.repository).to receive(:after_change_head) + project.change_head(project.default_branch) end @@ -1545,11 +1567,6 @@ describe Project, models: true do project.change_head(project.default_branch) end - it 'expires the avatar cache' do - expect(project.repository).to receive(:expire_avatar_cache) - project.change_head(project.default_branch) - end - it 'reloads the default branch' do expect(project).to receive(:reload_default_branch) project.change_head(project.default_branch) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index af7e89eae05..99ca53938c8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1150,6 +1150,24 @@ describe Repository, models: true do end end + describe '#after_change_head' do + it 'flushes the readme cache' do + expect(repository).to receive(:expire_method_caches).with([ + :readme, + :changelog, + :license, + :contributing, + :version, + :gitignore, + :koding, + :gitlab_ci, + :avatar + ]) + + repository.after_change_head + end + end + describe '#before_push_tag' do it 'flushes the cache' do expect(repository).to receive(:expire_statistics_caches) @@ -1513,14 +1531,6 @@ describe Repository, models: true do end end - describe '#expire_avatar_cache' do - it 'expires the cache' do - expect(repository).to receive(:expire_method_caches).with(%i(avatar)) - - repository.expire_avatar_cache - end - end - describe '#file_on_head' do context 'with a non-existing repository' do it 'returns nil' do diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index a786dc9edb3..12dd4bd83f7 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -50,6 +50,8 @@ describe API::Issues, api: true do end let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + let(:no_milestone_title) { URI.escape(Milestone::None.title) } + before do project.team << [user, :reporter] project.team << [guest, :guest] @@ -107,6 +109,7 @@ describe API::Issues, api: true do it 'returns an array of labeled issues when at least one label matches' do get api("/issues?labels=#{label.title},foo,bar", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -136,6 +139,51 @@ describe API::Issues, api: true do expect(json_response.length).to eq(0) end + it 'returns an empty array if no issue matches milestone' do + get api("/issues?milestone=#{empty_milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an empty array if milestone does not exist' do + get api("/issues?milestone=foo", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of issues in given milestone' do + get api("/issues?milestone=#{milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get api("/issues?milestone=#{milestone.title}"\ + '&state=closed', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get api("/issues?milestone=#{no_milestone_title}", author) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api('/issues', user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -318,6 +366,15 @@ describe API::Issues, api: true do expect(json_response.first['id']).to eq(group_closed_issue.id) end + it 'returns an array of issues with no milestone' do + get api("#{base_url}?milestone=#{no_milestone_title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api(base_url, user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -357,7 +414,6 @@ describe API::Issues, api: true do describe "GET /projects/:id/issues" do let(:base_url) { "/projects/#{project.id}" } - let(:title) { milestone.title } it "returns 404 on private projects for other users" do private_project = create(:empty_project, :private) @@ -433,8 +489,9 @@ describe API::Issues, api: true do expect(json_response.first['labels']).to eq([label.title]) end - it 'returns an array of labeled project issues when at least one label matches' do + it 'returns an array of labeled project issues where all labels match' do get api("#{base_url}/issues?labels=#{label.title},foo,bar", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(1) @@ -463,7 +520,8 @@ describe API::Issues, api: true do end it 'returns an array of issues in given milestone' do - get api("#{base_url}/issues?milestone=#{title}", user) + get api("#{base_url}/issues?milestone=#{milestone.title}", user) + expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) @@ -480,6 +538,15 @@ describe API::Issues, api: true do expect(json_response.first['id']).to eq(closed_issue.id) end + it 'returns an array of issues with no milestone' do + get api("#{base_url}/issues?milestone=#{no_milestone_title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + it 'sorts by created_at descending by default' do get api("#{base_url}/issues", user) response_dates = json_response.map { |issue| issue['created_at'] } @@ -547,12 +614,21 @@ describe API::Issues, api: true do it 'returns a project issue by iid' do get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) + expect(response.status).to eq 200 + expect(json_response.length).to eq 1 expect(json_response.first['title']).to eq issue.title expect(json_response.first['id']).to eq issue.id expect(json_response.first['iid']).to eq issue.iid end + it 'returns an empty array for an unknown project issue iid' do + get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) + + expect(response.status).to eq 200 + expect(json_response.length).to eq 0 + end + it "returns 404 if issue id not found" do get api("/projects/#{project.id}/issues/54321", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index f5788d15f93..cdb16b4c46b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1085,7 +1085,7 @@ describe API::Projects, api: true do end describe 'GET /projects/search/:query' do - let!(:query) { 'query'} + let!(:query) { 'query'} let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } @@ -1095,32 +1095,37 @@ describe API::Projects, api: true do let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } let!(:public) { create(:empty_project, :public, name: "public #{query}") } let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } + let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") } shared_examples_for 'project search response' do |args = {}| it 'returns project search responses' do - get api("/projects/search/#{query}", current_user) + get api("/projects/search/#{args[:query]}", current_user) expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.size).to eq(args[:results]) - json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*query.*/) } + json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } end end context 'when unauthenticated' do - it_behaves_like 'project search response', results: 1 do + it_behaves_like 'project search response', query: 'query', results: 1 do let(:current_user) { nil } end end context 'when authenticated' do - it_behaves_like 'project search response', results: 6 do + it_behaves_like 'project search response', query: 'query', results: 6 do let(:current_user) { user } end + it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do + let(:current_user) { user } + end + end context 'when authenticated as a different user' do - it_behaves_like 'project search response', results: 2, match_regex: /(internal|public) query/ do + it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do let(:current_user) { user2 } end end diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index ad9d8a25af4..91e3c333a02 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -16,6 +16,8 @@ describe API::Settings, 'Settings', api: true do expect(json_response['repository_storage']).to eq('default') expect(json_response['koding_enabled']).to be_falsey expect(json_response['koding_url']).to be_nil + expect(json_response['plantuml_enabled']).to be_falsey + expect(json_response['plantuml_url']).to be_nil end end @@ -28,7 +30,8 @@ describe API::Settings, 'Settings', api: true do it "updates application settings" do put api("/application/settings", admin), - default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com' + default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com', + plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com' expect(response).to have_http_status(200) expect(json_response['default_projects_limit']).to eq(3) expect(json_response['signin_enabled']).to be_falsey @@ -36,6 +39,8 @@ describe API::Settings, 'Settings', api: true do expect(json_response['repository_storages']).to eq(['custom']) expect(json_response['koding_enabled']).to be_truthy expect(json_response['koding_url']).to eq('http://koding.example.com') + expect(json_response['plantuml_enabled']).to be_truthy + expect(json_response['plantuml_url']).to eq('http://plantuml.example.com') end end @@ -47,5 +52,14 @@ describe API::Settings, 'Settings', api: true do expect(json_response['error']).to eq('koding_url is missing') end end + + context "missing plantuml_url value when plantuml_enabled is true" do + it "returns a blank parameter error message" do + put api("/application/settings", admin), plantuml_enabled: true + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('plantuml_url is missing') + end + end end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 45b7988a054..5bf5bf0739e 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -137,6 +137,15 @@ describe API::Users, api: true do expect(new_user.can_create_group).to eq(true) end + it "creates user with optional attributes" do + optional_attributes = { confirm: true } + attributes = attributes_for(:user).merge(optional_attributes) + + post api('/users', admin), attributes + + expect(response).to have_http_status(201) + end + it "creates non-admin user" do post api('/users', admin), attributes_for(:user, admin: false, can_create_group: false) expect(response).to have_http_status(201) @@ -265,6 +274,14 @@ describe API::Users, api: true do expect(response).to have_http_status(409) expect(json_response['message']).to eq('Username has already been taken') end + + it 'creates user with new identity' do + post api("/users", admin), attributes_for(:user, provider: 'github', extern_uid: '67890') + + expect(response).to have_http_status(201) + expect(json_response['identities'].first['extern_uid']).to eq('67890') + expect(json_response['identities'].first['provider']).to eq('github') + end end end diff --git a/spec/serializers/build_action_entity_spec.rb b/spec/serializers/build_action_entity_spec.rb new file mode 100644 index 00000000000..0f7be8b2c39 --- /dev/null +++ b/spec/serializers/build_action_entity_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe BuildActionEntity do + let(:build) { create(:ci_build, name: 'test_build') } + + let(:entity) do + described_class.new(build, request: double) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains original build name' do + expect(subject[:name]).to eq 'test_build' + end + + it 'contains path to the action play' do + expect(subject[:path]).to include "builds/#{build.id}/play" + end + end +end diff --git a/spec/serializers/build_artifact_entity_spec.rb b/spec/serializers/build_artifact_entity_spec.rb new file mode 100644 index 00000000000..2fc60aa9de6 --- /dev/null +++ b/spec/serializers/build_artifact_entity_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe BuildArtifactEntity do + let(:build) { create(:ci_build, name: 'test:build') } + + let(:entity) do + described_class.new(build, request: double) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains build name' do + expect(subject[:name]).to eq 'test:build' + end + + it 'contains path to the artifacts' do + expect(subject[:path]) + .to include "builds/#{build.id}/artifacts/download" + end + end +end diff --git a/spec/serializers/commit_entity_spec.rb b/spec/serializers/commit_entity_spec.rb index 15f11ac3df9..0333d73b5b5 100644 --- a/spec/serializers/commit_entity_spec.rb +++ b/spec/serializers/commit_entity_spec.rb @@ -33,10 +33,12 @@ describe CommitEntity do it 'contains path to commit' do expect(subject).to include(:commit_path) + expect(subject[:commit_path]).to include "commit/#{commit.id}" end it 'contains URL to commit' do expect(subject).to include(:commit_url) + expect(subject[:commit_path]).to include "commit/#{commit.id}" end it 'needs to receive project in the request' do @@ -45,4 +47,8 @@ describe CommitEntity do subject end + + it 'exposes gravatar url that belongs to author' do + expect(subject.fetch(:author_gravatar_url)).to match /gravatar/ + end end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb new file mode 100644 index 00000000000..b19464c7117 --- /dev/null +++ b/spec/serializers/pipeline_entity_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' + +describe PipelineEntity do + let(:user) { create(:user) } + let(:request) { double('request') } + + before do + allow(request).to receive(:user).and_return(user) + end + + let(:entity) do + described_class.represent(pipeline, request: request) + end + + describe '#as_json' do + subject { entity.as_json } + + context 'when pipeline is empty' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'contains required fields' do + expect(subject).to include :id, :user, :path + expect(subject).to include :ref, :commit + expect(subject).to include :updated_at, :created_at + end + + it 'contains details' do + expect(subject).to include :details + expect(subject[:details]) + .to include :duration, :finished_at + expect(subject[:details]) + .to include :stages, :artifacts, :manual_actions + expect(subject[:details][:status]).to include :icon, :text, :label + end + + it 'contains flags' do + expect(subject).to include :flags + expect(subject[:flags]) + .to include :latest, :triggered, :stuck, + :yaml_errors, :retryable, :cancelable + end + end + + context 'when pipeline is retryable' do + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_pipeline, status: :success, project: project) + end + + before do + create(:ci_build, :failed, pipeline: pipeline) + end + + context 'user has ability to retry pipeline' do + before { project.team << [user, :developer] } + + it 'retryable flag is true' do + expect(subject[:flags][:retryable]).to eq true + end + + it 'contains retry path' do + expect(subject[:retry_path]).to be_present + end + end + + context 'user does not have ability to retry pipeline' do + it 'retryable flag is false' do + expect(subject[:flags][:retryable]).to eq false + end + + it 'does not contain retry path' do + expect(subject).not_to have_key(:retry_path) + end + end + end + + context 'when pipeline is cancelable' do + let(:project) { create(:empty_project) } + + let(:pipeline) do + create(:ci_pipeline, status: :running, project: project) + end + + before do + create(:ci_build, :pending, pipeline: pipeline) + end + + context 'user has ability to cancel pipeline' do + before { project.team << [user, :developer] } + + it 'cancelable flag is true' do + expect(subject[:flags][:cancelable]).to eq true + end + + it 'contains cancel path' do + expect(subject[:cancel_path]).to be_present + end + end + + context 'user does not have ability to cancel pipeline' do + it 'cancelable flag is false' do + expect(subject[:flags][:cancelable]).to eq false + end + + it 'does not contain cancel path' do + expect(subject).not_to have_key(:cancel_path) + end + end + end + + context 'when pipeline has YAML errors' do + let(:pipeline) do + create(:ci_pipeline, config: { rspec: { invalid: :value } }) + end + + it 'contains flag that indicates there are errors' do + expect(subject[:flags][:yaml_errors]).to be true + end + + it 'contains information about error' do + expect(subject[:yaml_errors]).to be_present + end + end + + context 'when pipeline does not have YAML errors' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'contains flag that indicates there are no errors' do + expect(subject[:flags][:yaml_errors]).to be false + end + + it 'does not contain field that normally holds an error' do + expect(subject).not_to have_key(:yaml_errors) + end + end + end +end diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb new file mode 100644 index 00000000000..3a32cb394dd --- /dev/null +++ b/spec/serializers/pipeline_serializer_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe PipelineSerializer do + let(:user) { create(:user) } + + let(:serializer) do + described_class.new(user: user) + end + + let(:entity) do + serializer.represent(resource) + end + + subject { entity.as_json } + + describe '#represent' do + context 'when used without pagination' do + it 'created a not paginated serializer' do + expect(serializer).not_to be_paginated + end + + context 'when a single object is being serialized' do + let(:resource) { create(:ci_empty_pipeline) } + + it 'serializers the pipeline object' do + expect(subject[:id]).to eq resource.id + end + end + + context 'when multiple objects are being serialized' do + let(:resource) { create_list(:ci_pipeline, 2) } + + it 'serializers the array of pipelines' do + expect(subject).not_to be_empty + end + end + end + + context 'when used with pagination' do + let(:request) { spy('request') } + let(:response) { spy('response') } + let(:pagination) { {} } + + before do + allow(request) + .to receive(:query_parameters) + .and_return(pagination) + end + + let(:serializer) do + described_class.new(user: user) + .with_pagination(request, response) + end + + it 'created a paginated serializer' do + expect(serializer).to be_paginated + end + + context 'when resource does is not paginatable' do + context 'when a single pipeline object is being serialized' do + let(:resource) { create(:ci_empty_pipeline) } + let(:pagination) { { page: 1, per_page: 1 } } + + it 'raises error' do + expect { subject } + .to raise_error(PipelineSerializer::InvalidResourceError) + end + end + end + + context 'when resource is paginatable relation' do + let(:resource) { Ci::Pipeline.all } + let(:pagination) { { page: 1, per_page: 2 } } + + context 'when a single pipeline object is present in relation' do + before { create(:ci_empty_pipeline) } + + it 'serializes pipeline relation' do + expect(subject.first).to have_key :id + end + end + + context 'when a multiple pipeline objects are being serialized' do + before { create_list(:ci_empty_pipeline, 3) } + + it 'serializes appropriate number of objects' do + expect(subject.count).to be 2 + end + + it 'appends relevant headers' do + expect(response).to receive(:[]=).with('X-Total', '3') + expect(response).to receive(:[]=).with('X-Total-Pages', '2') + expect(response).to receive(:[]=).with('X-Per-Page', '2') + + subject + end + end + end + end + end +end diff --git a/spec/serializers/request_aware_entity_spec.rb b/spec/serializers/request_aware_entity_spec.rb new file mode 100644 index 00000000000..aa666b961dc --- /dev/null +++ b/spec/serializers/request_aware_entity_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe RequestAwareEntity do + subject do + Class.new.include(described_class).new + end + + it 'includes URL helpers' do + expect(subject).to respond_to(:namespace_project_path) + end + + it 'includes method for checking abilities' do + expect(subject).to respond_to(:can?) + end + + it 'fetches request from options' do + expect(subject).to receive(:options) + .and_return({ request: 'some value' }) + + expect(subject.request).to eq 'some value' + end +end diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb new file mode 100644 index 00000000000..4ab40d08432 --- /dev/null +++ b/spec/serializers/stage_entity_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe StageEntity do + let(:pipeline) { create(:ci_pipeline) } + let(:request) { double('request') } + let(:user) { create(:user) } + + let(:entity) do + described_class.new(stage, request: request) + end + + let(:stage) do + build(:ci_stage, pipeline: pipeline, name: 'test') + end + + before do + allow(request).to receive(:user).and_return(user) + create(:ci_build, :success, pipeline: pipeline) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains relevant fields' do + expect(subject).to include :name, :status, :path + end + + it 'contains detailed status' do + expect(subject[:status]).to include :text, :label, :group, :icon + expect(subject[:status][:label]).to eq 'passed' + end + + it 'contains valid name' do + expect(subject[:name]).to eq 'test' + end + + it 'contains path to the stage' do + expect(subject[:path]) + .to include "pipelines/#{pipeline.id}##{stage.name}" + end + + it 'contains path to the stage dropdown' do + expect(subject[:dropdown_path]) + .to include "pipelines/#{pipeline.id}/stage.json?stage=test" + end + + it 'contains stage title' do + expect(subject[:title]).to eq 'test: passed' + end + end +end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb new file mode 100644 index 00000000000..89428b4216e --- /dev/null +++ b/spec/serializers/status_entity_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe StatusEntity do + let(:entity) { described_class.new(status) } + + let(:status) do + Gitlab::Ci::Status::Success.new(double('object'), double('user')) + end + + before do + allow(status).to receive(:has_details?).and_return(true) + allow(status).to receive(:details_path).and_return('some/path') + end + + describe '#as_json' do + subject { entity.as_json } + + it 'contains status details' do + expect(subject).to include :text, :icon, :label, :group + expect(subject).to include :has_details, :details_path + end + end +end diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb new file mode 100644 index 00000000000..063b3bd76eb --- /dev/null +++ b/spec/services/projects/participants_service_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Projects::ParticipantsService, services: true do + describe '#groups' do + describe 'avatar_url' do + let(:project) { create(:empty_project, :public) } + let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) } + let(:user) { create(:user) } + let(:base_url) { Settings.send(:build_base_gitlab_url) } + let!(:group_member) { create(:group_member, group: group, user: user) } + + it 'should return an url for the avatar' do + participants = described_class.new(project, user) + groups = participants.groups + + expect(groups.size).to eq 1 + expect(groups.first[:avatar_url]).to eq "#{base_url}/uploads/group/avatar/#{group.id}/dk.png" + end + + it 'should return an url for the avatar with relative url' do + stub_config_setting(relative_url_root: '/gitlab') + stub_config_setting(url: Settings.send(:build_gitlab_url)) + + participants = described_class.new(project, user) + groups = participants.groups + + expect(groups.size).to eq 1 + expect(groups.first[:avatar_url]).to eq "#{base_url}/gitlab/uploads/group/avatar/#{group.id}/dk.png" + end + end + end +end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 1f6919151de..9fbb61565e3 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -20,7 +20,7 @@ describe Users::RefreshAuthorizedProjectsService do to_remove = create_authorization(project2, user) expect(service).to receive(:update_with_lease). - with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]]) + with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) service.execute end @@ -29,7 +29,7 @@ describe Users::RefreshAuthorizedProjectsService do to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) expect(service).to receive(:update_with_lease). - with([to_remove.id], [[user.id, project.id, Gitlab::Access::MASTER]]) + with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) service.execute end @@ -90,7 +90,7 @@ describe Users::RefreshAuthorizedProjectsService do it 'removes authorizations that should be removed' do authorization = create_authorization(project, user) - service.update_authorizations([authorization.id]) + service.update_authorizations([authorization.project_id]) expect(user.project_authorizations).to be_empty end @@ -147,7 +147,12 @@ describe Users::RefreshAuthorizedProjectsService do end it 'sets the values to the project authorization rows' do - expect(hash.values).to eq([ProjectAuthorization.first]) + expect(hash.values.length).to eq(1) + + value = hash.values[0] + + expect(value.project_id).to eq(project.id) + expect(value.access_level).to eq(Gitlab::Access::MASTER) end end @@ -167,10 +172,6 @@ describe Users::RefreshAuthorizedProjectsService do expect(service.current_authorizations.length).to eq(1) end - it 'includes the row ID for every row' do - expect(row.id).to be_a_kind_of(Numeric) - end - it 'includes the project ID for every row' do expect(row.project_id).to eq(project.id) end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb index 99e98eebdb4..0b8729db0f9 100644 --- a/spec/support/javascript_fixtures_helpers.rb +++ b/spec/support/javascript_fixtures_helpers.rb @@ -20,12 +20,26 @@ module JavaScriptFixturesHelpers # Public: Store a response object as fixture file # - # response - response object to store + # response - string or response object to store # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH) # def store_frontend_fixture(response, fixture_file_name) fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH) + fixture = response.respond_to?(:body) ? parse_response(response) : response + + FileUtils.mkdir_p(File.dirname(fixture_file_name)) + File.write(fixture_file_name, fixture) + end + + private + + # Private: Prepare a response object for use as a frontend fixture + # + # response - response object to prepare + # + def parse_response(response) fixture = response.body + fixture.force_encoding("utf-8") response_mime_type = Mime::Type.lookup(response.content_type) if response_mime_type.html? @@ -34,7 +48,7 @@ module JavaScriptFixturesHelpers link_tags = doc.css('link') link_tags.remove - scripts = doc.css('script') + scripts = doc.css("script:not([type='text/template'])") scripts.remove fixture = doc.to_html @@ -44,7 +58,6 @@ module JavaScriptFixturesHelpers fixture.gsub!(%r{="/}, "=\"http://#{test_host}/") end - FileUtils.mkdir_p(File.dirname(fixture_file_name)) - File.write(fixture_file_name, fixture) + fixture end end diff --git a/spec/support/reactive_caching_helpers.rb b/spec/support/reactive_caching_helpers.rb index 279db3c5748..98eb57f8b54 100644 --- a/spec/support/reactive_caching_helpers.rb +++ b/spec/support/reactive_caching_helpers.rb @@ -3,31 +3,35 @@ module ReactiveCachingHelpers ([subject.class.reactive_cache_key.call(subject)].flatten + qualifiers).join(':') end - def stub_reactive_cache(subject = nil, data = nil) + def alive_reactive_cache_key(subject, *qualifiers) + reactive_cache_key(subject, *(qualifiers + ['alive'])) + end + + def stub_reactive_cache(subject = nil, data = nil, *qualifiers) allow(ReactiveCachingWorker).to receive(:perform_async) allow(ReactiveCachingWorker).to receive(:perform_in) - write_reactive_cache(subject, data) if data + write_reactive_cache(subject, data, *qualifiers) if data end - def read_reactive_cache(subject) - Rails.cache.read(reactive_cache_key(subject)) + def read_reactive_cache(subject, *qualifiers) + Rails.cache.read(reactive_cache_key(subject, *qualifiers)) end - def write_reactive_cache(subject, data) - start_reactive_cache_lifetime(subject) - Rails.cache.write(reactive_cache_key(subject), data) + def write_reactive_cache(subject, data, *qualifiers) + start_reactive_cache_lifetime(subject, *qualifiers) + Rails.cache.write(reactive_cache_key(subject, *qualifiers), data) end - def reactive_cache_alive?(subject) - Rails.cache.read(reactive_cache_key(subject, 'alive')) + def reactive_cache_alive?(subject, *qualifiers) + Rails.cache.read(alive_reactive_cache_key(subject, *qualifiers)) end - def invalidate_reactive_cache(subject) - Rails.cache.delete(reactive_cache_key(subject, 'alive')) + def invalidate_reactive_cache(subject, *qualifiers) + Rails.cache.delete(alive_reactive_cache_key(subject, *qualifiers)) end - def start_reactive_cache_lifetime(subject) - Rails.cache.write(reactive_cache_key(subject, 'alive'), true) + def start_reactive_cache_lifetime(subject, *qualifiers) + Rails.cache.write(alive_reactive_cache_key(subject, *qualifiers), true) end def expect_reactive_cache_update_queued(subject) diff --git a/spec/support/seed_helper.rb b/spec/support/seed_helper.rb index 3f8398a31e3..03fa0a66b9a 100644 --- a/spec/support/seed_helper.rb +++ b/spec/support/seed_helper.rb @@ -25,32 +25,32 @@ module SeedHelper end def create_bare_seeds - system(git_env, *%W(git clone --bare #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{GITLAB_URL}), chdir: SEED_REPOSITORY_PATH, out: '/dev/null', err: '/dev/null') end def create_normal_seeds - system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_NORMAL_REPO_PATH}), out: '/dev/null', err: '/dev/null') end def create_mutable_seeds - system(git_env, *%W(git clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone #{TEST_REPO_PATH} #{TEST_MUTABLE_REPO_PATH}), out: '/dev/null', err: '/dev/null') system(git_env, *%w(git branch -t feature origin/feature), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') - system(git_env, *%W(git remote add expendable #{GITLAB_URL}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} remote add expendable #{GITLAB_URL}), chdir: TEST_MUTABLE_REPO_PATH, out: '/dev/null', err: '/dev/null') end def create_broken_seeds - system(git_env, *%W(git clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}), + system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_BROKEN_REPO_PATH}), out: '/dev/null', err: '/dev/null') diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb new file mode 100644 index 00000000000..18597b5c71f --- /dev/null +++ b/spec/support/stub_env.rb @@ -0,0 +1,7 @@ +module StubENV + def stub_env(key, value) + allow(ENV).to receive(:[]).and_call_original unless @env_already_stubbed + @env_already_stubbed ||= true + allow(ENV).to receive(:[]).with(key).and_return(value) + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index a9fea5f1e81..bc751d20ce1 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -41,7 +41,7 @@ describe 'gitlab:app namespace rake task' do context 'gitlab version' do before do - allow(Dir).to receive(:glob).and_return([]) + allow(Dir).to receive(:glob).and_return(['1_gitlab_backup.tar']) allow(Dir).to receive(:chdir) allow(File).to receive(:exist?).and_return(true) allow(Kernel).to receive(:system).and_return(true) diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 33cabd14913..7f123b15194 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -7,6 +7,7 @@ describe 'projects/merge_requests/show.html.haml' do let(:project) { create(:project) } let(:fork_project) { create(:project, forked_from_project: project) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) } let(:closed_merge_request) do create(:closed_merge_request, @@ -19,8 +20,12 @@ describe 'projects/merge_requests/show.html.haml' do assign(:project, project) assign(:merge_request, closed_merge_request) assign(:commits_count, 0) + assign(:note, note) + assign(:noteable, closed_merge_request) + assign(:notes, []) + assign(:pipelines, Ci::Pipeline.none) - allow(view).to receive(:can?).and_return(true) + allow(view).to receive_messages(current_user: user, can?: true) end context 'when the merge request is closed' do diff --git a/spec/views/shared/milestones/_issuables.html.haml.rb b/spec/views/shared/milestones/_issuables.html.haml.rb new file mode 100644 index 00000000000..4769d569548 --- /dev/null +++ b/spec/views/shared/milestones/_issuables.html.haml.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'shared/milestones/_issuables.html.haml' do + let(:issuables_size) { 100 } + + before do + allow(view).to receive_messages(title: nil, id: nil, show_project_name: nil, + show_full_project_name: nil, dom_class: '', + issuables: double(size: issuables_size).as_null_object) + + stub_template 'shared/milestones/_issuable.html.haml' => '' + end + + it 'should show the issuables count if show_counter is true' do + render 'shared/milestones/issuables', show_counter: true + expect(rendered).to have_content('100') + end + + it 'should not show the issuables count if show_counter is false' do + render 'shared/milestones/issuables', show_counter: false + expect(rendered).not_to have_content('100') + end + + describe 'a high issuables count' do + let(:issuables_size) { 1000 } + + it 'should show a delimited number if show_counter is true' do + render 'shared/milestones/issuables', show_counter: true + expect(rendered).to have_content('1,000') + end + end +end diff --git a/spec/workers/use_key_worker_spec.rb b/spec/workers/use_key_worker_spec.rb new file mode 100644 index 00000000000..e50c788b82a --- /dev/null +++ b/spec/workers/use_key_worker_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe UseKeyWorker do + describe "#perform" do + it "updates the key's last_used_at attribute to the current time when it exists" do + worker = described_class.new + key = create(:key) + current_time = Time.zone.now + + Timecop.freeze(current_time) do + expect { worker.perform(key.id) } + .to change { key.reload.last_used_at }.from(nil).to be_like_time(current_time) + end + end + + it "returns false and skips the job when the key doesn't exist" do + worker = described_class.new + key = create(:key) + + expect(worker.perform(key.id + 1)).to eq false + end + end +end |