diff options
author | Eric Eastwood <contact@ericeastwood.com> | 2018-02-22 18:34:44 +0300 |
---|---|---|
committer | Eric Eastwood <contact@ericeastwood.com> | 2018-02-22 18:34:44 +0300 |
commit | 2e29597c942a524d206895510a10c541ff724c71 (patch) | |
tree | e564de8aa995a9d2dbce3049c7022eb55647b4b4 /spec | |
parent | 21c16b900be62bb7074753517bacf5b59f971d2c (diff) | |
parent | 275efeeb529cdcba606c3d36f4d700551d5ba371 (diff) |
Merge branch 'master' into 42431-add-auto-devops-and-clusters-button-to-projects
Diffstat (limited to 'spec')
125 files changed, 3398 insertions, 1363 deletions
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb index d7825364ed5..c1f42bbb9d7 100644 --- a/spec/controllers/concerns/issuable_collections_spec.rb +++ b/spec/controllers/concerns/issuable_collections_spec.rb @@ -8,6 +8,10 @@ describe IssuableCollections do def self.helper_method(name); end include IssuableCollections + + def finder_type + IssuesFinder + end end controller = klass.new diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 6ba599cdf83..f6ba3a581ca 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -180,8 +180,8 @@ FactoryBot.define do trait :artifacts do after(:create) do |build| - create(:ci_job_artifact, :archive, job: build) - create(:ci_job_artifact, :metadata, job: build) + create(:ci_job_artifact, :archive, job: build, expire_at: build.artifacts_expire_at) + create(:ci_job_artifact, :metadata, job: build, expire_at: build.artifacts_expire_at) build.reload end end diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index f0c43f3d6f5..3f0c60f32b7 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -5,6 +5,10 @@ FactoryBot.define do title key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' } + factory :key_without_comment do + key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate } + end + factory :deploy_key, class: 'DeployKey' factory :personal_key do @@ -18,38 +22,104 @@ FactoryBot.define do factory :rsa_key_2048 do key do <<~KEY.delete("\n") - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9 - 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5 - /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7 - M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC - rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0 - 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC98dbu7gxcbmAvwMqz/6AALhSr1jiX + G0UC8FQMvoDt+ciB+uSJhg7KlxinKjYJnPGfhX+q2K+mmCGAmI/D6q7rFxE+bn09O+75 + qgkTHi+suDVE6KG7L3n0alGd/qSevfomR77Snh6fQPdG6sEAZz3kehcpfVnq5/IuLFq9 + FBrgmu52Jd4XZLQZKkDq6zYOJ69FUkGf93LZIV/OOaS+f+qkOGPCUkdKl7oEcgpVNY9S + RjBCduXnvi2CyQnnJVkBguGL5VlXwFXH+17Whs7oFWmdiG+4jzBRLIMz4EuIW09b8Su5 + PW6+bBuXOifHA8KG5TMmjs5LYdCMPFnhTyDyO3a1 dummy@gitlab.com KEY end factory :rsa_deploy_key_2048, class: 'DeployKey' end + factory :rsa_key_4096 do + key do + <<~KEY.delete("\n") + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDGSD77lLtjmzewiBs6nu2R5nu6oNkrA + kH/0co1fHHosKfRr+sWkSTKXOVcL7bhRu+tniGBmB5pn+i1qX7BXtrcnv//bCXWIp+me0 + 27L4RJa5/Ep077iiTJlzTpcV664xNUXC8mzBr601HR/Z2TzX5DWJvnyqqFkN7qHTYo/+I + oKECnKqNzI5SQrAxgi6sbWA5DFQ/nwcqsUSBo5gCCJ/0QPrR19yVV5lJA19EY2LawOb1S + JNOFo4mQupSlBZwvERZJ7IqhBTPtQIfrqqz5VJbI13jK3ViZTugIZqydWAhosUyejP3Sd + Cj1KMexrvV95tjUtmhVFlph4tKThQO0p9pXKZNCzYsbQTye6O6Hk2rojOJLyFWqNBVKtI + 8Ymfu7OQWppRnuUFuhuuS515H1s888bZFMPsC74mPyo0Y7Q9wAoTnQ9Hw6b0J6OfY3PIR + VphaCmxh6b7dgSPFdD7TA6j0xk6PCTOIEzBKuc85B3GQc8Nt4sTv6fW8lGeuYWqepW74i + geC4qB6U3/3+p3nPdq/bTM1txrhnQsl1r4dv6TLZ51EtHp6sXayp0qd0pRaiavebXFC0i + aETLraQpye4FWbBL/8xTjQ/0VPrYVuUCDvDSMIIS3/9g7Kp7ERUDC9jUqOVonm4pTXL9i + ItiUBlK7Mob9C4fQIRFnVR00DCmkmVgw== dummy@gitlab.com + KEY + end + end + + factory :rsa_key_5120 do + key do + <<~KEY.delete("\n") + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACgQDxnZP0TucLH3zcrvt75DPNq+xKqOmJk + CEzTytKq4S5MDH0nlx+xOZ9WykhwDHXU0iZBJF7yRdLkZweYDJVKnBzr4t7QP5Sw2/ZdL + elvUMWGJjuz28x8Z+8NZ+IxL/exDz7itrhCsLupQhGO1obiIwf8xVzzPoxrQ9dxaN4x96 + 5N+QdQcld8O6xfpSE0p5Y3sRn3kp57aHWoNa/bUGZy0OHLr/ig0uc6EKyWsTmEESOgDyV + 94wOyHR0KNGEENyxQt4BwAbEBn3Y41HKqD358KKh+XjbECebrrBFigdDL/eYFIUlstJ07 + SK/HtYjZbiUZCPs8bJA+SBaLK0pGGqguM2LXRoMeMUZFwKKKS2LpRqjKGj3Qt7qMnp1Sk + VhiMnxNqL4nJnDOOVo07xDIPKqIBYO67/cp4Icv3IjKxy6K3EIpLr+iRCxcllpDogxolz + FC+pEDVpmEvcrGEv1ON6HcCdk/6Q8Iekr8rYDHpKCU5FF2uBHkqq7yNJ1/+NFC4dgyOo0 + xCVL4D3DvDKNxFYkrzW4ICt0f5XcMnU10yS/OFXz8JwA3jvuLvMRe5JdFiIjb/l86+TgY + yvK8Y8N/UWgSgyjXUCv8nxdvpsxdz5h7HBF8E2DIxCVMC23655e5rp5eJW9EU9X5YFZc3 + u6uWJ1f1aO+1ViTtqkPrqxovNDD+gVel8Ny6MJ4MvmDKY+eM8beNMSSf1n1Oyh/SvCffh + ZpUqrXdTr9qwZEOaC75T74AJ7KBl9VvO3vPLZuJrt38R2OZG/4SlNEUA6bb5TWQLtdor/ + qpPN5jAskkAUzOh5L/M+dmq2jNn03U9xwORCYPZj+fFM9bL99/0knsV0ypZDZyWH dummy@gitlab.com + KEY + end + end + + factory :rsa_key_8192 do + key do + <<~KEY.delete("\n") + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAEAQC5jMyGtgMOVX4t2GuXkbirJA0Edr+ql + OH9grnRBPHPo0Npt6XE6ZN3J3hDULTQo03wmekGw42dxdNSgk+F0GjsUBrMLbqrk485MM + e0cUbP4lRXNu4ao87wPVM5fAsD4E3FQiZcI6Df011ZGIL7hGTHt6eafTfr9cJheRyYSu6 + g06rlnFWbbtSh9oQ7Y6sfDLBcsC9ECcXwe3mwViuQXPIVomZ02EdnBbAhbGHDtA+ZbSvT + fraxOMjkxkVvvdjLxXEykpwVuZf8eZ+R/Js8jQ5RKvTZMbfxJNsGEqHD32s43ml4VF549 + Qz2GJDXF7Cld/n3CT6wvw0mMPM0LnykL2v0CMr44bjIA3KsNEs5MhkcBO8sv5hGfcPhrp + m9WwI6gd9vdZVcxarVI+iQS947owvdn4VbEZXynCDqEEv3Zh+FA5p23mf2p7DkG/swiK/ + IPrjr1wmsiWmwIUsENzJNyJtibKuRsBawC4ZdL797tFilSoTzSpriegSL13joPXz3eOHC + Vu4ATHMo3QyLfIFbxrf9PQ79nyOpHoX2YeFXvei3xFkGMundkOqeI+pnJKDyqbiLV7UVl + clua11QWNQZf1ZUd0n1wZ1g89de+wl3oJSRbSA5ZpveZEPstcMC/JhogY4JBYsvCT1yHO + oNWHo90NZQsUCjNnR+/FVaACtpt2zcPTjjbXvxwCDlT3gXTmTBp/kEZq6u8p+BOlqFgxc + P/sdAR8jWTin3Iw/YAcbqNgRHdjMUzJBrPQ5NcK6xFcmkOEQahdJDZs98xozCHkD4Urx6 + +auTr/uqRYobKoNUNiYqN1n7/dfZjQJJVkHtKd06JTFx+7/SqyfrTKS+/EIf2Hypdy9r9 + IFR+SWAOi11N/wflS/ZbH95Qt3STifXRecmHzyYGkMOZ+mg3Hi2YU0yn7k+P1jy627xud + pT9Ak3HWT5ji8tMyn9udL7m80dYpUiEAxoYZdbSSNCDaKP4ViABnGIeZreIujabI8IdtE + IjFQTaF2d5HTYjp28/qf576CFP5L7AGydypipYqZUmsYnay5YVjdm89He3TMD71SwspJl + POC4RnM0HS87OE+U0+mVaIe8YYbcjTekpVU9mkqsE/GQ34Egw79VMNNgWq5avOzpT8msC + lTJxgfJ1agGgigTvGxUM0FB07+sIdJxxNymAGpLKZ1op8xaJI3o8D86jWgI22za1zxUB5 + il9U7+KOzaWo9mp3bmhvZWGDwzTXEZhUJYMRby7o6UxSHlA6fKE63JSDD2yhXk4CjsQRN + C7Ph9cYSB+Wa3i9Am4rRlJgrF79okmEOMpj1idliHkpIsy/k2CN9Lf2EIHOD4NMuLrSUH + 4qJsPUq19ZbGIMdImD3vMS5b dummy@gitlab.com + KEY + end + end + factory :dsa_key_2048 do key do <<~KEY.delete("\n") - ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G - Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp - YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ - /pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz - OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv - 5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB - AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t - poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1 - M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH - MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H - nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A - 1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb - aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI - zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex - PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z - wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS - Taja+Cf9kMo== dummy@gitlab.com + ssh-dss AAAAB3NzaC1kc3MAAAEBALEB3sM2kPy6LKLiyL+UlDx2vzuKrzSD2nsW2Kb7 + 0ivIqDNJu5CbqIQSkjdMzJiocs33ESFqXid6ezOtVdDwXHJQRxKGalW1kBbFAPjtMxlD + bf559+7qN2zfCfcQsgTmNAZ7O+wltqJmyLv5i4QqNwPDvyeBvJ4C+770DzlcQtpkflKJ + X+O7i8Ylq34h6UTCTnjry+dFVm1xz97LPf7XuzXGZcAG/eGUNQgxQ2bferKnrpYOXx6c + ocSRj9W54nrRFMWuDeOspWp4MoYK0FRMfDQYPksUayGUnm1KQTGuDbB0ahRNCOm8b3tf + P9Z+vjANAkqenzDuXCpz2PU/Oj6/N/UAAAAhAPOLyut12Mjcp3eUXLe1xSoI5IRXSLso + W9no93dcFNprAAABAQCLhpqKY+PNcwbhhPruL+f+uROghHzDwRNX+e231F4wHHeDDomf + WyLVFj31XrHdDXZnS9tTTj5D2XWLovSSxYb3H7earTctmktL0lQ3HapujzvOkn+VM0pG + s6B3j54+AM3mg50KZdYWxxv+v/lb6oEcsCjfKNyRIx/5pqX6XI3dxl9MMIxrfVWpkNX+ + FI68v1LVV61DC9PkNyEHU0v9YBOfrTiS21TIlVIZcSFhuDjg52MekfZAnoKaP7YFJNF3 + fdCrXaU3hYQrwB9XdskBUppwxKGhf7O6SWEZhAEfPA9kgxaWHoJvsDz8aca576UNe7BP + mjzo/SLUX+P4uvcaffd+AAABAEqzpmwjzTxB+DV8C+0LnmKf3L/UlQWyGdmhd65rnbkH + GgRMAAkoh4GBOEHL5bznNRmO7X/H6g2fR7SEabxfbvb903KI4nbfFF+3QtnwyIbTBAcH + 0893D3bi5rsaJcz+c6lBob2En2nThRciefXUk2oPzCQuDyFIyHLJikqRQVcalHCdQ00c + /H/JkiJedHNqaeU4TeMk8SM53Brjplj/iiJq+ujc5MlEgACdCwWp0BviFACEoYyFaa3R + kc7Xdm9vFpclm9fzgUfPloASA0SkO945in3mIqMfODTb4yRvbjk8If9483fEPgQkczpd + ptBz1VAKg8AmRcz1GmBIxs+Stn0= dummy@gitlab.com KEY end end diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb new file mode 100644 index 00000000000..31fbbcf562c --- /dev/null +++ b/spec/features/groups/members/search_members_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe 'Search group member' do + let(:user) { create :user } + let(:member) { create :user } + + let!(:guest_group) do + create(:group) do |group| + group.add_guest(user) + group.add_guest(member) + end + end + + before do + sign_in(user) + visit group_group_members_path(guest_group) + end + + it 'renders member users' do + page.within '.member-search-form' do + fill_in 'search', with: member.name + find('.member-search-btn').click + end + + group_members_list = find(".panel .content-list") + expect(group_members_list).to have_content(member.name) + expect(group_members_list).not_to have_content(user.name) + end +end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index 1b41b3842c8..20337f1d3b0 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Group milestones', :js do +feature 'Group milestones' do let(:group) { create(:group) } let!(:project) { create(:project_empty_repo, group: group) } let(:user) { create(:group_member, :master, user: create(:user), group: group ).user } @@ -13,7 +13,7 @@ feature 'Group milestones', :js do sign_in(user) end - context 'create a milestone' do + context 'create a milestone', :js do before do visit new_group_milestone_path(group) end @@ -61,55 +61,132 @@ feature 'Group milestones', :js do end context 'milestones list' do - let!(:other_project) { create(:project_empty_repo, group: group) } - - let!(:active_project_milestone1) { create(:milestone, project: project, state: 'active', title: 'v1.0') } - let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.0') } - let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') } - let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') } - let!(:active_group_milestone) { create(:milestone, group: group, state: 'active') } - let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') } - - before do - visit group_milestones_path(group) + context 'when no milestones' do + it 'renders no milestones text' do + visit group_milestones_path(group) + expect(page).to have_content('No milestones to show') + end end - it 'counts milestones correctly' do - expect(find('.top-area .active .badge').text).to eq("2") - expect(find('.top-area .closed .badge').text).to eq("2") - expect(find('.top-area .all .badge').text).to eq("4") - end + context 'when milestones exists' do + let!(:other_project) { create(:project_empty_repo, group: group) } + + let!(:active_project_milestone1) do + create( + :milestone, + project: project, + state: 'active', + title: 'v1.0', + due_date: '2114-08-20', + description: 'Lorem Ipsum is simply dummy text' + ) + end + let!(:active_project_milestone2) { create(:milestone, project: other_project, state: 'active', title: 'v1.0') } + let!(:closed_project_milestone1) { create(:milestone, project: project, state: 'closed', title: 'v2.0') } + let!(:closed_project_milestone2) { create(:milestone, project: other_project, state: 'closed', title: 'v2.0') } + let!(:active_group_milestone) { create(:milestone, group: group, state: 'active', title: 'GL-113') } + let!(:closed_group_milestone) { create(:milestone, group: group, state: 'closed') } + let!(:issue) do + create :issue, project: project, assignees: [user], author: user, milestone: active_project_milestone1 + end - it 'lists legacy group milestones and group milestones' do - legacy_milestone = GroupMilestone.build_collection(group, group.projects, { state: 'active' }).first + before do + visit group_milestones_path(group) + end - expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1) - expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1) - end + it 'counts milestones correctly' do + expect(find('.top-area .active .badge').text).to eq("2") + expect(find('.top-area .closed .badge').text).to eq("2") + expect(find('.top-area .all .badge').text).to eq("4") + end - it 'updates milestone' do - page.within(".milestones #milestone_#{active_group_milestone.id}") do - click_link('Edit') + it 'lists legacy group milestones and group milestones' do + legacy_milestone = GroupMilestone.build_collection(group, group.projects, { state: 'active' }).first + + expect(page).to have_selector("#milestone_#{active_group_milestone.id}", count: 1) + expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1) end - page.within('.milestone-form') do - fill_in 'milestone_title', with: 'new title' - click_button('Update milestone') + it 'updates milestone' do + page.within(".milestones #milestone_#{active_group_milestone.id}") do + click_link('Edit') + end + + page.within('.milestone-form') do + fill_in 'milestone_title', with: 'new title' + click_button('Update milestone') + end + + expect(find('#content-body h2')).to have_content('new title') end - expect(find('#content-body h2')).to have_content('new title') - end + it 'shows milestone detail and supports its edit' do + page.within(".milestones #milestone_#{active_group_milestone.id}") do + click_link(active_group_milestone.title) + end + + page.within('.detail-page-header') do + click_link('Edit') + end - it 'shows milestone detail and supports its edit' do - page.within(".milestones #milestone_#{active_group_milestone.id}") do - click_link(active_group_milestone.title) + expect(page).to have_selector('.milestone-form') end - page.within('.detail-page-header') do - click_link('Edit') + it 'renders milestones' do + expect(page).to have_content('v1.0') + expect(page).to have_content('GL-113') + expect(page).to have_link( + '1 Issue', + href: issues_group_path(group, milestone_title: 'v1.0') + ) + expect(page).to have_link( + '0 Merge Requests', + href: merge_requests_group_path(group, milestone_title: 'v1.0') + ) end - expect(page).to have_selector('.milestone-form') + it 'renders group milestone details' do + click_link 'v1.0' + + expect(page).to have_content('expires on Aug 20, 2114') + expect(page).to have_content('v1.0') + expect(page).to have_content('Issues 1 Open: 1 Closed: 0') + expect(page).to have_link(issue.title, href: project_issue_path(issue.project, issue)) + end + + describe 'labels' do + before do + create(:label, project: project, title: 'bug') do |label| + issue.labels << label + end + + create(:label, project: project, title: 'feature') do |label| + issue.labels << label + end + end + + it 'renders labels' do + click_link 'v1.0' + + page.within('#tab-issues') do + expect(page).to have_content 'bug' + expect(page).to have_content 'feature' + end + end + + it 'renders labels list', :js do + click_link 'v1.0' + + page.within('.content .nav-links') do + page.find(:xpath, "//a[@href='#tab-labels']").click + end + + page.within('#tab-labels') do + expect(page).to have_content 'bug' + expect(page).to have_content 'feature' + end + end + end end end end diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index b02d2d4261c..cc12a1005ba 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -66,15 +66,16 @@ feature 'Milestone' do end end - feature 'Open a milestone' do + feature 'Open a milestone', :js do scenario 'shows total issue time spent correctly when no time has been logged' do milestone = create(:milestone, project: project, title: 8.7) visit project_milestone_path(project, milestone) - page.within('.block.time_spent') do - expect(page).to have_content 'No time spent' - expect(page).to have_content 'None' + wait_for_requests + + page.within('.time-tracking-no-tracking-pane') do + expect(page).to have_content 'No estimate or time spent' end end @@ -89,8 +90,10 @@ feature 'Milestone' do visit project_milestone_path(project, milestone) - page.within('.block.time_spent') do - expect(page).to have_content '3h' + wait_for_requests + + page.within('.time-tracking-spend-only-pane') do + expect(page).to have_content 'Spent: 3h' end end end diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb index 4665626f114..1d7700b6767 100644 --- a/spec/features/profiles/password_spec.rb +++ b/spec/features/profiles/password_spec.rb @@ -1,6 +1,15 @@ require 'spec_helper' describe 'Profile > Password' do + let(:user) { create(:user) } + + def fill_passwords(password, confirmation) + fill_in 'New password', with: password + fill_in 'Password confirmation', with: confirmation + + click_button 'Save password' + end + context 'Password authentication enabled' do let(:user) { create(:user, password_automatically_set: true) } @@ -9,13 +18,6 @@ describe 'Profile > Password' do visit edit_profile_password_path end - def fill_passwords(password, confirmation) - fill_in 'New password', with: password - fill_in 'Password confirmation', with: confirmation - - click_button 'Save password' - end - context 'User with password automatically set' do describe 'User puts different passwords in the field and in the confirmation' do it 'shows an error message' do @@ -73,4 +75,64 @@ describe 'Profile > Password' do end end end + + context 'Change passowrd' do + before do + sign_in(user) + visit(edit_profile_password_path) + end + + it 'does not change user passowrd without old one' do + page.within '.update-password' do + fill_passwords('22233344', '22233344') + end + + page.within '.flash-container' do + expect(page).to have_content 'You must provide a valid current password' + end + end + + it 'does not change password with invalid old password' do + page.within '.update-password' do + fill_in 'user_current_password', with: 'invalid' + fill_passwords('password', 'confirmation') + end + + page.within '.flash-container' do + expect(page).to have_content 'You must provide a valid current password' + end + end + + it 'changes user password' do + page.within '.update-password' do + fill_in "user_current_password", with: user.password + fill_passwords('22233344', '22233344') + end + + expect(current_path).to eq new_user_session_path + end + end + + context 'when password is expired' do + before do + sign_in(user) + + user.update_attributes(password_expires_at: 1.hour.ago) + user.identities.delete + expect(user.ldap_user?).to eq false + end + + it 'needs change user password' do + visit edit_profile_password_path + + expect(current_path).to eq new_profile_password_path + + fill_in :user_current_password, with: user.password + fill_in :user_password, with: '12345678' + fill_in :user_password_confirmation, with: '12345678' + click_button 'Set new password' + + expect(current_path).to eq new_user_session_path + end + end end diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb new file mode 100644 index 00000000000..0b5eacbe916 --- /dev/null +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe 'User edit profile' do + let(:user) { create(:user) } + + before do + sign_in(user) + visit(profile_path) + end + + it 'changes user profile' do + fill_in 'user_skype', with: 'testskype' + fill_in 'user_linkedin', with: 'testlinkedin' + fill_in 'user_twitter', with: 'testtwitter' + fill_in 'user_website_url', with: 'testurl' + fill_in 'user_location', with: 'Ukraine' + fill_in 'user_bio', with: 'I <3 GitLab' + fill_in 'user_organization', with: 'GitLab' + click_button 'Update profile settings' + + expect(user.reload).to have_attributes( + skype: 'testskype', + linkedin: 'testlinkedin', + twitter: 'testtwitter', + website_url: 'testurl', + bio: 'I <3 GitLab', + organization: 'GitLab' + ) + + expect(find('#user_location').value).to eq 'Ukraine' + expect(page).to have_content('Profile was successfully updated') + end + + context 'user avatar' do + before do + attach_file(:user_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif')) + click_button 'Update profile settings' + end + + it 'changes user avatar' do + expect(page).to have_link('Remove avatar') + + user.reload + expect(user.avatar).to be_instance_of AvatarUploader + expect(user.avatar.url).to eq "/uploads/-/system/user/avatar/#{user.id}/banana_sample.gif" + end + + it 'removes user avatar' do + click_link 'Remove avatar' + + user.reload + + expect(user.avatar?).to eq false + expect(page).not_to have_link('Remove avatar') + expect(page).to have_link('gravatar.com') + end + end +end diff --git a/spec/features/profiles/user_manages_applications_spec.rb b/spec/features/profiles/user_manages_applications_spec.rb new file mode 100644 index 00000000000..387584fef62 --- /dev/null +++ b/spec/features/profiles/user_manages_applications_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'User manages applications' do + let(:user) { create(:user) } + + before do + sign_in(user) + visit applications_profile_path + end + + it 'manages applications' do + expect(page).to have_content 'Add new application' + + fill_in :doorkeeper_application_name, with: 'test' + fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com' + click_on 'Save application' + + expect(page).to have_content 'Application: test' + expect(page).to have_content 'Application Id' + expect(page).to have_content 'Secret' + + click_on 'Edit' + + expect(page).to have_content 'Edit application' + fill_in :doorkeeper_application_name, with: 'test_changed' + click_on 'Save application' + + expect(page).to have_content 'test_changed' + expect(page).to have_content 'Application Id' + expect(page).to have_content 'Secret' + + visit applications_profile_path + + page.within '.oauth-applications' do + click_on 'Destroy' + end + expect(page.find('.oauth-applications')).not_to have_content 'test_changed' + end +end diff --git a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb index a50ebb29e01..0f419c3c2c0 100644 --- a/spec/features/profiles/user_visits_profile_authentication_log_spec.rb +++ b/spec/features/profiles/user_visits_profile_authentication_log_spec.rb @@ -3,13 +3,28 @@ require 'spec_helper' describe 'User visits the authentication log' do let(:user) { create(:user) } - before do - sign_in(user) + context 'when user signed in' do + before do + sign_in(user) + end - visit(audit_log_profile_path) + it 'shows correct menu item' do + visit(audit_log_profile_path) + + expect(page).to have_active_navigation('Authentication log') + end end - it 'shows correct menu item' do - expect(page).to have_active_navigation('Authentication log') + context 'when user has activity' do + before do + create(:closed_issue_event, author: user) + gitlab_sign_in(user) + end + + it 'shows user activity' do + visit(audit_log_profile_path) + + expect(page).to have_content 'Signed in with standard authentication' + end end end diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb index a5d80439143..713112477c8 100644 --- a/spec/features/profiles/user_visits_profile_spec.rb +++ b/spec/features/profiles/user_visits_profile_spec.rb @@ -5,20 +5,58 @@ describe 'User visits their profile' do before do sign_in(user) - - visit(profile_path) end it 'shows correct menu item' do + visit(profile_path) + expect(page).to have_active_navigation('Profile') end - describe 'profile settings', :js do - it 'saves updates' do - fill_in 'user_bio', with: 'bio' - click_button 'Update profile settings' + it 'shows profile info' do + visit(profile_path) + + expect(page).to have_content "This information will appear on your profile" + end + + context 'when user has groups' do + let(:group) do + create :group do |group| + group.add_owner(user) + end + end + + let!(:project) do + create(:project, :repository, namespace: group) do |project| + create(:closed_issue_event, project: project) + project.add_master(user) + end + end + + def click_on_profile_picture + find(:css, '.header-user-dropdown-toggle').click + + page.within ".header-user" do + click_link "Profile" + end + end + + it 'shows user groups', :js do + visit(profile_path) + click_on_profile_picture + + page.within ".cover-block" do + expect(page).to have_content user.name + expect(page).to have_content user.username + end + + page.within ".content" do + click_link "Groups" + end - expect(page).to have_content('Profile was successfully updated') + page.within "#groups" do + expect(page).to have_content group.name + end end end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 02dbd3380b3..4d47cdb500c 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -25,7 +25,7 @@ feature 'Gcp Cluster', :js do context 'when user has a GCP project with billing enabled' do before do allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('true') + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(true) end context 'when user does not have a cluster and visits cluster index page' do @@ -134,7 +134,7 @@ feature 'Gcp Cluster', :js do context 'when user does not have a GCP project with billing enabled' do before do allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) - allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('false') + allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(false) visit project_clusters_path(project) diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb index 4cf48098401..134c8b8bc39 100644 --- a/spec/features/projects/members/share_with_group_spec.rb +++ b/spec/features/projects/members/share_with_group_spec.rb @@ -149,6 +149,11 @@ feature 'Project > Members > Share with Group', :js do create(:group).add_owner(master) visit project_settings_members_path(project) + + click_link 'Share with group' + + find('.ajax-groups-select.select2-container') + execute_script 'GROUP_SELECT_PER_PAGE = 1;' open_select2 '#link_group_id' end diff --git a/spec/features/projects/network_graph_spec.rb b/spec/features/projects/network_graph_spec.rb new file mode 100644 index 00000000000..9f9a7787093 --- /dev/null +++ b/spec/features/projects/network_graph_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe 'Project Network Graph', :js do + let(:user) { create :user } + let(:project) { create :project, :repository, namespace: user.namespace } + + before do + sign_in(user) + + # Stub Graph max_size to speed up test (10 commits vs. 650) + allow(Network::Graph).to receive(:max_count).and_return(10) + end + + context 'when branch is master' do + def switch_ref_to(ref_name) + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link ref_name + end + end + + def click_show_only_selected_branch_checkbox + find('#filter_ref').click + end + + before do + visit project_network_path(project, 'master') + end + + it 'renders project network' do + expect(page).to have_selector ".network-graph" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" + page.within '.network-graph' do + expect(page).to have_content 'master' + end + end + + it 'switches ref to branch' do + switch_ref_to('feature') + + expect(page).to have_selector '.dropdown-menu-toggle', text: 'feature' + page.within '.network-graph' do + expect(page).to have_content 'feature' + end + end + + it 'switches ref to tag' do + switch_ref_to('v1.0.0') + + expect(page).to have_selector '.dropdown-menu-toggle', text: 'v1.0.0' + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' + end + end + + it 'renders by commit sha of "v1.0.0"' do + page.within ".network-form" do + fill_in 'extended_sha1', with: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' + find('button').click + end + + expect(page).to have_selector ".network-graph" + expect(page).to have_selector '.dropdown-menu-toggle', text: "master" + page.within '.network-graph' do + expect(page).to have_content 'v1.0.0' + end + end + + it 'filters select tag' do + switch_ref_to('v1.0.0') + + expect(page).to have_css 'title', text: 'Graph · v1.0.0', visible: false + page.within '.network-graph' do + expect(page).to have_content 'Change some files' + end + + click_show_only_selected_branch_checkbox + + page.within '.network-graph' do + expect(page).not_to have_content 'Change some files' + end + + click_show_only_selected_branch_checkbox + + page.within '.network-graph' do + expect(page).to have_content 'Change some files' + end + end + + it 'renders error message when sha commit not exists' do + page.within ".network-form" do + fill_in 'extended_sha1', with: ';' + find('button').click + end + + expect(page).to have_selector '.flash-alert', text: "Git revision ';' does not exist." + end + end + + it 'renders project network with test branch' do + visit project_network_path(project, "'test'") + + page.within '.network-graph' do + expect(page).to have_content "'test'" + end + end +end diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 2e334caa98f..3f1ef0b2a47 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -17,6 +17,7 @@ feature 'Pages' do scenario 'does not see anything to destroy' do visit project_pages_path(project) + expect(page).to have_content('Configure pages') expect(page).not_to have_link('Remove pages') expect(page).not_to have_text('Only the project owner can remove pages') end @@ -32,14 +33,163 @@ feature 'Pages' do allow_any_instance_of(Project).to receive(:pages_deployed?) { true } end - scenario 'sees "Remove pages" link' do + scenario 'renders Access pages' do visit project_pages_path(project) - expect(page).to have_link('Remove pages') + expect(page).to have_content('Access pages') + end + + context 'when support for external domains is disabled' do + before do + allow(Gitlab.config.pages).to receive(:external_http).and_return(nil) + allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) + end + + it 'renders message that support is disabled' do + visit project_pages_path(project) + + expect(page).to have_content('Support for domains and certificates is disabled') + end + end + + context 'when pages are exposed on external HTTP address' do + shared_examples 'adds new domain' do + it 'adds new domain' do + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + click_button 'Create New Domain' + + expect(page).to have_content('Domains (1)') + expect(page).to have_content('my.test.domain.com') + end + end + + before do + allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80']) + allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) + end + + it 'allows to add new domain' do + visit project_pages_path(project) + + expect(page).to have_content('New Domain') + end + + it_behaves_like 'adds new domain' + + context 'when project in group namespace' do + it_behaves_like 'adds new domain' do + let(:group) { create :group } + let(:project) { create :project, namespace: group } + end + end + + context 'when pages domain is added' do + before do + project.pages_domains.create!(domain: 'my.test.domain.com') + + visit new_project_pages_domain_path(project) + end + + it 'renders certificates is disabled' do + expect(page).to have_content('Support for custom certificates is disabled') + end + + it 'does not adds new domain and renders error message' do + fill_in 'Domain', with: 'my.test.domain.com' + click_button 'Create New Domain' + + expect(page).to have_content('Domain has already been taken') + end + end + end + + context 'when pages are exposed on external HTTPS address' do + let(:certificate_pem) do + <<~PEM + -----BEGIN CERTIFICATE----- + MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0 + LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ + MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw + gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa + SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT + nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w + DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD + VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh + IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ + joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese + 5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg + YHi2yesCrOvVXt+lgPTd + -----END CERTIFICATE----- + PEM + end + + let(:certificate_key) do + <<~KEY + -----BEGIN PRIVATE KEY----- + MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN + SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t + PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB + kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd + j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/ + uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR + 5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O + AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K + EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh + Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C + m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH + EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx + 63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi + nNp/xedE1YxutQ== + -----END PRIVATE KEY----- + KEY + end + + before do + allow(Gitlab.config.pages).to receive(:external_http).and_return(['1.1.1.1:80']) + allow(Gitlab.config.pages).to receive(:external_https).and_return(['1.1.1.1:443']) + end + + it 'adds new domain with certificate' do + visit new_project_pages_domain_path(project) + + fill_in 'Domain', with: 'my.test.domain.com' + fill_in 'Certificate (PEM)', with: certificate_pem + fill_in 'Key (PEM)', with: certificate_key + click_button 'Create New Domain' + + expect(page).to have_content('Domains (1)') + expect(page).to have_content('my.test.domain.com') + end end end it_behaves_like 'no pages deployed' + + describe 'project settings page' do + it 'renders "Pages" tab' do + visit edit_project_path(project) + + page.within '.nav-sidebar' do + expect(page).to have_link('Pages') + end + end + + context 'when pages are disabled' do + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(false) + end + + it 'does not render "Pages" tab' do + visit edit_project_path(project) + + page.within '.nav-sidebar' do + expect(page).not_to have_link('Pages') + end + end + end + end end context 'when the user is not the owner' do @@ -57,4 +207,54 @@ feature 'Pages' do it_behaves_like 'no pages deployed' end + + describe 'Remove page' do + context 'when user is the owner' do + let(:project) { create :project, :repository } + + before do + project.namespace.update(owner: user) + end + + context 'when pages are deployed' do + let(:pipeline) do + commit_sha = project.commit('HEAD').sha + + project.pipelines.create( + ref: 'HEAD', + sha: commit_sha, + source: :push, + protected: false + ) + end + + let(:ci_build) do + build( + :ci_build, + project: project, + pipeline: pipeline, + ref: 'HEAD', + legacy_artifacts_file: fixture_file_upload(Rails.root.join('spec/fixtures/pages.zip')), + legacy_artifacts_metadata: fixture_file_upload(Rails.root.join('spec/fixtures/pages.zip.meta')) + ) + end + + before do + result = Projects::UpdatePagesService.new(project, ci_build).execute + expect(result[:status]).to eq(:success) + expect(project).to be_pages_deployed + end + + it 'removes the pages' do + visit project_pages_path(project) + + expect(page).to have_link('Remove pages') + + click_link 'Remove pages' + + expect(project.pages_deployed?).to be_falsey + end + end + end + end end diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb new file mode 100644 index 00000000000..e9502178bd7 --- /dev/null +++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb @@ -0,0 +1,92 @@ +require 'spec_helper' + +describe 'User activates issue tracker', :js do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:url) { 'http://tracker.example.com' } + + def fill_form(active = true) + check 'Active' if active + + fill_in 'service_project_url', with: url + fill_in 'service_issues_url', with: "#{url}/:id" + fill_in 'service_new_issue_url', with: url + end + + before do + project.add_master(user) + sign_in(user) + + visit project_settings_integrations_path(project) + end + + shared_examples 'external issue tracker activation' do |tracker:| + describe 'user sets and activates the Service' do + context 'when the connection test succeeds' do + before do + stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' }) + + click_link(tracker) + fill_form + click_button('Test settings and save changes') + wait_for_requests + end + + it 'activates the service' do + expect(page).to have_content("#{tracker} activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'shows the link in the menu' do + page.within('.nav-sidebar') do + expect(page).to have_link(tracker, href: url) + end + end + end + + context 'when the connection test fails' do + it 'activates the service' do + stub_request(:head, url).to_raise(HTTParty::Error) + + click_link(tracker) + fill_form + click_button('Test settings and save changes') + wait_for_requests + + expect(find('.flash-container-page')).to have_content 'Test failed.' + expect(find('.flash-container-page')).to have_content 'Save anyway' + + find('.flash-alert .flash-action').click + wait_for_requests + + expect(page).to have_content("#{tracker} activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + end + end + + describe 'user sets the service but keeps it disabled' do + before do + click_link(tracker) + fill_form(false) + click_button('Save changes') + end + + it 'saves but does not activate the service' do + expect(page).to have_content("#{tracker} settings saved, but not activated.") + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'does not show the external tracker link in the menu' do + page.within('.nav-sidebar') do + expect(page).not_to have_link(tracker, href: url) + end + end + end + end + + it_behaves_like 'external issue tracker activation', tracker: 'Redmine' + it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla' + it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker' +end diff --git a/spec/features/projects/services/user_activates_jira_spec.rb b/spec/features/projects/services/user_activates_jira_spec.rb index 028669eeaf2..429128ec096 100644 --- a/spec/features/projects/services/user_activates_jira_spec.rb +++ b/spec/features/projects/services/user_activates_jira_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' describe 'User activates Jira', :js do let(:user) { create(:user) } let(:project) { create(:project) } - let(:service) { project.create_jira_service } let(:url) { 'http://jira.example.com' } let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' } @@ -26,7 +25,7 @@ describe 'User activates Jira', :js do describe 'user sets and activates Jira Service' do context 'when Jira connection test succeeds' do - it 'activates the JIRA service' do + before do server_info = { key: 'value' }.to_json WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info) @@ -34,10 +33,18 @@ describe 'User activates Jira', :js do fill_form click_button('Test settings and save changes') wait_for_requests + end + it 'activates the JIRA service' do expect(page).to have_content('JIRA activated.') expect(current_path).to eq(project_settings_integrations_path(project)) end + + it 'shows the JIRA link in the menu' do + page.within('.nav-sidebar') do + expect(page).to have_link('JIRA', href: url) + end + end end context 'when Jira connection test fails' do @@ -75,14 +82,20 @@ describe 'User activates Jira', :js do end describe 'user sets Jira Service but keeps it disabled' do - context 'when Jira connection test succeeds' do - it 'activates the JIRA service' do - click_link('JIRA') - fill_form(false) - click_button('Save changes') + before do + click_link('JIRA') + fill_form(false) + click_button('Save changes') + end - expect(page).to have_content('JIRA settings saved, but not activated.') - expect(current_path).to eq(project_settings_integrations_path(project)) + it 'saves but does not activate the JIRA service' do + expect(page).to have_content('JIRA settings saved, but not activated.') + expect(current_path).to eq(project_settings_integrations_path(project)) + end + + it 'does not show the JIRA link in the menu' do + page.within('.nav-sidebar') do + expect(page).not_to have_link('JIRA', href: url) end end end diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb deleted file mode 100644 index 917fad74ef1..00000000000 --- a/spec/features/signup_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'spec_helper' - -feature 'Signup' do - describe 'signup with no errors' do - context "when sending confirmation email" do - before do - stub_application_setting(send_user_confirmation_email: true) - end - - it 'creates the user account and sends a confirmation email' do - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_email_confirmation', with: user.email - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq users_almost_there_path - expect(page).to have_content("Please check your email to confirm your account") - end - end - - context "when sigining up with different cased emails" do - it "creates the user successfully" do - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_email_confirmation', with: user.email.capitalize - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq dashboard_projects_path - expect(page).to have_content("Welcome! You have signed up successfully.") - end - end - - context "when not sending confirmation email" do - before do - stub_application_setting(send_user_confirmation_email: false) - end - - it 'creates the user account and goes to dashboard' do - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_email_confirmation', with: user.email - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq dashboard_projects_path - expect(page).to have_content("Welcome! You have signed up successfully.") - end - end - end - - describe 'signup with errors' do - it "displays the errors" do - existing_user = create(:user) - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: existing_user.email - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq user_registration_path - expect(page).to have_content("errors prohibited this user from being saved") - expect(page).to have_content("Email has already been taken") - expect(page).to have_content("Email confirmation doesn't match") - end - - it 'does not redisplay the password' do - existing_user = create(:user) - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: existing_user.email - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq user_registration_path - expect(page.body).not_to match(/#{user.password}/) - end - end -end diff --git a/spec/features/login_spec.rb b/spec/features/users/login_spec.rb index 6dfabcc7225..6ef235cf870 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -1,6 +1,26 @@ require 'spec_helper' feature 'Login' do + scenario 'Successful user signin invalidates password reset token' do + user = create(:user) + + expect(user.reset_password_token).to be_nil + + visit new_user_password_path + fill_in 'user_email', with: user.email + click_button 'Reset password' + + user.reload + expect(user.reset_password_token).not_to be_nil + + find('a[href="#login-pane"]').click + gitlab_sign_in(user) + expect(current_path).to eq root_path + + user.reload + expect(user.reset_password_token).to be_nil + end + describe 'initial login after setup' do it 'allows the initial admin to create a password' do # This behavior is dependent on there only being one user diff --git a/spec/features/logout_spec.rb b/spec/features/users/logout_spec.rb index 635729efa53..635729efa53 100644 --- a/spec/features/logout_spec.rb +++ b/spec/features/users/logout_spec.rb diff --git a/spec/features/users/projects_spec.rb b/spec/features/users/projects_spec.rb deleted file mode 100644 index f079771cee1..00000000000 --- a/spec/features/users/projects_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -describe 'Projects tab on a user profile', :js do - let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace) } - let!(:project2) { create(:project, namespace: user.namespace) } - - before do - allow(Project).to receive(:default_per_page).and_return(1) - - sign_in(user) - - visit user_path(user) - - page.within('.user-profile-nav') do - click_link('Personal projects') - end - - wait_for_requests - end - - it 'paginates results' do - expect(page).to have_content(project2.name) - - click_link('Next') - - expect(page).to have_content(project.name) - end -end diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb new file mode 100644 index 00000000000..5d539f0ccbe --- /dev/null +++ b/spec/features/users/signup_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe 'Signup' do + let(:new_user) { build_stubbed(:user) } + + describe 'username validation', :js do + before do + visit root_path + click_link 'Register' + end + + it 'does not show an error border if the username is available' do + fill_in 'new_user_username', with: 'new-user' + wait_for_requests + + expect(find('.username')).not_to have_css '.gl-field-error-outline' + end + + it 'does not show an error border if the username contains dots (.)' do + fill_in 'new_user_username', with: 'new.user.username' + wait_for_requests + + expect(find('.username')).not_to have_css '.gl-field-error-outline' + end + + it 'shows an error border if the username already exists' do + existing_user = create(:user) + + fill_in 'new_user_username', with: existing_user.username + wait_for_requests + + expect(find('.username')).to have_css '.gl-field-error-outline' + end + + it 'shows an error border if the username contains special characters' do + fill_in 'new_user_username', with: 'new$user!username' + wait_for_requests + + expect(find('.username')).to have_css '.gl-field-error-outline' + end + end + + context 'with no errors' do + context "when sending confirmation email" do + before do + stub_application_setting(send_user_confirmation_email: true) + end + + it 'creates the user account and sends a confirmation email' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + + expect { click_button 'Register' }.to change { User.count }.by(1) + + expect(current_path).to eq users_almost_there_path + expect(page).to have_content("Please check your email to confirm your account") + end + end + + context "when sigining up with different cased emails" do + it "creates the user successfully" do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email.capitalize + fill_in 'new_user_password', with: new_user.password + click_button "Register" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + + context "when not sending confirmation email" do + before do + stub_application_setting(send_user_confirmation_email: false) + end + + it 'creates the user account and goes to dashboard' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + click_button "Register" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + end + + context 'with errors' do + it "displays the errors" do + existing_user = create(:user) + + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: existing_user.email + fill_in 'new_user_password', with: new_user.password + click_button "Register" + + expect(current_path).to eq user_registration_path + expect(page).to have_content("errors prohibited this user from being saved") + expect(page).to have_content("Email has already been taken") + expect(page).to have_content("Email confirmation doesn't match") + end + + it 'does not redisplay the password' do + existing_user = create(:user) + + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: existing_user.email + fill_in 'new_user_password', with: new_user.password + click_button "Register" + + expect(current_path).to eq user_registration_path + expect(page.body).not_to match(/#{new_user.password}/) + end + end +end diff --git a/spec/features/user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb index 19c587e53c8..a70637c8370 100644 --- a/spec/features/user_page_spec.rb +++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'User page', :js do +describe 'Users > User browses projects on user page', :js do let!(:user) { create :user } let!(:private_project) do create :project, :private, name: 'private', namespace: user.namespace do |project| @@ -26,6 +26,28 @@ describe 'User page', :js do end end + it 'paginates projects', :js do + project = create(:project, namespace: user.namespace) + project2 = create(:project, namespace: user.namespace) + allow(Project).to receive(:default_per_page).and_return(1) + + sign_in(user) + + visit user_path(user) + + page.within('.user-profile-nav') do + click_link('Personal projects') + end + + wait_for_requests + + expect(page).to have_content(project2.name) + + click_link('Next') + + expect(page).to have_content(project.name) + end + context 'when not signed in' do it 'renders user public project' do visit user_path(user) diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb deleted file mode 100644 index a9973cdf214..00000000000 --- a/spec/features/users_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'spec_helper' - -feature 'Users', :js do - let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } - - scenario 'GET /users/sign_in creates a new user account' do - visit new_user_session_path - click_link 'Register' - fill_in 'new_user_name', with: 'Name Surname' - fill_in 'new_user_username', with: 'Great' - fill_in 'new_user_email', with: 'name@mail.com' - fill_in 'new_user_email_confirmation', with: 'name@mail.com' - fill_in 'new_user_password', with: 'password1234' - expect { click_button 'Register' }.to change { User.count }.by(1) - end - - scenario 'Successful user signin invalidates password reset token' do - expect(user.reset_password_token).to be_nil - - visit new_user_password_path - fill_in 'user_email', with: user.email - click_button 'Reset password' - - user.reload - expect(user.reset_password_token).not_to be_nil - - find('a[href="#login-pane"]').click - gitlab_sign_in(user) - expect(current_path).to eq root_path - - user.reload - expect(user.reset_password_token).to be_nil - end - - scenario 'Should show one error if email is already taken' do - visit new_user_session_path - click_link 'Register' - fill_in 'new_user_name', with: 'Another user name' - fill_in 'new_user_username', with: 'anotheruser' - fill_in 'new_user_email', with: user.email - fill_in 'new_user_email_confirmation', with: user.email - fill_in 'new_user_password', with: '12341234' - expect { click_button 'Register' }.to change { User.count }.by(0) - expect(page).to have_text('Email has already been taken') - expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}' - end - - describe 'redirect alias routes' do - before do - expect(user).to be_persisted - end - - scenario '/u/user1 redirects to user page' do - visit '/u/user1' - - expect(current_path).to eq user_path(user) - expect(page).to have_text(user.name) - end - - scenario '/u/user1/groups redirects to user groups page' do - visit '/u/user1/groups' - - expect(current_path).to eq user_groups_path(user) - end - - scenario '/u/user1/projects redirects to user projects page' do - visit '/u/user1/projects' - - expect(current_path).to eq user_projects_path(user) - end - end - - feature 'username validation' do - let(:loading_icon) { '.fa.fa-spinner' } - let(:username_input) { 'new_user_username' } - - before do - visit new_user_session_path - click_link 'Register' - end - - scenario 'doesn\'t show an error border if the username is available' do - fill_in username_input, with: 'new-user' - wait_for_requests - expect(find('.username')).not_to have_css '.gl-field-error-outline' - end - - scenario 'does not show an error border if the username contains dots (.)' do - fill_in username_input, with: 'new.user.username' - wait_for_requests - expect(find('.username')).not_to have_css '.gl-field-error-outline' - end - - scenario 'shows an error border if the username already exists' do - fill_in username_input, with: user.username - wait_for_requests - expect(find('.username')).to have_css '.gl-field-error-outline' - end - - scenario 'shows an error border if the username contains special characters' do - fill_in username_input, with: 'new$user!username' - wait_for_requests - expect(find('.username')).to have_css '.gl-field-error-outline' - end - end - - def errors_on_page(page) - page.find('#error_explanation').find('ul').all('li').map { |item| item.text }.join("\n") - end - - def number_of_errors_on_page(page) - page.find('#error_explanation').find('ul').all('li').count - end -end diff --git a/spec/fixtures/api/schemas/public_api/v4/blobs.json b/spec/fixtures/api/schemas/public_api/v4/blobs.json index 9cb1eae3762..a812815838f 100644 --- a/spec/fixtures/api/schemas/public_api/v4/blobs.json +++ b/spec/fixtures/api/schemas/public_api/v4/blobs.json @@ -7,11 +7,12 @@ "data": { "type": "string" }, "filename": { "type": ["string"] }, "id": { "type": ["string", "null"] }, + "project_id": { "type": "integer" }, "ref": { "type": "string" }, "startline": { "type": "integer" } }, "required": [ - "basename", "data", "filename", "id", "ref", "startline" + "basename", "data", "filename", "id", "ref", "startline", "project_id" ], "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/commit/detail.json b/spec/fixtures/api/schemas/public_api/v4/commit/detail.json index 88a3cad62f6..477e776a804 100644 --- a/spec/fixtures/api/schemas/public_api/v4/commit/detail.json +++ b/spec/fixtures/api/schemas/public_api/v4/commit/detail.json @@ -4,9 +4,9 @@ { "$ref": "basic.json" }, { "required" : [ - "stats", "status", - "last_pipeline" + "last_pipeline", + "project_id" ], "properties": { "stats": { "$ref": "../commit_stats.json" }, @@ -16,7 +16,8 @@ { "type": "null" }, { "$ref": "../pipeline/basic.json" } ] - } + }, + "project_id": { "type": "integer" } } } ] diff --git a/spec/fixtures/api/schemas/public_api/v4/commits_details.json b/spec/fixtures/api/schemas/public_api/v4/commits_details.json new file mode 100644 index 00000000000..1f5b1ad86ef --- /dev/null +++ b/spec/fixtures/api/schemas/public_api/v4/commits_details.json @@ -0,0 +1,4 @@ +{ + "type": "array", + "items": { "$ref": "commit/detail.json" } +} diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json index 78977118b0a..6f6b044115b 100644 --- a/spec/fixtures/api/schemas/variable.json +++ b/spec/fixtures/api/schemas/variable.json @@ -10,7 +10,8 @@ "id": { "type": "integer" }, "key": { "type": "string" }, "value": { "type": "string" }, - "protected": { "type": "boolean" } + "protected": { "type": "boolean" }, + "environment_scope": { "type": "string", "optional": true } }, "additionalProperties": false } diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index f7a4a7afced..43cb0dfe163 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -63,13 +63,29 @@ describe ApplicationHelper do end end - describe 'avatar_icon' do + describe 'avatar_icon_for' do + let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') } + let(:email) { 'foo@example.com' } + let!(:another_user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: email) } + + it 'prefers the user to retrieve the avatar_url' do + expect(helper.avatar_icon_for(user, email).to_s) + .to eq(user.avatar.url) + end + + it 'falls back to email lookup if no user given' do + expect(helper.avatar_icon_for(nil, email).to_s) + .to eq(another_user.avatar.url) + end + end + + describe 'avatar_icon_for_email' do let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) } context 'using an email' do context 'when there is a matching user' do it 'returns a relative URL for the avatar' do - expect(helper.avatar_icon(user.email).to_s) + expect(helper.avatar_icon_for_email(user.email).to_s) .to eq(user.avatar.url) end end @@ -78,17 +94,37 @@ describe ApplicationHelper do it 'calls gravatar_icon' do expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2) - helper.avatar_icon('foo@example.com', 20, 2) + helper.avatar_icon_for_email('foo@example.com', 20, 2) + end + end + + context 'without an email passed' do + it 'calls gravatar_icon' do + expect(helper).to receive(:gravatar_icon).with(nil, 20, 2) + + helper.avatar_icon_for_email(nil, 20, 2) end end end + end + + describe 'avatar_icon_for_user' do + let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) } - describe 'using a user' do + context 'with a user object passed' do it 'returns a relative URL for the avatar' do - expect(helper.avatar_icon(user).to_s) + expect(helper.avatar_icon_for_user(user).to_s) .to eq(user.avatar.url) end end + + context 'without a user object passed' do + it 'calls gravatar_icon' do + expect(helper).to receive(:gravatar_icon).with(nil, 20, 2) + + helper.avatar_icon_for_user(nil, 20, 2) + end + end end describe 'gravatar_icon' do diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index f44e7ef6843..04c6d259135 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -29,7 +29,7 @@ describe AvatarsHelper do is_expected.to eq tag( :img, alt: "#{user.name}'s avatar", - src: avatar_icon(user, 16), + src: avatar_icon_for_user(user, 16), data: { container: 'body' }, class: 'avatar s16 has-tooltip', title: user.name @@ -43,7 +43,7 @@ describe AvatarsHelper do is_expected.to eq tag( :img, alt: "#{user.name}'s avatar", - src: avatar_icon(user, 16), + src: avatar_icon_for_user(user, 16), data: { container: 'body' }, class: "avatar s16 #{options[:css_class]} has-tooltip", title: user.name @@ -58,7 +58,7 @@ describe AvatarsHelper do is_expected.to eq tag( :img, alt: "#{user.name}'s avatar", - src: avatar_icon(user, options[:size]), + src: avatar_icon_for_user(user, options[:size]), data: { container: 'body' }, class: "avatar s#{options[:size]} has-tooltip", title: user.name @@ -89,7 +89,7 @@ describe AvatarsHelper do :img, alt: "#{user.name}'s avatar", src: LazyImageTagHelper.placeholder_image, - data: { container: 'body', src: avatar_icon(user, 16) }, + data: { container: 'body', src: avatar_icon_for_user(user, 16) }, class: "avatar s16 has-tooltip lazy", title: user.name ) @@ -104,7 +104,7 @@ describe AvatarsHelper do is_expected.to eq tag( :img, alt: "#{user.name}'s avatar", - src: avatar_icon(user, 16), + src: avatar_icon_for_user(user, 16), data: { container: 'body' }, class: "avatar s16 has-tooltip", title: user.name @@ -119,7 +119,7 @@ describe AvatarsHelper do is_expected.to eq tag( :img, alt: "#{user.name}'s avatar", - src: avatar_icon(user, 16), + src: avatar_icon_for_user(user, 16), class: "avatar s16", title: user.name ) @@ -137,7 +137,7 @@ describe AvatarsHelper do is_expected.to eq tag( :img, alt: "#{user.name}'s avatar", - src: avatar_icon(user, 16), + src: avatar_icon_for_user(user, 16), data: { container: 'body' }, class: "avatar s16 has-tooltip", title: user.name @@ -149,7 +149,7 @@ describe AvatarsHelper do is_expected.to eq tag( :img, alt: "#{options[:user_name]}'s avatar", - src: avatar_icon(options[:user_email], 16), + src: avatar_icon_for_email(options[:user_email], 16), data: { container: 'body' }, class: "avatar s16 has-tooltip", title: options[:user_name] diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index 8a80b88da5d..fccde8b7eba 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -20,5 +20,9 @@ describe EventsHelper do it 'handles nil values' do expect(helper.event_commit_title(nil)).to eq('') end + + it 'does not escape HTML entities' do + expect(helper.event_commit_title("foo & bar")).to eq("foo & bar") + end end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index ffe4266b51a..a160cc9d5ec 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -215,7 +215,7 @@ describe ProjectsHelper do let(:expected) { double } before do - expect(helper).to receive(:avatar_icon).with(user, 16).and_return(expected) + expect(helper).to receive(:avatar_icon_for_user).with(user, 16).and_return(expected) end it 'returns image tag for member avatar' do diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js index c93b7cc6cac..95c2c122403 100644 --- a/spec/javascripts/ajax_loading_spinner_spec.js +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -1,5 +1,3 @@ -import 'jquery'; -import 'jquery-ujs'; import AjaxLoadingSpinner from '~/ajax_loading_spinner'; describe('Ajax Loading Spinner', () => { diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 268b5b83b73..8e4bbb90ccb 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -79,7 +79,7 @@ import '~/lib/utils/common_utils'; return expect($emojiMenu.length).toBe(1); }); }); - return it('should remove emoji menu when body is clicked', function(done) { + it('should remove emoji menu when body is clicked', function(done) { $('.js-add-award').eq(0).click(); return lazyAssert(done, function() { var $emojiMenu; @@ -90,6 +90,17 @@ import '~/lib/utils/common_utils'; return expect($('.js-awards-block.current').length).toBe(0); }); }); + it('should not remove emoji menu when search is clicked', function(done) { + $('.js-add-award').eq(0).click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + $('.emoji-search').click(); + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(true); + return expect($('.js-awards-block.current').length).toBe(1); + }); + }); }); describe('::addAwardToEmojiBar', function() { it('should add emoji to votes block', function() { @@ -127,7 +138,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent(); $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.userAuthored($thumbsUpEmoji); - return expect($thumbsUpEmoji.data("original-title")).toBe("You cannot vote on your own issue, MR and note"); + return expect($thumbsUpEmoji.data("originalTitle")).toBe("You cannot vote on your own issue, MR and note"); }); it('should restore tooltip back to initial vote list', function() { var $thumbsUpEmoji, $votesBlock; @@ -138,7 +149,7 @@ import '~/lib/utils/common_utils'; awardsHandler.userAuthored($thumbsUpEmoji); jasmine.clock().tick(2801); jasmine.clock().uninstall(); - return expect($thumbsUpEmoji.data("original-title")).toBe("sam"); + return expect($thumbsUpEmoji.data("originalTitle")).toBe("sam"); }); }); describe('::getAwardUrl', function() { @@ -183,7 +194,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("original-title")).toBe('You, sam, jerry, max, and andy'); + return expect($thumbsUpEmoji.data("originalTitle")).toBe('You, sam, jerry, max, and andy'); }); return it('handles the special case where "You" is not cleanly comma seperated', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -193,7 +204,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.attr('data-title', 'sam'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("original-title")).toBe('You and sam'); + return expect($thumbsUpEmoji.data("originalTitle")).toBe('You and sam'); }); }); describe('::removeYouToUserList', function() { @@ -206,7 +217,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("original-title")).toBe('sam, jerry, max, and andy'); + return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam, jerry, max, and andy'); }); return it('handles the special case where "You" is not cleanly comma seperated', function() { var $thumbsUpEmoji, $votesBlock, awardUrl; @@ -217,7 +228,7 @@ import '~/lib/utils/common_utils'; $thumbsUpEmoji.addClass('active'); awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); $thumbsUpEmoji.tooltip(); - return expect($thumbsUpEmoji.data("original-title")).toBe('sam'); + return expect($thumbsUpEmoji.data("originalTitle")).toBe('sam'); }); }); describe('::searchEmojis', () => { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 8287c58ac5a..e500bbe750f 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -15,7 +15,7 @@ describe('requiresInput', () => { }); it('enables submit when no field is required', () => { - $('*[required=required]').removeAttr('required'); + $('*[required=required]').prop('required', false); $('.js-requires-input').requiresInput(); expect(submitButton).not.toBeDisabled(); }); diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js index 5b9cdceee71..ee457a9c48c 100644 --- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js @@ -1,8 +1,10 @@ +import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import AjaxFormVariableList from '~/ci_variable_list/ajax_variable_list'; const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-project/variables'; +const HIDE_CLASS = 'hide'; describe('AjaxFormVariableList', () => { preloadFixtures('projects/ci_cd_settings.html.raw'); @@ -45,16 +47,16 @@ describe('AjaxFormVariableList', () => { const loadingIcon = saveButton.querySelector('.js-secret-variables-save-loading-icon'); mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(() => { - expect(loadingIcon.classList.contains('hide')).toEqual(false); + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(false); return [200, {}]; }); - expect(loadingIcon.classList.contains('hide')).toEqual(true); + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true); ajaxVariableList.onSaveClicked() .then(() => { - expect(loadingIcon.classList.contains('hide')).toEqual(true); + expect(loadingIcon.classList.contains(HIDE_CLASS)).toEqual(true); }) .then(done) .catch(done.fail); @@ -78,11 +80,11 @@ describe('AjaxFormVariableList', () => { it('hides any previous error box', (done) => { mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200); - expect(errorBox.classList.contains('hide')).toEqual(true); + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); ajaxVariableList.onSaveClicked() .then(() => { - expect(errorBox.classList.contains('hide')).toEqual(true); + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); }) .then(done) .catch(done.fail); @@ -103,17 +105,39 @@ describe('AjaxFormVariableList', () => { .catch(done.fail); }); + it('hides secret values', (done) => { + mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {}); + + const row = container.querySelector('.js-row:first-child'); + const valueInput = row.querySelector('.js-ci-variable-input-value'); + const valuePlaceholder = row.querySelector('.js-secret-value-placeholder'); + + valueInput.value = 'bar'; + $(valueInput).trigger('input'); + + expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(true); + expect(valueInput.classList.contains(HIDE_CLASS)).toBe(false); + + ajaxVariableList.onSaveClicked() + .then(() => { + expect(valuePlaceholder.classList.contains(HIDE_CLASS)).toBe(false); + expect(valueInput.classList.contains(HIDE_CLASS)).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + it('shows error box with validation errors', (done) => { const validationError = 'some validation error'; mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(400, [ validationError, ]); - expect(errorBox.classList.contains('hide')).toEqual(true); + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); ajaxVariableList.onSaveClicked() .then(() => { - expect(errorBox.classList.contains('hide')).toEqual(false); + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(false); expect(errorBox.textContent.trim().replace(/\n+\s+/m, ' ')).toEqual(`Validation failed ${validationError}`); }) .then(done) @@ -123,11 +147,11 @@ describe('AjaxFormVariableList', () => { it('shows flash message when request fails', (done) => { mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(500); - expect(errorBox.classList.contains('hide')).toEqual(true); + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); ajaxVariableList.onSaveClicked() .then(() => { - expect(errorBox.classList.contains('hide')).toEqual(true); + expect(errorBox.classList.contains(HIDE_CLASS)).toEqual(true); }) .then(done) .catch(done.fail); @@ -170,9 +194,9 @@ describe('AjaxFormVariableList', () => { const valueInput = row.querySelector('.js-ci-variable-input-value'); keyInput.value = 'foo'; - keyInput.dispatchEvent(new Event('input')); + $(keyInput).trigger('input'); valueInput.value = 'bar'; - valueInput.dispatchEvent(new Event('input')); + $(valueInput).trigger('input'); expect(idInput.value).toEqual(''); diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js index 8acb346901f..cac785fd3c6 100644 --- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -1,6 +1,8 @@ import VariableList from '~/ci_variable_list/ci_variable_list'; import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; +const HIDE_CLASS = 'hide'; + describe('VariableList', () => { preloadFixtures('pipeline_schedules/edit.html.raw'); preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); @@ -92,14 +94,14 @@ describe('VariableList', () => { const $inputValue = $row.find('.js-ci-variable-input-value'); const $placeholder = $row.find('.js-secret-value-placeholder'); - expect($placeholder.hasClass('hide')).toBe(false); - expect($inputValue.hasClass('hide')).toBe(true); + expect($placeholder.hasClass(HIDE_CLASS)).toBe(false); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(true); // Reveal values $wrapper.find('.js-secret-value-reveal-button').click(); - expect($placeholder.hasClass('hide')).toBe(true); - expect($inputValue.hasClass('hide')).toBe(false); + expect($placeholder.hasClass(HIDE_CLASS)).toBe(true); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(false); }); }); }); @@ -179,4 +181,35 @@ describe('VariableList', () => { expect($wrapper.find('.js-ci-variable-input-key:not([disabled])').length).toBe(3); }); }); + + describe('hideValues', () => { + beforeEach(() => { + loadFixtures('projects/ci_cd_settings.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should hide value input and show placeholder stars', () => { + const $row = $wrapper.find('.js-row'); + const $inputValue = $row.find('.js-ci-variable-input-value'); + const $placeholder = $row.find('.js-secret-value-placeholder'); + + $row.find('.js-ci-variable-input-value') + .val('foo') + .trigger('input'); + + expect($placeholder.hasClass(HIDE_CLASS)).toBe(true); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(false); + + variableList.hideValues(); + + expect($placeholder.hasClass(HIDE_CLASS)).toBe(false); + expect($inputValue.hasClass(HIDE_CLASS)).toBe(true); + }); + }); }); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 44ec9e4eabf..1daccc8dd02 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -4,6 +4,8 @@ import axios from '~/lib/utils/axios_utils'; import CommitsList from '~/commits'; describe('Commits List', () => { + let commitsList; + beforeEach(() => { setFixtures(` <form class="commits-search-form" action="/h5bp/html5-boilerplate/commits/master"> @@ -11,6 +13,7 @@ describe('Commits List', () => { </form> <ol id="commits-list"></ol> `); + commitsList = new CommitsList(25); }); it('should be defined', () => { @@ -19,7 +22,7 @@ describe('Commits List', () => { describe('processCommits', () => { it('should join commit headers', () => { - CommitsList.$contentList = $(` + commitsList.$contentList = $(` <div> <li class="commit-header" data-day="2016-09-20"> <span class="day">20 Sep, 2016</span> @@ -39,7 +42,7 @@ describe('Commits List', () => { // The last commit header should be removed // since the previous one has the same data-day value. - expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); + expect(commitsList.processCommits(data).find('li.commit-header').length).toBe(0); }); }); @@ -48,8 +51,7 @@ describe('Commits List', () => { let mock; beforeEach(() => { - CommitsList.init(25); - CommitsList.searchField.val(''); + commitsList.searchField.val(''); spyOn(history, 'replaceState').and.stub(); mock = new MockAdapter(axios); @@ -66,11 +68,11 @@ describe('Commits List', () => { }); it('should save the last search string', (done) => { - CommitsList.searchField.val('GitLab'); - CommitsList.filterResults() + commitsList.searchField.val('GitLab'); + commitsList.filterResults() .then(() => { expect(ajaxSpy).toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual('GitLab'); + expect(commitsList.lastSearch).toEqual('GitLab'); done(); }) @@ -78,10 +80,10 @@ describe('Commits List', () => { }); it('should not make ajax call if the input does not change', (done) => { - CommitsList.filterResults() + commitsList.filterResults() .then(() => { expect(ajaxSpy).not.toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual(''); + expect(commitsList.lastSearch).toEqual(''); done(); }) diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js index 6e1b0429ab7..f3f80cb3771 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_spec.js +++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js @@ -1,11 +1,13 @@ import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; import * as featureHighlight from '~/feature_highlight/feature_highlight'; +import axios from '~/lib/utils/axios_utils'; +import MockAdapter from 'axios-mock-adapter'; describe('feature highlight', () => { beforeEach(() => { setFixtures(` <div> - <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled> + <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" data-dismiss-endpoint="/test" disabled> Trigger </div> </div> @@ -19,13 +21,21 @@ describe('feature highlight', () => { }); describe('setupFeatureHighlightPopover', () => { + let mock; const selector = '.js-feature-highlight[data-highlight=test]'; + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet('/test').reply(200); spyOn(window, 'addEventListener'); spyOn(window, 'removeEventListener'); featureHighlight.setupFeatureHighlightPopover('test', 0); }); + afterEach(() => { + mock.restore(); + }); + it('setup popover content', () => { const $popoverContent = $('.feature-highlight-popover-content'); const outerHTML = $popoverContent.prop('outerHTML'); @@ -51,15 +61,6 @@ describe('feature highlight', () => { }, 0); }); - it('setup inserted.bs.popover', () => { - $(selector).trigger('mouseenter'); - const popoverId = $(selector).attr('aria-describedby'); - const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click'); - - $(`#${popoverId} .dismiss-feature-highlight`).click(); - expect(spyEvent).toHaveBeenTriggered(); - }); - it('setup show.bs.popover', () => { $(selector).trigger('show.bs.popover'); expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); @@ -75,9 +76,19 @@ describe('feature highlight', () => { }); it('displays popover', () => { - expect($(selector).attr('aria-describedby')).toBeFalsy(); + expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeFalsy(); $(selector).trigger('mouseenter'); - expect($(selector).attr('aria-describedby')).toBeTruthy(); + expect(document.querySelector(selector).getAttribute('aria-describedby')).toBeTruthy(); + }); + + it('toggles when clicked', () => { + $(selector).trigger('mouseenter'); + const popoverId = $(selector).attr('aria-describedby'); + const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + + $(`#${popoverId} .dismiss-feature-highlight`).click(); + + expect(toggleSpy).toHaveBeenCalled(); }); }); diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index f1e6119253e..c37a964975d 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -1,8 +1,6 @@ -import '~/filtered_search/dropdown_utils'; -import '~/filtered_search/filtered_search_tokenizer'; -import '~/filtered_search/filtered_search_dropdown'; -import '~/filtered_search/dropdown_user'; - +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import DropdownUser from '~/filtered_search/dropdown_user'; +import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; describe('Dropdown User', () => { @@ -10,18 +8,18 @@ describe('Dropdown User', () => { let dropdownUser; beforeEach(() => { - spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); - spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); + spyOn(DropdownUtils, 'getSearchInput').and.callFake(() => {}); - dropdownUser = new gl.DropdownUser({ + dropdownUser = new DropdownUser({ tokenKeys: FilteredSearchTokenKeys, }); }); it('should not return the double quote found in value', () => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + spyOn(FilteredSearchTokenizer, 'processTokens').and.returnValue({ lastToken: '"johnny appleseed', }); @@ -29,7 +27,7 @@ describe('Dropdown User', () => { }); it('should not return the single quote found in value', () => { - spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + spyOn(FilteredSearchTokenizer, 'processTokens').and.returnValue({ lastToken: '\'larry boy', }); @@ -39,22 +37,22 @@ describe('Dropdown User', () => { describe('config AjaxFilter\'s endpoint', () => { beforeEach(() => { - spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); }); it('should return endpoint', () => { window.gon = { relative_url_root: '', }; - const dropdown = new gl.DropdownUser(); + const dropdown = new DropdownUser(); expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); }); it('should return endpoint when relative_url_root is undefined', () => { - const dropdown = new gl.DropdownUser(); + const dropdown = new DropdownUser(); expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); }); @@ -63,7 +61,7 @@ describe('Dropdown User', () => { window.gon = { relative_url_root: '/gitlab_directory', }; - const dropdown = new gl.DropdownUser(); + const dropdown = new DropdownUser(); expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); }); @@ -84,7 +82,7 @@ describe('Dropdown User', () => { loadFixtures(fixtureTemplate); authorFilterDropdownElement = document.querySelector('#js-dropdown-author'); const dummyInput = document.createElement('div'); - dropdown = new gl.DropdownUser({ + dropdown = new DropdownUser({ dropdown: authorFilterDropdownElement, input: dummyInput, }); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index d6e1af105f1..3d6dec19eca 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -1,6 +1,5 @@ -import '~/filtered_search/dropdown_utils'; -import '~/filtered_search/filtered_search_tokenizer'; -import '~/filtered_search/filtered_search_dropdown_manager'; +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; @@ -10,25 +9,25 @@ describe('Dropdown Utils', () => { describe('getEscapedText', () => { it('should return same word when it has no space', () => { - const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace'); + const escaped = DropdownUtils.getEscapedText('textWithoutSpace'); expect(escaped).toBe('textWithoutSpace'); }); it('should escape with double quotes', () => { - let escaped = gl.DropdownUtils.getEscapedText('text with space'); + let escaped = DropdownUtils.getEscapedText('text with space'); expect(escaped).toBe('"text with space"'); - escaped = gl.DropdownUtils.getEscapedText('won\'t fix'); + escaped = 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'); + const escaped = 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'); + const escaped = DropdownUtils.getEscapedText('won"t\' fix'); expect(escaped).toBe('\'won"t\' fix\''); }); }); @@ -50,14 +49,14 @@ describe('Dropdown Utils', () => { it('should filter without symbol', () => { input.value = 'roo'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); + const updatedItem = DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with symbol', () => { input.value = '@roo'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('@', input, item); + const updatedItem = DropdownUtils.filterWithSymbol('@', input, item); expect(updatedItem.droplab_hidden).toBe(false); }); @@ -69,56 +68,56 @@ describe('Dropdown Utils', () => { it('should filter with double quote', () => { input.value = '"'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote and symbol', () => { input.value = '~"'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote and multiple words', () => { input.value = '"community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with double quote, symbol and multiple words', () => { input.value = '~"community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote', () => { input.value = '\''; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote and symbol', () => { input.value = '~\''; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote and multiple words', () => { input.value = '\'community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); it('should filter with single quote, symbol and multiple words', () => { input.value = '~\'community con'; - const updatedItem = gl.DropdownUtils.filterWithSymbol('~', input, multipleWordItem); + const updatedItem = DropdownUtils.filterWithSymbol('~', input, multipleWordItem); expect(updatedItem.droplab_hidden).toBe(false); }); }); @@ -150,26 +149,26 @@ describe('Dropdown Utils', () => { it('should filter', () => { input.value = 'l'; - let updatedItem = gl.DropdownUtils.filterHint(config(), { + let updatedItem = DropdownUtils.filterHint(config(), { hint: 'label', }); expect(updatedItem.droplab_hidden).toBe(false); input.value = 'o'; - updatedItem = gl.DropdownUtils.filterHint(config(), { + updatedItem = DropdownUtils.filterHint(config(), { hint: 'label', }); expect(updatedItem.droplab_hidden).toBe(true); }); it('should return droplab_hidden false when item has no hint', () => { - const updatedItem = gl.DropdownUtils.filterHint(config(), {}, ''); + const updatedItem = DropdownUtils.filterHint(config(), {}, ''); expect(updatedItem.droplab_hidden).toBe(false); }); it('should allow multiple if item.type is array', () => { input.value = 'label:~first la'; - const updatedItem = gl.DropdownUtils.filterHint(config(), { + const updatedItem = DropdownUtils.filterHint(config(), { hint: 'label', type: 'array', }); @@ -178,12 +177,12 @@ describe('Dropdown Utils', () => { it('should prevent multiple if item.type is not array', () => { input.value = 'milestone:~first mile'; - let updatedItem = gl.DropdownUtils.filterHint(config(), { + let updatedItem = DropdownUtils.filterHint(config(), { hint: 'milestone', }); expect(updatedItem.droplab_hidden).toBe(true); - updatedItem = gl.DropdownUtils.filterHint(config(), { + updatedItem = DropdownUtils.filterHint(config(), { hint: 'milestone', type: 'string', }); @@ -205,7 +204,7 @@ describe('Dropdown Utils', () => { color: '#000000', }; - const updated = gl.DropdownUtils.mergeDuplicateLabels(dataMap, newLabel); + const updated = DropdownUtils.mergeDuplicateLabels(dataMap, newLabel); expect(updated[newLabel.title]).toEqual(newLabel); }); @@ -215,36 +214,36 @@ describe('Dropdown Utils', () => { color: '#000000', }; - const updated = gl.DropdownUtils.mergeDuplicateLabels(dataMap, duplicate); + const updated = DropdownUtils.mergeDuplicateLabels(dataMap, duplicate); expect(updated.label.multipleColors).toEqual([dataMap.label.color, duplicate.color]); }); }); describe('duplicateLabelColor', () => { it('should linear-gradient 2 colors', () => { - const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']); + const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000']); expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 50%, #000000 50%, #000000 100%)'); }); it('should linear-gradient 3 colors', () => { - const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333']); + const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333']); expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 33%, #000000 33%, #000000 66%, #333333 66%, #333333 100%)'); }); it('should linear-gradient 4 colors', () => { - const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD']); + const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD']); expect(gradient).toEqual('linear-gradient(#FFFFFF 0%, #FFFFFF 25%, #000000 25%, #000000 50%, #333333 50%, #333333 75%, #DDDDDD 75%, #DDDDDD 100%)'); }); it('should not linear-gradient more than 4 colors', () => { - const gradient = gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD', '#EEEEEE']); + const gradient = DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000', '#333333', '#DDDDDD', '#EEEEEE']); expect(gradient.indexOf('#EEEEEE') === -1).toEqual(true); }); }); describe('duplicateLabelPreprocessing', () => { it('should set preprocessed to true', () => { - const results = gl.DropdownUtils.duplicateLabelPreprocessing([]); + const results = DropdownUtils.duplicateLabelPreprocessing([]); expect(results.preprocessed).toEqual(true); }); @@ -256,7 +255,7 @@ describe('Dropdown Utils', () => { title: 'label2', color: '#000000', }]; - const results = gl.DropdownUtils.duplicateLabelPreprocessing(data); + const results = DropdownUtils.duplicateLabelPreprocessing(data); expect(results.length).toEqual(2); expect(results[0]).toEqual(data[0]); @@ -271,14 +270,14 @@ describe('Dropdown Utils', () => { title: 'label', color: '#000000', }]; - const results = gl.DropdownUtils.duplicateLabelPreprocessing(data); + const results = DropdownUtils.duplicateLabelPreprocessing(data); it('should merge duplicate labels', () => { expect(results.length).toEqual(1); }); it('should convert multiple colored labels into linear-gradient', () => { - expect(results[0].color).toEqual(gl.DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000'])); + expect(results[0].color).toEqual(DropdownUtils.duplicateLabelColor(['#FFFFFF', '#000000'])); }); it('should set multiple colored label text color to black', () => { @@ -289,7 +288,7 @@ describe('Dropdown Utils', () => { describe('setDataValueIfSelected', () => { beforeEach(() => { - spyOn(gl.FilteredSearchDropdownManager, 'addWordToInput') + spyOn(FilteredSearchDropdownManager, 'addWordToInput') .and.callFake(() => {}); }); @@ -298,8 +297,8 @@ describe('Dropdown Utils', () => { getAttribute: () => 'value', }; - gl.DropdownUtils.setDataValueIfSelected(null, selected); - expect(gl.FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); + DropdownUtils.setDataValueIfSelected(null, selected); + expect(FilteredSearchDropdownManager.addWordToInput.calls.count()).toEqual(1); }); it('returns true when dataValue exists', () => { @@ -307,7 +306,7 @@ describe('Dropdown Utils', () => { getAttribute: () => 'value', }; - const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + const result = DropdownUtils.setDataValueIfSelected(null, selected); expect(result).toBe(true); }); @@ -316,7 +315,7 @@ describe('Dropdown Utils', () => { getAttribute: () => null, }; - const result = gl.DropdownUtils.setDataValueIfSelected(null, selected); + const result = DropdownUtils.setDataValueIfSelected(null, selected); expect(result).toBe(false); }); }); @@ -326,7 +325,7 @@ describe('Dropdown Utils', () => { const value = 'label:none '; it('should return selectionStart when cursor is at the trailing space', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 11, value, }); @@ -336,7 +335,7 @@ describe('Dropdown Utils', () => { }); it('should return input when cursor is at the start of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 0, value, }); @@ -346,7 +345,7 @@ describe('Dropdown Utils', () => { }); it('should return input when cursor is at the middle of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 7, value, }); @@ -356,7 +355,7 @@ describe('Dropdown Utils', () => { }); it('should return input when cursor is at the end of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 10, value, }); @@ -370,7 +369,7 @@ describe('Dropdown Utils', () => { const value = 'label:~"Community Contribution"'; it('should return input when cursor is after the first word', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 17, value, }); @@ -380,7 +379,7 @@ describe('Dropdown Utils', () => { }); it('should return input when cursor is before the second word', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 18, value, }); @@ -394,7 +393,7 @@ describe('Dropdown Utils', () => { const value = 'label:~"Community Contribution'; it('should return entire input when cursor is at the start of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 0, value, }); @@ -404,7 +403,7 @@ describe('Dropdown Utils', () => { }); it('should return entire input when cursor is at the end of input', () => { - const { left, right } = gl.DropdownUtils.getInputSelectionPosition({ + const { left, right } = DropdownUtils.getInputSelectionPosition({ selectionStart: 30, value, }); @@ -434,7 +433,7 @@ describe('Dropdown Utils', () => { const valueContainer = authorToken.querySelector('.value-container'); valueContainer.dataset.originalValue = originalValue; - const searchQuery = gl.DropdownUtils.getSearchQuery(); + const searchQuery = DropdownUtils.getSearchQuery(); expect(searchQuery).toBe(' search term author:original dance'); }); diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js index 5c7e9115aac..71c14582329 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js @@ -1,6 +1,4 @@ -import '~/filtered_search/filtered_search_visual_tokens'; -import '~/filtered_search/filtered_search_tokenizer'; -import '~/filtered_search/filtered_search_dropdown_manager'; +import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager'; describe('Filtered Search Dropdown Manager', () => { beforeEach(() => { @@ -28,7 +26,7 @@ describe('Filtered Search Dropdown Manager', () => { describe('input has no existing value', () => { it('should add just tokenName', () => { - gl.FilteredSearchDropdownManager.addWordToInput('milestone'); + FilteredSearchDropdownManager.addWordToInput('milestone'); const token = document.querySelector('.tokens-container .js-visual-token'); @@ -38,7 +36,7 @@ describe('Filtered Search Dropdown Manager', () => { }); it('should add tokenName and tokenValue', () => { - gl.FilteredSearchDropdownManager.addWordToInput('label'); + FilteredSearchDropdownManager.addWordToInput('label'); let token = document.querySelector('.tokens-container .js-visual-token'); @@ -46,9 +44,9 @@ describe('Filtered Search Dropdown Manager', () => { expect(token.querySelector('.name').innerText).toBe('label'); expect(getInputValue()).toBe(''); - gl.FilteredSearchDropdownManager.addWordToInput('label', 'none'); + FilteredSearchDropdownManager.addWordToInput('label', 'none'); // We have to get that reference again - // Because gl.FilteredSearchDropdownManager deletes the previous token + // Because FilteredSearchDropdownManager deletes the previous token token = document.querySelector('.tokens-container .js-visual-token'); expect(token.classList.contains('filtered-search-token')).toEqual(true); @@ -61,7 +59,7 @@ describe('Filtered Search Dropdown Manager', () => { describe('input has existing value', () => { it('should be able to just add tokenName', () => { setInputValue('a'); - gl.FilteredSearchDropdownManager.addWordToInput('author'); + FilteredSearchDropdownManager.addWordToInput('author'); const token = document.querySelector('.tokens-container .js-visual-token'); @@ -71,10 +69,10 @@ describe('Filtered Search Dropdown Manager', () => { }); it('should replace tokenValue', () => { - gl.FilteredSearchDropdownManager.addWordToInput('author'); + FilteredSearchDropdownManager.addWordToInput('author'); setInputValue('roo'); - gl.FilteredSearchDropdownManager.addWordToInput(null, '@root'); + FilteredSearchDropdownManager.addWordToInput(null, '@root'); const token = document.querySelector('.tokens-container .js-visual-token'); @@ -85,10 +83,10 @@ describe('Filtered Search Dropdown Manager', () => { }); it('should add tokenValues containing spaces', () => { - gl.FilteredSearchDropdownManager.addWordToInput('label'); + FilteredSearchDropdownManager.addWordToInput('label'); setInputValue('"test '); - gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); + FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\''); const token = document.querySelector('.tokens-container .js-visual-token'); diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 0ed9a587dc1..95d02974bdc 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -5,9 +5,10 @@ import RecentSearchesServiceError from '~/filtered_search/services/recent_search import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; import '~/lib/utils/common_utils'; -import '~/filtered_search/filtered_search_tokenizer'; -import '~/filtered_search/filtered_search_dropdown_manager'; -import '~/filtered_search/filtered_search_manager'; +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; +import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager'; +import FilteredSearchManager from '~/filtered_search/filtered_search_manager'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Manager', () => { @@ -49,21 +50,21 @@ describe('Filtered Search Manager', () => { </div> `); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); + spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); }); const initializeManager = () => { /* eslint-disable jasmine/no-unsafe-spy */ - spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); - spyOn(gl.FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); - spyOn(gl.FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); + spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); + spyOn(FilteredSearchManager.prototype, 'tokenChange').and.callFake(() => {}); + spyOn(FilteredSearchDropdownManager.prototype, 'updateDropdownOffset').and.callFake(() => {}); spyOn(gl.utils, 'getParameterByName').and.returnValue(null); - spyOn(gl.FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'unselectTokens').and.callThrough(); /* eslint-enable jasmine/no-unsafe-spy */ input = document.querySelector('.filtered-search'); tokensContainer = document.querySelector('.tokens-container'); - manager = new gl.FilteredSearchManager({ page }); + manager = new FilteredSearchManager({ page }); manager.setup(); }; @@ -81,7 +82,7 @@ describe('Filtered Search Manager', () => { }); it('should instantiate RecentSearchesStore with isLocalStorageAvailable', () => { - manager = new gl.FilteredSearchManager({ page }); + manager = new FilteredSearchManager({ page }); expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); expect(recentSearchesStoreSrc.default).toHaveBeenCalledWith({ @@ -93,7 +94,7 @@ describe('Filtered Search Manager', () => { describe('setup', () => { beforeEach(() => { - manager = new gl.FilteredSearchManager({ page }); + manager = new FilteredSearchManager({ page }); }); it('should not instantiate Flash if an RecentSearchesServiceError is caught', () => { @@ -108,7 +109,7 @@ describe('Filtered Search Manager', () => { describe('searchState', () => { beforeEach(() => { - spyOn(gl.FilteredSearchManager.prototype, 'search').and.callFake(() => {}); + spyOn(FilteredSearchManager.prototype, 'search').and.callFake(() => {}); initializeManager(); }); @@ -134,7 +135,7 @@ describe('Filtered Search Manager', () => { }; manager.searchState(e); - expect(gl.FilteredSearchManager.prototype.search).not.toHaveBeenCalled(); + expect(FilteredSearchManager.prototype.search).not.toHaveBeenCalled(); }); it('should call search when there is state', () => { @@ -149,7 +150,7 @@ describe('Filtered Search Manager', () => { }; manager.searchState(e); - expect(gl.FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened'); + expect(FilteredSearchManager.prototype.search).toHaveBeenCalledWith('opened'); }); }); @@ -251,44 +252,44 @@ describe('Filtered Search Manager', () => { }); it('removes last token', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); dispatchBackspaceEvent(input, 'keyup'); dispatchBackspaceEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.removeLastTokenPartial).toHaveBeenCalled(); }); it('sets the input', () => { - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); dispatchDeleteEvent(input, 'keyup'); dispatchDeleteEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.getLastTokenPartial).toHaveBeenCalled(); expect(input.value).toEqual('~bug'); }); }); it('does not remove token or change input when there is existing input', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); input.value = 'text'; dispatchDeleteEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); expect(input.value).toEqual('text'); }); it('does not remove previous token on single backspace press', () => { - spyOn(gl.FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); - spyOn(gl.FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'removeLastTokenPartial').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'getLastTokenPartial').and.callThrough(); input.value = 't'; dispatchDeleteEvent(input, 'keyup'); - expect(gl.FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); - expect(gl.FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.removeLastTokenPartial).not.toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.getLastTokenPartial).not.toHaveBeenCalled(); expect(input.value).toEqual('t'); }); }); @@ -309,7 +310,7 @@ describe('Filtered Search Manager', () => { describe('unselected token', () => { beforeEach(() => { - spyOn(gl.FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough(); + spyOn(FilteredSearchManager.prototype, 'removeSelectedToken').and.callThrough(); tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML( FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'none'), @@ -380,16 +381,16 @@ describe('Filtered Search Manager', () => { describe('removeSelectedToken', () => { beforeEach(() => { - spyOn(gl.FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough(); - spyOn(gl.FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough(); - spyOn(gl.FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough(); + spyOn(FilteredSearchVisualTokens, 'removeSelectedToken').and.callThrough(); + spyOn(FilteredSearchManager.prototype, 'handleInputPlaceholder').and.callThrough(); + spyOn(FilteredSearchManager.prototype, 'toggleClearSearchButton').and.callThrough(); initializeManager(); }); it('calls FilteredSearchVisualTokens.removeSelectedToken', () => { manager.removeSelectedToken(); - expect(gl.FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled(); + expect(FilteredSearchVisualTokens.removeSelectedToken).toHaveBeenCalled(); }); it('calls handleInputPlaceholder', () => { @@ -421,12 +422,12 @@ describe('Filtered Search Manager', () => { manager.filteredSearchInput.value = inputValue; manager.filteredSearchInput.dispatchEvent(new Event('input')); - expect(gl.DropdownUtils.getSearchQuery()).toEqual(inputValue); + expect(DropdownUtils.getSearchQuery()).toEqual(inputValue); manager.clearSearchButton.click(); expect(manager.filteredSearchInput.value).toEqual(''); - expect(gl.DropdownUtils.getSearchQuery()).toEqual(''); + expect(DropdownUtils.getSearchQuery()).toEqual(''); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index bf8b66f1110..465f5f79931 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -1,19 +1,19 @@ import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; -import '~/filtered_search/filtered_search_tokenizer'; +import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer'; describe('Filtered Search Tokenizer', () => { const allowedKeys = FilteredSearchTokenKeys.getKeys(); describe('processTokens', () => { it('returns for input containing only search value', () => { - const results = gl.FilteredSearchTokenizer.processTokens('searchTerm', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('searchTerm', allowedKeys); 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 + const results = FilteredSearchTokenizer .processTokens('author:@root label:~"Very Important" milestone:%v1.0 assignee:none', allowedKeys); expect(results.searchToken).toBe(''); expect(results.tokens.length).toBe(4); @@ -37,7 +37,7 @@ describe('Filtered Search Tokenizer', () => { }); it('returns for input starting with search value and ending with tokens', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('searchTerm anotherSearchTerm milestone:none', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(1); @@ -48,7 +48,7 @@ describe('Filtered Search Tokenizer', () => { }); it('returns for input starting with tokens and ending with search value', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('assignee:@user searchTerm', allowedKeys); expect(results.searchToken).toBe('searchTerm'); @@ -60,7 +60,7 @@ describe('Filtered Search Tokenizer', () => { }); it('returns for input containing search value wrapped between tokens', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('author:@root label:~"Won\'t fix" searchTerm anotherSearchTerm milestone:none', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); @@ -81,7 +81,7 @@ describe('Filtered Search Tokenizer', () => { }); it('returns for input containing search value in between tokens', () => { - const results = gl.FilteredSearchTokenizer + const results = FilteredSearchTokenizer .processTokens('author:@root searchTerm assignee:none anotherSearchTerm label:~Doing', allowedKeys); expect(results.searchToken).toBe('searchTerm anotherSearchTerm'); expect(results.tokens.length).toBe(3); @@ -101,14 +101,14 @@ describe('Filtered Search Tokenizer', () => { }); it('returns search value for invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('fake:token', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('fake:token', allowedKeys); expect(results.lastToken).toBe('fake:token'); expect(results.searchToken).toBe('fake:token'); expect(results.tokens.length).toEqual(0); }); it('returns search value and token for mix of valid and invalid tokens', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:real fake:token', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('label:real fake:token', allowedKeys); expect(results.tokens.length).toEqual(1); expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('real'); @@ -118,13 +118,13 @@ describe('Filtered Search Tokenizer', () => { }); it('returns search value for invalid symbols', () => { - const results = gl.FilteredSearchTokenizer.processTokens('std::includes', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('std::includes', allowedKeys); expect(results.lastToken).toBe('std::includes'); expect(results.searchToken).toBe('std::includes'); }); it('removes duplicated values', () => { - const results = gl.FilteredSearchTokenizer.processTokens('label:~foo label:~foo', allowedKeys); + const results = FilteredSearchTokenizer.processTokens('label:~foo label:~foo', allowedKeys); expect(results.tokens.length).toBe(1); expect(results.tokens[0].key).toBe('label'); expect(results.tokens[0].value).toBe('foo'); diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js index 0684c3498a2..f1da5f81c0f 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -2,11 +2,12 @@ import _ from 'underscore'; import AjaxCache from '~/lib/utils/ajax_cache'; import UsersCache from '~/lib/utils/users_cache'; -import '~/filtered_search/filtered_search_visual_tokens'; +import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; +import DropdownUtils from '~/filtered_search//dropdown_utils'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Filtered Search Visual Tokens', () => { - const subject = gl.FilteredSearchVisualTokens; + const subject = FilteredSearchVisualTokens; const findElements = (tokenElement) => { const tokenNameElement = tokenElement.querySelector('.name'); @@ -860,25 +861,25 @@ describe('Filtered Search Visual Tokens', () => { it('does not preprocess more than once', () => { let labels = []; - spyOn(gl.DropdownUtils, 'duplicateLabelPreprocessing').and.callFake(() => []); + spyOn(DropdownUtils, 'duplicateLabelPreprocessing').and.callFake(() => []); - labels = gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); - gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); + labels = FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); + FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); - expect(gl.DropdownUtils.duplicateLabelPreprocessing.calls.count()).toEqual(1); + expect(DropdownUtils.duplicateLabelPreprocessing.calls.count()).toEqual(1); }); describe('not preprocessed before', () => { it('returns preprocessed labels', () => { let labels = []; expect(labels.preprocessed).not.toEqual(true); - labels = gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); + labels = FilteredSearchVisualTokens.preprocessLabel(endpoint, labels); expect(labels.preprocessed).toEqual(true); }); it('overrides AjaxCache with preprocessed results', () => { spyOn(AjaxCache, 'override').and.callFake(() => {}); - gl.FilteredSearchVisualTokens.preprocessLabel(endpoint, []); + FilteredSearchVisualTokens.preprocessLabel(endpoint, []); expect(AjaxCache.override.calls.count()).toEqual(1); }); }); @@ -926,7 +927,7 @@ describe('Filtered Search Visual Tokens', () => { }; const findLabel = tokenValue => labelData.find( - label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`, + label => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`, ); it('updates the color of a label token', (done) => { diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js index b13d1bf8dff..67b854f61c0 100644 --- a/spec/javascripts/gl_dropdown_spec.js +++ b/spec/javascripts/gl_dropdown_spec.js @@ -64,8 +64,8 @@ describe('glDropdown', function describeDropdown() { }); afterEach(() => { - $('body').unbind('keydown'); - this.dropdownContainerElement.unbind('keyup'); + $('body').off('keydown'); + this.dropdownContainerElement.off('keyup'); }); it('should open on click', () => { diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index 6599839a526..d8a8c8cc260 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */ import { scaleLinear, scaleTime } from 'd3-scale'; import { timeParse } from 'd3-time-format'; -import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph'; +import { ContributorsGraph, ContributorsMasterGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph'; const d3 = { scaleLinear, scaleTime, timeParse }; diff --git a/spec/javascripts/graphs/stat_graph_contributors_spec.js b/spec/javascripts/graphs/stat_graph_contributors_spec.js index 962423462e7..e03114c1cc5 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_spec.js @@ -1,5 +1,5 @@ -import ContributorsStatGraph from '~/graphs/stat_graph_contributors'; -import { ContributorsGraph } from '~/graphs/stat_graph_contributors_graph'; +import ContributorsStatGraph from '~/pages/projects/graphs/show/stat_graph_contributors'; +import { ContributorsGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph'; import { setLanguage } from '../helpers/locale_helper'; diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 9b47ab62181..22a9afe1a9d 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,6 +1,6 @@ /* eslint-disable quotes, no-var, camelcase, object-property-newline, comma-dangle, max-len, vars-on-top, quote-props */ -import ContributorsStatGraphUtil from '~/graphs/stat_graph_contributors_util'; +import ContributorsStatGraphUtil from '~/pages/projects/graphs/show/stat_graph_contributors_util'; describe("ContributorsStatGraphUtil", function () { describe("#parse_log", function () { diff --git a/spec/javascripts/importer_status_spec.js b/spec/javascripts/importer_status_spec.js index bb49c576e91..71a2cd51f63 100644 --- a/spec/javascripts/importer_status_spec.js +++ b/spec/javascripts/importer_status_spec.js @@ -3,9 +3,18 @@ import axios from '~/lib/utils/axios_utils'; import MockAdapter from 'axios-mock-adapter'; describe('Importer Status', () => { + let instance; + let mock; + + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + describe('addToImport', () => { - let instance; - let mock; const importUrl = '/import_url'; beforeEach(() => { @@ -21,11 +30,6 @@ describe('Importer Status', () => { spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {}); spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {}); instance = new ImporterStatus('', importUrl); - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); }); it('sets table row to active after post request', (done) => { @@ -44,4 +48,60 @@ describe('Importer Status', () => { .catch(done.fail); }); }); + + describe('autoUpdate', () => { + const jobsUrl = '/jobs_url'; + + beforeEach(() => { + const div = document.createElement('div'); + div.innerHTML = ` + <div id="project_1"> + <div class="job-status"> + </div> + </div> + `; + + document.body.appendChild(div); + + spyOn(ImporterStatus.prototype, 'initStatusPage').and.callFake(() => {}); + spyOn(ImporterStatus.prototype, 'setAutoUpdate').and.callFake(() => {}); + instance = new ImporterStatus(jobsUrl); + }); + + function setupMock(importStatus) { + mock.onGet(jobsUrl).reply(200, [{ + id: 1, + import_status: importStatus, + }]); + } + + function expectJobStatus(done, status) { + instance.autoUpdate() + .then(() => { + expect(document.querySelector('#project_1').innerText.trim()).toEqual(status); + done(); + }) + .catch(done.fail); + } + + it('sets the job status to done', (done) => { + setupMock('finished'); + expectJobStatus(done, 'done'); + }); + + it('sets the job status to scheduled', (done) => { + setupMock('scheduled'); + expectJobStatus(done, 'scheduled'); + }); + + it('sets the job status to started', (done) => { + setupMock('started'); + expectJobStatus(done, 'started'); + }); + + it('sets the job status to custom status', (done) => { + setupMock('custom status'); + expectJobStatus(done, 'custom status'); + }); + }); }); diff --git a/spec/javascripts/merge_request_notes_spec.js b/spec/javascripts/merge_request_notes_spec.js index 5d0ee91d977..0d16b23302f 100644 --- a/spec/javascripts/merge_request_notes_spec.js +++ b/spec/javascripts/merge_request_notes_spec.js @@ -23,7 +23,7 @@ describe('Merge request notes', () => { gl.utils.disableButtonIfEmptyField = _.noop; window.project_uploads_path = 'http://test.host/uploads'; $('body').attr('data-page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('author-id'); + window.gon.current_user_id = $('.note:last').data('authorId'); return new Notes('', []); }); @@ -76,7 +76,7 @@ describe('Merge request notes', () => { </form>`; setFixtures(diffsResponse.html + noteFormHtml); $('body').attr('data-page', 'projects:merge_requests:show'); - window.gon.current_user_id = $('.note:last').data('author-id'); + window.gon.current_user_id = $('.note:last').data('authorId'); return new Notes('', []); }); diff --git a/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js index d2386077aa6..349549b9e1f 100644 --- a/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js +++ b/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js @@ -22,19 +22,19 @@ describe('Abuse Reports', () => { it('should truncate long messages', () => { const $longMessage = findMessage('LONG MESSAGE'); - expect($longMessage.data('original-message')).toEqual(jasmine.anything()); + expect($longMessage.data('originalMessage')).toEqual(jasmine.anything()); assertMaxLength($longMessage); }); it('should not truncate short messages', () => { const $shortMessage = findMessage('SHORT MESSAGE'); - expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); + expect($shortMessage.data('originalMessage')).not.toEqual(jasmine.anything()); }); it('should allow clicking a truncated message to expand and collapse the full message', () => { const $longMessage = findMessage('LONG MESSAGE'); $longMessage.click(); - expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); + expect($longMessage.data('originalMessage').length).toEqual($longMessage.text().length); $longMessage.click(); assertMaxLength($longMessage); }); diff --git a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js index 040d14efed2..4655e29eed0 100644 --- a/spec/javascripts/pipeline_schedules/interval_pattern_input_spec.js +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; -import IntervalPatternInput from '~/pipeline_schedules/components/interval_pattern_input.vue'; +import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; Vue.use(Translate); diff --git a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js index ed481cb60a1..f95a7cef18a 100644 --- a/spec/javascripts/pipeline_schedules/pipeline_schedule_callout_spec.js +++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; -import PipelineSchedulesCallout from '~/pipeline_schedules/components/pipeline_schedules_callout.vue'; +import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue'; const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout); const cookieKey = 'pipeline_schedules_callout_dismissed'; diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 8ce33d410a7..e0ea3649646 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -15,7 +15,8 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'repeat', cssClass: 'bar', - id: 123, + pipelineId: 123, + type: 'explode', }, }).$mount(); }); @@ -39,8 +40,9 @@ describe('Pipelines Async Button', () => { describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { - eventHub.$on('actionConfirmationModal', (data) => { - expect(data.id).toEqual(123); + eventHub.$on('openConfirmationModal', (data) => { + expect(data.pipelineId).toEqual(123); + expect(data.type).toEqual('explode'); }); component = new AsyncButtonComponent({ @@ -49,7 +51,8 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - id: 123, + pipelineId: 123, + type: 'explode', }, }).$mount(); diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js index bc6413a159f..e58a8018ed5 100644 --- a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js +++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import Vue from 'vue'; -import PipelineMediator from '~/pipelines/pipeline_details_mediatior'; +import PipelineMediator from '~/pipelines/pipeline_details_mediator'; describe('PipelineMdediator', () => { let mediator; diff --git a/spec/javascripts/pipelines/pipeline_store_spec.js b/spec/javascripts/pipelines/pipeline_store_spec.js index 85d13445b01..ab2287cc344 100644 --- a/spec/javascripts/pipelines/pipeline_store_spec.js +++ b/spec/javascripts/pipelines/pipeline_store_spec.js @@ -8,7 +8,6 @@ describe('Pipeline Store', () => { }); it('should set defaults', () => { - expect(store.state).toEqual({ pipeline: {} }); expect(store.state.pipeline).toEqual({}); }); diff --git a/spec/javascripts/projects/project_import_gitlab_project_spec.js b/spec/javascripts/projects/project_import_gitlab_project_spec.js index 2f1aae109e3..126f73103e0 100644 --- a/spec/javascripts/projects/project_import_gitlab_project_spec.js +++ b/spec/javascripts/projects/project_import_gitlab_project_spec.js @@ -10,7 +10,7 @@ describe('Import Gitlab project', () => { <input class="js-path-name" /> `); - projectImportGitlab.bindEvents(); + projectImportGitlab(); }); afterEach(() => { diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js index c314ca8ab72..8731ce35d81 100644 --- a/spec/javascripts/projects/project_new_spec.js +++ b/spec/javascripts/projects/project_new_spec.js @@ -27,7 +27,7 @@ describe('New Project', () => { }); it('does not change project path for disabled $projectImportUrl', () => { - $projectImportUrl.attr('disabled', true); + $projectImportUrl.prop('disabled', true); projectNew.deriveProjectPathFromUrl($projectImportUrl); @@ -36,7 +36,7 @@ describe('New Project', () => { describe('for enabled $projectImportUrl', () => { beforeEach(() => { - $projectImportUrl.attr('disabled', false); + $projectImportUrl.prop('disabled', false); }); it('does not change project path if it is set by user', () => { diff --git a/spec/javascripts/sidebar/sidebar_move_issue_spec.js b/spec/javascripts/sidebar/sidebar_move_issue_spec.js index 97f762d07a7..0da5d91e376 100644 --- a/spec/javascripts/sidebar/sidebar_move_issue_spec.js +++ b/spec/javascripts/sidebar/sidebar_move_issue_spec.js @@ -78,7 +78,7 @@ describe('SidebarMoveIssue', () => { this.sidebarMoveIssue.onConfirmClicked(); expect(this.mediator.moveIssue).toHaveBeenCalled(); - expect(this.$confirmButton.attr('disabled')).toBe('disabled'); + expect(this.$confirmButton.prop('disabled')).toBeTruthy(); expect(this.$confirmButton.hasClass('is-loading')).toBe(true); }); @@ -93,7 +93,7 @@ describe('SidebarMoveIssue', () => { // Wait for the move issue request to fail setTimeout(() => { expect(window.Flash).toHaveBeenCalled(); - expect(this.$confirmButton.attr('disabled')).toBe(undefined); + expect(this.$confirmButton.prop('disabled')).toBeFalsy(); expect(this.$confirmButton.hasClass('is-loading')).toBe(false); done(); }); @@ -120,7 +120,7 @@ describe('SidebarMoveIssue', () => { this.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click'); expect(this.mediator.setMoveToProjectId).toHaveBeenCalledWith(0); - expect(this.$confirmButton.attr('disabled')).toBe('disabled'); + expect(this.$confirmButton.prop('disabled')).toBeTruthy(); done(); }, 0); }); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 9b2a5379855..323b8a9572d 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -1,6 +1,6 @@ /* eslint-disable jasmine/no-global-setup */ import $ from 'jquery'; -import 'jasmine-jquery'; +import 'vendor/jasmine-jquery'; import '~/commons'; import Vue from 'vue'; @@ -144,6 +144,9 @@ if (process.env.BABEL_ENV === 'coverage') { describe('Uncovered files', function () { const sourceFiles = require.context('~', true, /\.js$/); + + $.holdReady(true); + sourceFiles.keys().forEach(function (path) { // ignore if there is a matching spec file if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js index 33f20ab132d..c89e863d904 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js @@ -1,17 +1,24 @@ import Vue from 'vue'; -import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed'; +import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetNotAllowed', () => { - describe('template', () => { + let vm; + beforeEach(() => { const Component = Vue.extend(notAllowedComponent); - const vm = new Component({ - el: document.createElement('div'), - }); - it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('Ready to be merged automatically.'); - expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request'); - }); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders success icon', () => { + expect(vm.$el.querySelector('.ci-status-icon-success')).not.toBe(null); + }); + + it('renders informative text', () => { + expect(vm.$el.innerText).toContain('Ready to be merged automatically.'); + expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js index d0702f9f503..edab26286bc 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -1,16 +1,23 @@ import Vue from 'vue'; -import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked'; +import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; describe('MRWidgetPipelineBlocked', () => { - describe('template', () => { + let vm; + beforeEach(() => { const Component = Vue.extend(pipelineBlockedComponent); - const vm = new Component({ - el: document.createElement('div'), - }); - it('should have correct elements', () => { - expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed'); - }); + vm = mountComponent(Component); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders warning icon', () => { + expect(vm.$el.querySelector('.ci-status-icon-warning')).not.toBe(null); + }); + + it('renders information text', () => { + expect(vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ')).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed'); }); }); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index cd00d0a39a3..45035effe81 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -295,6 +295,15 @@ describe('mrWidgetOptions', () => { expect(notify.notifyMe).not.toHaveBeenCalled(); }); + + it('should not notify if no pipeline provided', () => { + vm.handleNotification({ + ...data, + pipeline: undefined, + }); + + expect(notify.notifyMe).not.toHaveBeenCalled(); + }); }); describe('resumePolling', () => { diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js new file mode 100644 index 00000000000..d6148cb785b --- /dev/null +++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js @@ -0,0 +1,192 @@ +import Vue from 'vue'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const modalComponent = Vue.extend(GlModal); + +describe('GlModal', () => { + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + describe('props', () => { + describe('with id', () => { + const props = { + id: 'my-modal', + }; + + beforeEach(() => { + vm = mountComponent(modalComponent, props); + }); + + it('assigns the id to the modal', () => { + expect(vm.$el.id).toBe(props.id); + }); + }); + + describe('without id', () => { + beforeEach(() => { + vm = mountComponent(modalComponent, { }); + }); + + it('does not add an id attribute to the modal', () => { + expect(vm.$el.hasAttribute('id')).toBe(false); + }); + }); + + describe('with headerTitleText', () => { + const props = { + headerTitleText: 'my title text', + }; + + beforeEach(() => { + vm = mountComponent(modalComponent, props); + }); + + it('sets the modal title', () => { + const modalTitle = vm.$el.querySelector('.modal-title'); + expect(modalTitle.innerHTML.trim()).toBe(props.headerTitleText); + }); + }); + + describe('with footerPrimaryButtonVariant', () => { + const props = { + footerPrimaryButtonVariant: 'danger', + }; + + beforeEach(() => { + vm = mountComponent(modalComponent, props); + }); + + it('sets the primary button class', () => { + const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type'); + expect(primaryButton).toHaveClass(`btn-${props.footerPrimaryButtonVariant}`); + }); + }); + + describe('with footerPrimaryButtonText', () => { + const props = { + footerPrimaryButtonText: 'my button text', + }; + + beforeEach(() => { + vm = mountComponent(modalComponent, props); + }); + + it('sets the primary button text', () => { + const primaryButton = vm.$el.querySelector('.modal-footer button:last-of-type'); + expect(primaryButton.innerHTML.trim()).toBe(props.footerPrimaryButtonText); + }); + }); + }); + + it('works with data-toggle="modal"', (done) => { + setFixtures(` + <button id="modal-button" data-toggle="modal" data-target="#my-modal"></button> + <div id="modal-container"></div> + `); + + const modalContainer = document.getElementById('modal-container'); + const modalButton = document.getElementById('modal-button'); + vm = mountComponent(modalComponent, { + id: 'my-modal', + }, modalContainer); + $(vm.$el).on('shown.bs.modal', () => done()); + + modalButton.click(); + }); + + describe('methods', () => { + const dummyEvent = 'not really an event'; + + beforeEach(() => { + vm = mountComponent(modalComponent, { }); + spyOn(vm, '$emit'); + }); + + describe('emitCancel', () => { + it('emits a cancel event', () => { + vm.emitCancel(dummyEvent); + + expect(vm.$emit).toHaveBeenCalledWith('cancel', dummyEvent); + }); + }); + + describe('emitSubmit', () => { + it('emits a submit event', () => { + vm.emitSubmit(dummyEvent); + + expect(vm.$emit).toHaveBeenCalledWith('submit', dummyEvent); + }); + }); + }); + + describe('slots', () => { + const slotContent = 'this should go into the slot'; + const modalWithSlot = (slotName) => { + let template; + if (slotName) { + template = ` + <gl-modal> + <template slot="${slotName}">${slotContent}</template> + </gl-modal> + `; + } else { + template = `<gl-modal>${slotContent}</gl-modal>`; + } + + return Vue.extend({ + components: { + GlModal, + }, + template, + }); + }; + + describe('default slot', () => { + beforeEach(() => { + vm = mountComponent(modalWithSlot()); + }); + + it('sets the modal body', () => { + const modalBody = vm.$el.querySelector('.modal-body'); + expect(modalBody.innerHTML).toBe(slotContent); + }); + }); + + describe('header slot', () => { + beforeEach(() => { + vm = mountComponent(modalWithSlot('header')); + }); + + it('sets the modal header', () => { + const modalHeader = vm.$el.querySelector('.modal-header'); + expect(modalHeader.innerHTML).toBe(slotContent); + }); + }); + + describe('title slot', () => { + beforeEach(() => { + vm = mountComponent(modalWithSlot('title')); + }); + + it('sets the modal title', () => { + const modalTitle = vm.$el.querySelector('.modal-title'); + expect(modalTitle.innerHTML).toBe(slotContent); + }); + }); + + describe('footer slot', () => { + beforeEach(() => { + vm = mountComponent(modalWithSlot('footer')); + }); + + it('sets the modal footer', () => { + const modalFooter = vm.$el.querySelector('.modal-footer'); + expect(modalFooter.innerHTML).toBe(slotContent); + }); + }); + }); +}); diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb index 91e18d876d5..1d98fc0d5db 100644 --- a/spec/lib/banzai/filter/html_entity_filter_spec.rb +++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb @@ -3,17 +3,12 @@ require 'spec_helper' describe Banzai::Filter::HtmlEntityFilter do include FilterSpecHelper - let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' } - let(:escaped) { 'foo <strike attr="foo">&&&</strike>' } + let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' } + let(:escaped) { 'foo <strike attr="foo">&&amp;&</strike>' } it 'converts common entities to their HTML-escaped equivalents' do output = filter(unescaped) expect(output).to eq(escaped) end - - it 'does not double-escape' do - escaped = ERB::Util.html_escape("Merge branch 'blabla' into 'master'") - expect(filter(escaped)).to eq(escaped) - end end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index f668f78c2b8..2a0e19ae796 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -95,6 +95,14 @@ module Gitlab expect(render(input, context)).to include('<p><code data-math-style="inline" class="code math js-render-math">2+2</code> is 4</p>') end end + + context 'outfilesuffix' do + it 'defaults to adoc' do + output = render("Inter-document reference <<README.adoc#>>", context) + + expect(output).to include("a href=\"README.adoc\"") + end + end end def render(*args) diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb new file mode 100644 index 00000000000..c76adcbe2f5 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb @@ -0,0 +1,262 @@ +require 'spec_helper' + +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers + + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:uploads) { table(:uploads) } + + before(:all) do + ensure_temporary_tracking_table_exists + end + + describe '#upload_path' do + def assert_upload_path(file_path, expected_upload_path) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.upload_path).to eq(expected_upload_path) + end + + context 'for an appearance logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') + end + end + + context 'for an appearance header_logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') + end + end + + context 'for a user avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') + end + end + + context 'for a group avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') + end + end + + context 'for a project avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the file path relative to the project directory in uploads' do + project = create_project + random_hex = SecureRandom.hex + + assert_upload_path("/#{get_full_path(project)}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") + end + end + end + + describe '#uploader' do + def assert_uploader(file_path, expected_uploader) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.uploader).to eq(expected_uploader) + end + + context 'for an appearance logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for an appearance header_logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') + end + end + + context 'for a user avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a group avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns FileUploader as a string' do + project = create_project + + assert_uploader("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') + end + end + end + + describe '#model_type' do + def assert_model_type(file_path, expected_model_type) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_type).to eq(expected_model_type) + end + + context 'for an appearance logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for an appearance header_logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns Note as a string' do + assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') + end + end + + context 'for a user avatar file path' do + it 'returns User as a string' do + assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') + end + end + + context 'for a group avatar file path' do + it 'returns Namespace as a string' do + assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') + end + end + + context 'for a project avatar file path' do + it 'returns Project as a string' do + assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns Project as a string' do + project = create_project + + assert_model_type("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'Project') + end + end + end + + describe '#model_id' do + def assert_model_id(file_path, expected_model_id) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_id).to eq(expected_model_id) + end + + context 'for an appearance logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) + end + end + + context 'for an appearance header_logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) + end + end + + context 'for a user avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a group avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the ID as a string' do + project = create_project + + assert_model_id("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", project.id) + end + end + end + + describe '#file_size' do + context 'for an appearance logo file path' do + let(:appearance) { create_or_update_appearance(logo: true) } + let(:untracked_file) { described_class.create!(path: get_uploads(appearance, 'Appearance').first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + + context 'for a project avatar file path' do + let(:project) { create_project(avatar: true) } + let(:untracked_file) { described_class.create!(path: get_uploads(project, 'Project').first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + let(:project) { create_project } + let(:untracked_file) { create_untracked_file("/#{get_full_path(project)}/#{get_uploads(project, 'Project').first.path}") } + + before do + add_markdown_attachment(project) + end + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + end + + def create_untracked_file(path_relative_to_upload_dir) + described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") + end +end diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index fb3f29ff4c9..0d2074eed22 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -1,54 +1,50 @@ require 'spec_helper' -# This migration is using UploadService, which sets uploads.secret that is only -# added to the DB schema in 20180129193323. Since the test isn't isolated, we -# just use the latest schema when testing this migration. -# Ideally, the test should not use factories nor UploadService, and rely on the -# `table` helper instead. -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do - include TrackUntrackedUploadsHelpers +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers subject { described_class.new } - let!(:untracked_files_for_uploads) { described_class::UntrackedFile } - let!(:uploads) { described_class::Upload } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:notes) { table(:notes) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } + let!(:uploads) { table(:uploads) } + let!(:users) { table(:users) } before do - DatabaseCleaner.clean - drop_temp_table_if_exists ensure_temporary_tracking_table_exists uploads.delete_all end - after(:all) do - drop_temp_table_if_exists - end - context 'with untracked files and tracked files in untracked_files_for_uploads' do - let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } - let!(:user1) { create(:user, :with_avatar) } - let!(:user2) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :legacy_storage, :with_avatar) } - let!(:project2) { create(:project, :legacy_storage, :with_avatar) } + let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } + let!(:user1) { create_user(avatar: true) } + let!(:user2) { create_user(avatar: true) } + let!(:project1) { create_project(avatar: true) } + let!(:project2) { create_project(avatar: true) } before do - UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload - UploadService.new(project2, uploaded_file, FileUploader).execute # Markdown upload + add_markdown_attachment(project1) + add_markdown_attachment(project2) # File records created by PrepareUntrackedUploads - untracked_files_for_uploads.create!(path: appearance.uploads.first.path) - untracked_files_for_uploads.create!(path: appearance.uploads.last.path) - untracked_files_for_uploads.create!(path: user1.uploads.first.path) - untracked_files_for_uploads.create!(path: user2.uploads.first.path) - untracked_files_for_uploads.create!(path: project1.uploads.first.path) - untracked_files_for_uploads.create!(path: project2.uploads.first.path) - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project1.full_path}/#{project1.uploads.last.path}") - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/#{project2.uploads.last.path}") + untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').first.path) + untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').last.path) + untracked_files_for_uploads.create!(path: get_uploads(user1, 'User').first.path) + untracked_files_for_uploads.create!(path: get_uploads(user2, 'User').first.path) + untracked_files_for_uploads.create!(path: get_uploads(project1, 'Project').first.path) + untracked_files_for_uploads.create!(path: get_uploads(project2, 'Project').first.path) + untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project1).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}") + untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project2).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}") # Untrack 4 files - user2.uploads.delete_all - project2.uploads.delete_all # 2 files: avatar and a Markdown upload - appearance.uploads.where("path like '%header_logo%'").delete_all + get_uploads(user2, 'User').delete_all + get_uploads(project2, 'Project').delete_all # 2 files: avatar and a Markdown upload + get_uploads(appearance, 'Appearance').where("path like '%header_logo%'").delete_all end it 'adds untracked files to the uploads table' do @@ -56,9 +52,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(1, untracked_files_for_uploads.reorder(:id).last.id) end.to change { uploads.count }.from(4).to(8) - expect(user2.uploads.count).to eq(1) - expect(project2.uploads.count).to eq(2) - expect(appearance.uploads.count).to eq(2) + expect(get_uploads(user2, 'User').count).to eq(1) + expect(get_uploads(project2, 'Project').count).to eq(2) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) end it 'deletes rows after processing them' do @@ -72,9 +68,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra it 'does not create duplicate uploads of already tracked files' do subject.perform(1, untracked_files_for_uploads.last.id) - expect(user1.uploads.count).to eq(1) - expect(project1.uploads.count).to eq(2) - expect(appearance.uploads.count).to eq(2) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) end it 'uses the start and end batch ids [only 1st half]' do @@ -86,11 +82,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(user1.uploads.count).to eq(1) - expect(user2.uploads.count).to eq(1) - expect(appearance.uploads.count).to eq(2) - expect(project1.uploads.count).to eq(2) - expect(project2.uploads.count).to eq(0) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(user2, 'User').count).to eq(1) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(project2, 'Project').count).to eq(0) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -105,11 +101,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(user1.uploads.count).to eq(1) - expect(user2.uploads.count).to eq(0) - expect(appearance.uploads.count).to eq(1) - expect(project1.uploads.count).to eq(2) - expect(project2.uploads.count).to eq(2) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(user2, 'User').count).to eq(0) + expect(get_uploads(appearance, 'Appearance').count).to eq(1) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(project2, 'Project').count).to eq(2) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -122,13 +118,13 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do - subject.perform(1, untracked_files_for_uploads.last.id) + expect(subject).to receive(:drop_temp_table_if_finished) - expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_falsey + subject.perform(1, untracked_files_for_uploads.last.id) end it 'does not block a whole batch because of one bad path' do - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a") expect(untracked_files_for_uploads.count).to eq(9) expect(uploads.count).to eq(4) @@ -139,7 +135,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'an unparseable path is shown in error output' do - bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a" + bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a" untracked_files_for_uploads.create!(path: bad_path) expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/) @@ -158,367 +154,100 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra describe 'upload outcomes for each path pattern' do shared_examples_for 'non_markdown_file' do - let!(:expected_upload_attrs) { model.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let!(:expected_upload_attrs) { model_uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - model.uploads.delete_all + model_uploads.delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model.reload.uploads.count }.from(0).to(1) + end.to change { model_uploads.count }.from(0).to(1) - expect(model.uploads.first.attributes).to include(expected_upload_attrs) + expect(model_uploads.first.attributes).to include(expected_upload_attrs) end end context 'for an appearance logo file path' do - let(:model) { create_or_update_appearance(logo: uploaded_file) } + let(:model) { create_or_update_appearance(logo: true) } + let(:model_uploads) { get_uploads(model, 'Appearance') } it_behaves_like 'non_markdown_file' end context 'for an appearance header_logo file path' do - let(:model) { create_or_update_appearance(header_logo: uploaded_file) } + let(:model) { create_or_update_appearance(header_logo: true) } + let(:model_uploads) { get_uploads(model, 'Appearance') } it_behaves_like 'non_markdown_file' end context 'for a pre-Markdown Note attachment file path' do - let(:model) { create(:note, :with_attachment) } - let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let(:model) { create_note(attachment: true) } + let!(:expected_upload_attrs) { get_uploads(model, 'Note').first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - Upload.where(model_type: 'Note', model_id: model.id).delete_all + get_uploads(model, 'Note').delete_all end # Can't use the shared example because Note doesn't have an `uploads` association it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1) + end.to change { get_uploads(model, 'Note').count }.from(0).to(1) - expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs) + expect(get_uploads(model, 'Note').first.attributes).to include(expected_upload_attrs) end end context 'for a user avatar file path' do - let(:model) { create(:user, :with_avatar) } + let(:model) { create_user(avatar: true) } + let(:model_uploads) { get_uploads(model, 'User') } it_behaves_like 'non_markdown_file' end context 'for a group avatar file path' do - let(:model) { create(:group, :with_avatar) } + let(:model) { create_group(avatar: true) } + let(:model_uploads) { get_uploads(model, 'Namespace') } it_behaves_like 'non_markdown_file' end context 'for a project avatar file path' do - let(:model) { create(:project, :legacy_storage, :with_avatar) } + let(:model) { create_project(avatar: true) } + let(:model_uploads) { get_uploads(model, 'Project') } it_behaves_like 'non_markdown_file' end context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:model) { create(:project, :legacy_storage) } + let(:model) { create_project } before do # Upload the file - UploadService.new(model, uploaded_file, FileUploader).execute + add_markdown_attachment(model) # Create the untracked_files_for_uploads record - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{model.full_path}/#{model.uploads.first.path}") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(model)}/#{get_uploads(model, 'Project').first.path}") # Save the expected upload attributes - @expected_upload_attrs = model.reload.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') + @expected_upload_attrs = get_uploads(model, 'Project').first.attributes.slice('path', 'uploader', 'size', 'checksum') # Untrack the file - model.reload.uploads.delete_all + get_uploads(model, 'Project').delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model.reload.uploads.count }.from(0).to(1) - - expect(model.uploads.first.attributes).to include(@expected_upload_attrs) - end - end - end -end - -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do - include TrackUntrackedUploadsHelpers - - let(:upload_class) { Gitlab::BackgroundMigration::PopulateUntrackedUploads::Upload } - - before(:all) do - ensure_temporary_tracking_table_exists - end - - after(:all) do - drop_temp_table_if_exists - end - - describe '#upload_path' do - def assert_upload_path(file_path, expected_upload_path) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.upload_path).to eq(expected_upload_path) - end - - context 'for an appearance logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') - end - end - - context 'for an appearance header_logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') - end - end - - context 'for a user avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') - end - end - - context 'for a group avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') - end - end - - context 'for a project avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') - end - end + end.to change { get_uploads(model, 'Project').count }.from(0).to(1) - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the file path relative to the project directory in uploads' do - project = create(:project, :legacy_storage) - random_hex = SecureRandom.hex - - assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") - end - end - end - - describe '#uploader' do - def assert_uploader(file_path, expected_uploader) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.uploader).to eq(expected_uploader) - end - - context 'for an appearance logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for an appearance header_logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') - end - end - - context 'for a user avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a group avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns FileUploader as a string' do - project = create(:project, :legacy_storage) - - assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') - end - end - end - - describe '#model_type' do - def assert_model_type(file_path, expected_model_type) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_type).to eq(expected_model_type) - end - - context 'for an appearance logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') + expect(get_uploads(model, 'Project').first.attributes).to include(@expected_upload_attrs) end end - - context 'for an appearance header_logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns Note as a string' do - assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') - end - end - - context 'for a user avatar file path' do - it 'returns User as a string' do - assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') - end - end - - context 'for a group avatar file path' do - it 'returns Namespace as a string' do - assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') - end - end - - context 'for a project avatar file path' do - it 'returns Project as a string' do - assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns Project as a string' do - project = create(:project, :legacy_storage) - - assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project') - end - end - end - - describe '#model_id' do - def assert_model_id(file_path, expected_model_id) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_id).to eq(expected_model_id) - end - - context 'for an appearance logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) - end - end - - context 'for an appearance header_logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) - end - end - - context 'for a user avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a group avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the ID as a string' do - project = create(:project, :legacy_storage) - - assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id) - end - end - end - - describe '#file_size' do - context 'for an appearance logo file path' do - let(:appearance) { create_or_update_appearance(logo: uploaded_file) } - let(:untracked_file) { described_class.create!(path: appearance.uploads.first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(appearance.logo.size) - end - end - - context 'for a project avatar file path' do - let(:project) { create(:project, :legacy_storage, avatar: uploaded_file) } - let(:untracked_file) { described_class.create!(path: project.uploads.first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(project.avatar.size) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:project) { create(:project, :legacy_storage) } - let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") } - - before do - UploadService.new(project, uploaded_file, FileUploader).execute - end - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(project.uploads.first.size) - end - end - end - - def create_untracked_file(path_relative_to_upload_dir) - described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") end end diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb index 43f3548eadc..35750d89c35 100644 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -1,18 +1,16 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do - include TrackUntrackedUploadsHelpers - include MigrationsHelpers - - let!(:untracked_files_for_uploads) { described_class::UntrackedFile } - - before do - DatabaseCleaner.clean - end - - after do - drop_temp_table_if_exists - end +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers + + let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:uploads) { table(:uploads) } + let!(:users) { table(:users) } around do |example| # Especially important so the follow-up migration does not get run @@ -23,19 +21,17 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do shared_examples 'prepares the untracked_files_for_uploads table' do context 'when files were uploaded before and after hashed storage was enabled' do - let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } - let!(:user) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :with_avatar, :legacy_storage) } - let(:project2) { create(:project) } # instantiate after enabling hashed_storage + let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } + let!(:user) { create_user(avatar: true) } + let!(:project1) { create_project(avatar: true) } + let(:project2) { create_project } # instantiate after enabling hashed_storage before do # Markdown upload before enabling hashed_storage - UploadService.new(project1, uploaded_file, FileUploader).execute - - stub_application_setting(hashed_storage_enabled: true) + add_markdown_attachment(project1) # Markdown upload after enabling hashed_storage - UploadService.new(project2, uploaded_file, FileUploader).execute + add_markdown_attachment(project2, hashed_storage: true) end it 'has a path field long enough for really long paths' do @@ -69,14 +65,15 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do it 'does not add hashed files to the untracked_files_for_uploads table' do described_class.new.perform - hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path + hashed_file_path = get_uploads(project2, 'Project').where(uploader: 'FileUploader').first.path expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey end it 'correctly schedules the follow-up background migration jobs' do described_class.new.perform - expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5) + ids = described_class::UntrackedFile.all.order(:id).pluck(:id) + expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(ids.first, ids.last) expect(BackgroundMigrationWorker.jobs.size).to eq(1) end @@ -150,9 +147,11 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do # may not have an upload directory because they have no uploads. context 'when no files were ever uploaded' do it 'deletes the `untracked_files_for_uploads` table (and does not raise error)' do - described_class.new.perform + background_migration = described_class.new + + expect(background_migration).to receive(:drop_temp_table) - expect(untracked_files_for_uploads.connection.table_exists?(:untracked_files_for_uploads)).to be_falsey + background_migration.perform end end end diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb index 475b5c5cfb2..b49ddbfc780 100644 --- a/spec/lib/gitlab/checks/change_access_spec.rb +++ b/spec/lib/gitlab/checks/change_access_spec.rb @@ -190,7 +190,7 @@ describe Gitlab::Checks::ChangeAccess do context 'with LFS not enabled' do it 'skips the validation' do - expect_any_instance_of(described_class).not_to receive(:lfs_file_locks_validation) + expect_any_instance_of(Gitlab::Checks::CommitCheck).not_to receive(:validate) subject.exec end @@ -207,7 +207,7 @@ describe Gitlab::Checks::ChangeAccess do end end - context 'when change is sent by the author od the lock' do + context 'when change is sent by the author of the lock' do let(:user) { owner } it "doesn't raise any error" do diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 28c679af12a..8d4862932b2 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -365,6 +365,20 @@ describe Gitlab::ClosingIssueExtractor do .to match_array([issue, other_issue, third_issue]) end + it 'allows oxford commas (comma before and) when referencing multiple issues' do + message = "Closes #{reference}, #{reference2}, and #{reference3}" + + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue, third_issue]) + end + + it 'allows spaces before commas when referencing multiple issues' do + message = "Closes #{reference} , #{reference2} , and #{reference3}" + + expect(subject.closed_by_message(message)) + .to match_array([issue, other_issue, third_issue]) + end + it 'fetches issues in multi-line message' do message = "Awesome commit (closes #{reference})\nAlso fixes #{reference2}" diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index cd602ccab8e..73d60c021c8 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -72,6 +72,28 @@ describe Gitlab::Diff::Highlight do expect(subject[5].text).to eq(code) expect(subject[5].text).to be_html_safe end + + context 'when the inline diff marker has an invalid range' do + before do + allow_any_instance_of(Gitlab::Diff::InlineDiffMarker).to receive(:mark).and_raise(RangeError) + end + + it 'keeps the original rich line' do + code = %q{+ raise RuntimeError, "System commands must be given as an array of strings"} + + expect(subject[5].text).to eq(code) + expect(subject[5].text).not_to be_html_safe + end + + it 'reports to Sentry if configured' do + allow(Gitlab::Sentry).to receive(:enabled?).and_return(true) + + expect(Gitlab::Sentry).to receive(:context) + expect(Raven).to receive(:capture_exception) + + subject + end + end end end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index 59e9e1cc94c..a6341cd509b 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -276,6 +276,7 @@ describe Gitlab::Git::Blob, seed_helper: true do expect(blobs.count).to eq(1) expect(blobs).to all( be_a(Gitlab::Git::Blob) ) + expect(blobs).to be_an(Array) end it 'accepts blob IDs as a lazy enumerator' do diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 85e6efd7ca2..0b20a6349a2 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -102,6 +102,10 @@ describe Gitlab::Git::Commit, seed_helper: true do expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit end + it "returns an array of parent ids" do + expect(described_class.find(repository, SeedRepo::Commit::ID).parent_ids).to be_an(Array) + end + it "should return valid commit for tag" do expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index edcf8889c27..d601a383a98 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require "spec_helper" describe Gitlab::Git::Repository, seed_helper: true do @@ -599,6 +600,33 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#branch_names_contains_sha' do + shared_examples 'returning the right branches' do + let(:head_id) { repository.rugged.head.target.oid } + let(:new_branch) { head_id } + + before do + repository.create_branch(new_branch, 'master') + end + + after do + repository.delete_branch(new_branch) + end + + it 'displays that branch' do + expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch) + end + end + + context 'when Gitaly is enabled' do + it_behaves_like 'returning the right branches' + end + + context 'when Gitaly is disabled', :disable_gitaly do + it_behaves_like 'returning the right branches' + end + end + describe "#refs_hash" do subject { repository.refs_hash } @@ -1589,7 +1617,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expected_languages = [ { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" }, { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, - { value: 7.9, label: "HTML", color: "#e44b23", highlight: "#e44b23" }, + { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" }, { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" } ] @@ -2203,7 +2231,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'sparse checkout', :skip_gitaly_mock do let(:expected_files) { %w(files files/js files/js/application.js) } - before do + it 'checks out only the files in the diff' do allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| m.call(*args) do worktree_path = args[0] @@ -2215,11 +2243,45 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(Dir[files_pattern]).to eq(expected) end end - end - it 'checkouts only the files in the diff' do subject end + + context 'when the diff contains a rename' do + let(:repo) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged } + let(:end_sha) { new_commit_move_file(repo).oid } + + after do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + repo.references.update('refs/heads/master', SeedRepo::LastCommit::ID) + end + + it 'does not include the renamed file in the sparse checkout' do + allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| + m.call(*args) do + worktree_path = args[0] + files_pattern = File.join(worktree_path, '**', '*') + + expect(Dir[files_pattern]).not_to include('CHANGELOG') + expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG') + end + end + + subject + end + end + end + + context 'with an ASCII-8BIT diff', :skip_gitaly_mock do + let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+✓ testme\n ======\n \n Sample repo for testing gitlab features\n" } + + it 'applies a ASCII-8BIT diff' do + allow(repository).to receive(:run_git!).and_call_original + allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) + + expect(subject).to match(/\h{40}/) + end end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 3c3697e7aa9..19d3f55501e 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -18,8 +18,9 @@ describe Gitlab::GitAccess do redirected_path: redirected_path) end - let(:push_access_check) { access.check('git-receive-pack', '_any') } - let(:pull_access_check) { access.check('git-upload-pack', '_any') } + let(:changes) { '_any' } + let(:push_access_check) { access.check('git-receive-pack', changes) } + let(:pull_access_check) { access.check('git-upload-pack', changes) } describe '#check with single protocols allowed' do def disable_protocol(protocol) @@ -646,6 +647,20 @@ describe Gitlab::GitAccess do end end + describe 'check LFS integrity' do + let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master', '6f6d7e7ed 570e7b2ab refs/heads/feature'] } + + before do + project.add_developer(user) + end + + it 'checks LFS integrity only for first change' do + expect_any_instance_of(Gitlab::Checks::LfsIntegrity).to receive(:objects_missing?).exactly(1).times + + push_access_check + end + end + describe '#check_push_access!' do before do merge_into_protected_branch diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 186b2d9279d..215f1ecc9c5 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::GitAccessWiki do let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :wiki_repo) } let(:user) { create(:user) } let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] } let(:redirected_path) { nil } @@ -48,6 +48,18 @@ describe Gitlab::GitAccessWiki do it 'give access to download wiki code' do expect { subject }.not_to raise_error end + + context 'when the wiki repository does not exist' do + it 'returns not found' do + wiki_repo = project.wiki.repository + FileUtils.rm_rf(wiki_repo.path) + + # Sanity check for rm_rf + expect(wiki_repo.exists?).to eq(false) + + expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'A repository for this project does not exist yet.') + end + end end context 'when wiki feature is disabled' do diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index cbc7ce1c1b0..c50e73cecfc 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -84,4 +84,30 @@ describe Gitlab::GitalyClient::RepositoryService do expect(client.has_local_branches?).to be(true) end end + + describe '#rebase_in_progress?' do + let(:rebase_id) { 1 } + + it 'sends a repository_rebase_in_progress message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:is_rebase_in_progress) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(in_progress: true)) + + client.rebase_in_progress?(rebase_id) + end + end + + describe '#squash_in_progress?' do + let(:squash_id) { 1 } + + it 'sends a repository_squash_in_progress message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:is_squash_in_progress) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(in_progress: true)) + + client.squash_in_progress?(squash_id) + end + end end diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 46a57e08963..5bedfc79dd3 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -11,7 +11,8 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do import_source: 'foo/bar', repository_storage_path: 'foo', disk_path: 'foo', - repository: repository + repository: repository, + create_wiki: true ) end @@ -192,7 +193,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do expect(importer.import_wiki_repository).to eq(true) end - it 'marks the import as failed if an error was raised' do + it 'marks the import as failed and creates an empty repo if an error was raised' do expect(importer.gitlab_shell) .to receive(:import_repository) .and_raise(Gitlab::Shell::Error) @@ -201,6 +202,9 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do .to receive(:fail_import) .and_return(false) + expect(project) + .to receive(:create_wiki) + expect(importer.import_wiki_repository).to eq(false) end end diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb index e3bf2801406..67c62458f0f 100644 --- a/spec/lib/gitlab/gpg/commit_spec.rb +++ b/spec/lib/gitlab/gpg/commit_spec.rb @@ -49,7 +49,9 @@ describe Gitlab::Gpg::Commit do end it 'returns a valid signature' do - expect(described_class.new(commit).signature).to have_attributes( + signature = described_class.new(commit).signature + + expect(signature).to have_attributes( commit_sha: commit_sha, project: project, gpg_key: gpg_key, @@ -58,9 +60,31 @@ describe Gitlab::Gpg::Commit do gpg_key_user_email: GpgHelpers::User1.emails.first, verification_status: 'verified' ) + expect(signature.persisted?).to be_truthy end it_behaves_like 'returns the cached signature on second call' + + context 'read-only mode' do + before do + allow(Gitlab::Database).to receive(:read_only?).and_return(true) + end + + it 'does not create a cached signature' do + signature = described_class.new(commit).signature + + expect(signature).to have_attributes( + commit_sha: commit_sha, + project: project, + gpg_key: gpg_key, + gpg_key_primary_keyid: GpgHelpers::User1.primary_keyid, + gpg_key_user_name: GpgHelpers::User1.names.first, + gpg_key_user_email: GpgHelpers::User1.emails.first, + verification_status: 'verified' + ) + expect(signature.persisted?).to be_falsey + end + end end context 'commit signed with a subkey' do diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb index ca2213cd112..e10837578a8 100644 --- a/spec/lib/gitlab/ldap/config_spec.rb +++ b/spec/lib/gitlab/ldap/config_spec.rb @@ -5,6 +5,14 @@ describe Gitlab::LDAP::Config do let(:config) { described_class.new('ldapmain') } + describe '.servers' do + it 'returns empty array if no server information is available' do + allow(Gitlab.config).to receive(:ldap).and_return('enabled' => false) + + expect(described_class.servers).to eq [] + end + end + describe '#initialize' do it 'requires a provider' do expect { described_class.new }.to raise_error ArgumentError diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index b54d4000b53..05e1e394bb1 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -66,15 +66,6 @@ describe Gitlab::LDAP::Person do end end - describe '.validate_entry' do - it 'raises InvalidEntryError' do - entry['foo'] = 'bar' - - expect { described_class.new(entry, 'ldapmain') } - .to raise_error(Gitlab::LDAP::Person::InvalidEntryError) - end - end - describe '#name' do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 03e0a9e2a03..b8455403bdb 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -724,6 +724,10 @@ describe Gitlab::OAuth::User do it "does not update the user location" do expect(gl_user.location).not_to eq(info_hash[:address][:country]) end + + it 'does not create associated user synced attributes metadata' do + expect(gl_user.user_synced_attributes_metadata).to be_nil + end end end diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb index 4a43dbb2371..f02b1cf55fb 100644 --- a/spec/lib/gitlab/profiler_spec.rb +++ b/spec/lib/gitlab/profiler_spec.rb @@ -53,6 +53,15 @@ describe Gitlab::Profiler do described_class.profile('/', user: user) end + context 'when providing a user without a personal access token' do + it 'raises an error' do + user = double(:user) + allow(user).to receive_message_chain(:personal_access_tokens, :active, :pluck).and_return([]) + + expect { described_class.profile('/', user: user) }.to raise_error('Your user must have a personal_access_token') + end + end + it 'uses the private_token for auth if both it and user are set' do user = double(:user) user_token = 'user' diff --git a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb index b49bc5c328c..f8faeffb935 100644 --- a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb +++ b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb @@ -1,19 +1,34 @@ require 'spec_helper' describe Gitlab::QueryLimiting::ActiveSupportSubscriber do + let(:transaction) { instance_double(Gitlab::QueryLimiting::Transaction, increment: true) } + + before do + allow(Gitlab::QueryLimiting::Transaction) + .to receive(:current) + .and_return(transaction) + end + describe '#sql' do it 'increments the number of executed SQL queries' do - transaction = double(:transaction) - - allow(Gitlab::QueryLimiting::Transaction) - .to receive(:current) - .and_return(transaction) + User.count expect(transaction) - .to receive(:increment) - .at_least(:once) + .to have_received(:increment) + .once + end - User.count + context 'when the query is actually a rails cache hit' do + it 'does not increment the number of executed SQL queries' do + ActiveRecord::Base.connection.cache do + User.count + User.count + end + + expect(transaction) + .to have_received(:increment) + .once + end end end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index 8b54d72d6f7..a1079e54975 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -18,6 +18,7 @@ describe Gitlab::Regex do subject { described_class.environment_name_regex } it { is_expected.to match('foo') } + it { is_expected.to match('a') } it { is_expected.to match('foo-1') } it { is_expected.to match('FOO') } it { is_expected.to match('foo/1') } @@ -25,6 +26,10 @@ describe Gitlab::Regex do it { is_expected.not_to match('9&foo') } it { is_expected.not_to match('foo-^') } it { is_expected.not_to match('!!()()') } + it { is_expected.not_to match('/foo') } + it { is_expected.not_to match('foo/') } + it { is_expected.not_to match('/foo/') } + it { is_expected.not_to match('/') } end describe '.environment_slug_regex' do diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb index 93d538141ce..a6ea07e8b6d 100644 --- a/spec/lib/gitlab/ssh_public_key_spec.rb +++ b/spec/lib/gitlab/ssh_public_key_spec.rb @@ -37,11 +37,60 @@ describe Gitlab::SSHPublicKey, lib: true do end end + describe '.sanitize(key_content)' do + let(:content) { build(:key).key } + + context 'when key has blank space characters' do + it 'removes the extra blank space characters' do + unsanitized = content.insert(100, "\n") + .insert(40, "\r\n") + .insert(30, ' ') + + sanitized = described_class.sanitize(unsanitized) + _, body = sanitized.split + + expect(sanitized).not_to eq(unsanitized) + expect(body).not_to match(/\s/) + end + end + + context "when key doesn't have blank space characters" do + it "doesn't modify the content" do + sanitized = described_class.sanitize(content) + + expect(sanitized).to eq(content) + end + end + + context "when key is invalid" do + it 'returns the original content' do + unsanitized = "ssh-foo any content==" + sanitized = described_class.sanitize(unsanitized) + + expect(sanitized).to eq(unsanitized) + end + end + end + describe '#valid?' do subject { public_key } context 'with a valid SSH key' do - it { is_expected.to be_valid } + where(:factory) do + %i(rsa_key_2048 + rsa_key_4096 + rsa_key_5120 + rsa_key_8192 + dsa_key_2048 + ecdsa_key_256 + ed25519_key_256) + end + + with_them do + let(:key) { attributes_for(factory)[:key] } + + it { is_expected.to be_valid } + end end context 'with an invalid SSH key' do @@ -82,6 +131,9 @@ describe Gitlab::SSHPublicKey, lib: true do where(:factory, :bits) do [ [:rsa_key_2048, 2048], + [:rsa_key_4096, 4096], + [:rsa_key_5120, 5120], + [:rsa_key_8192, 8192], [:dsa_key_2048, 2048], [:ecdsa_key_256, 256], [:ed25519_key_256, 256] @@ -106,8 +158,11 @@ describe Gitlab::SSHPublicKey, lib: true do where(:factory, :fingerprint) do [ - [:rsa_key_2048, '2e:ca:dc:e0:37:29:ed:fc:f0:1d:bf:66:d4:cd:51:b1'], - [:dsa_key_2048, 'bc:c1:a4:be:7e:8c:84:56:b3:58:93:53:c6:80:78:8c'], + [:rsa_key_2048, '58:a8:9d:cd:1f:70:f8:5a:d9:e4:24:8e:da:89:e4:fc'], + [:rsa_key_4096, 'df:73:db:29:3c:a5:32:cf:09:17:7e:8e:9d:de:d7:f7'], + [:rsa_key_5120, 'fe:fa:3a:4d:7d:51:ec:bf:c7:64:0c:96:d0:17:8a:d0'], + [:rsa_key_8192, 'fb:53:7f:e9:2f:f7:17:aa:c8:32:52:06:8e:05:e2:82'], + [:dsa_key_2048, 'c8:85:1e:df:44:0f:20:00:3c:66:57:2b:21:10:5a:27'], [:ecdsa_key_256, '67:a3:a9:7d:b8:e1:15:d4:80:40:21:34:bb:ed:97:38'], [:ed25519_key_256, 'e6:eb:45:8a:3c:59:35:5f:e9:5b:80:12:be:7e:22:73'] ] diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 59eda025108..bcbb9287199 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -463,59 +463,30 @@ describe Notify do end describe 'project access requested' do - context 'for a project in a user namespace' do - let(:project) do - create(:project, :public, :access_requestable) do |project| - project.add_master(project.owner, current_user: project.owner) - end - end - - let(:project_member) do - project.request_access(user) - project.requesters.find_by(user_id: user.id) - end - subject { described_class.member_access_requested_email('project', project_member.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" - - it 'contains all the useful information' do - to_emails = subject.header[:to].addrs - expect(to_emails.size).to eq(1) - expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email) - - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace - is_expected.to have_body_text project_project_members_url(project) - is_expected.to have_body_text project_member.human_access + let(:project) do + create(:project, :public, :access_requestable) do |project| + project.add_master(project.owner) end end - context 'for a project in a group' do - let(:group_owner) { create(:user) } - let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } - let(:project) { create(:project, :public, :access_requestable, namespace: group) } - let(:project_member) do - project.request_access(user) - project.requesters.find_by(user_id: user.id) - end - subject { described_class.member_access_requested_email('project', project_member.id) } + let(:project_member) do + project.request_access(user) + project.requesters.find_by(user_id: user.id) + end + subject { described_class.member_access_requested_email('project', project_member.id, recipient.notification_email) } - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" - it 'contains all the useful information' do - to_emails = subject.header[:to].addrs - expect(to_emails.size).to eq(1) - expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email) + it 'contains all the useful information' do + to_emails = subject.header[:to].addrs.map(&:address) + expect(to_emails).to eq([recipient.notification_email]) - is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" - is_expected.to have_html_escaped_body_text project.name_with_namespace - is_expected.to have_body_text project_project_members_url(project) - is_expected.to have_body_text project_member.human_access - end + is_expected.to have_subject "Request to join the #{project.name_with_namespace} project" + is_expected.to have_html_escaped_body_text project.name_with_namespace + is_expected.to have_body_text project_project_members_url(project) + is_expected.to have_body_text project_member.human_access end end @@ -959,13 +930,16 @@ describe Notify do group.request_access(user) group.requesters.find_by(user_id: user.id) end - subject { described_class.member_access_requested_email('group', group_member.id) } + subject { described_class.member_access_requested_email('group', group_member.id, recipient.notification_email) } it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' it_behaves_like "a user cannot unsubscribe through footer link" it 'contains all the useful information' do + to_emails = subject.header[:to].addrs.map(&:address) + expect(to_emails).to eq([recipient.notification_email]) + is_expected.to have_subject "Request to join the #{group.name} group" is_expected.to have_html_escaped_body_text group.name is_expected.to have_body_text group_group_members_url(group) diff --git a/spec/migrations/README.md b/spec/migrations/README.md index 45cf25b96de..49760fa62b8 100644 --- a/spec/migrations/README.md +++ b/spec/migrations/README.md @@ -89,5 +89,5 @@ end ## Best practices 1. Note that this type of tests do not run within the transaction, we use -a truncation database cleanup strategy. Do not depend on transaction being +a deletion database cleanup strategy. Do not depend on transaction being present. diff --git a/spec/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb index fe4d5b8a279..2fccfb3f12c 100644 --- a/spec/migrations/track_untracked_uploads_spec.rb +++ b/spec/migrations/track_untracked_uploads_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads') describe TrackUntrackedUploads, :migration, :sidekiq do - include TrackUntrackedUploadsHelpers + include MigrationsHelpers::TrackUntrackedUploadsHelpers it 'correctly schedules the follow-up background migration' do Sidekiq::Testing.fake! do diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb index 7c66c98231b..a5ce245c21d 100644 --- a/spec/models/identity_spec.rb +++ b/spec/models/identity_spec.rb @@ -70,5 +70,38 @@ describe Identity do end end end + + context 'after_destroy' do + let!(:user) { create(:user) } + let(:ldap_identity) { create(:identity, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', user: user) } + let(:ldap_user_synced_attributes) { { provider: 'ldapmain', name_synced: true, email_synced: true } } + let(:other_provider_user_synced_attributes) { { provider: 'other', name_synced: true, email_synced: true } } + + describe 'if user synced attributes metadada provider' do + context 'matches the identity provider ' do + it 'removes the user synced attributes' do + user.create_user_synced_attributes_metadata(ldap_user_synced_attributes) + + expect(user.user_synced_attributes_metadata.provider).to eq 'ldapmain' + + ldap_identity.destroy + + expect(user.reload.user_synced_attributes_metadata).to be_nil + end + end + + context 'does not matche the identity provider' do + it 'does not remove the user synced attributes' do + user.create_user_synced_attributes_metadata(other_provider_user_synced_attributes) + + expect(user.user_synced_attributes_metadata.provider).to eq 'other' + + ldap_identity.destroy + + expect(user.reload.user_synced_attributes_metadata.provider).to eq 'other' + end + end + end + end end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 7398fd25aa8..06d26ef89f1 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -12,6 +12,9 @@ describe Key, :mailer do it { is_expected.to validate_presence_of(:key) } it { is_expected.to validate_length_of(:key).is_at_most(5000) } it { is_expected.to allow_value(attributes_for(:rsa_key_2048)[:key]).for(:key) } + it { is_expected.to allow_value(attributes_for(:rsa_key_4096)[:key]).for(:key) } + it { is_expected.to allow_value(attributes_for(:rsa_key_5120)[:key]).for(:key) } + it { is_expected.to allow_value(attributes_for(:rsa_key_8192)[:key]).for(:key) } it { is_expected.to allow_value(attributes_for(:dsa_key_2048)[:key]).for(:key) } it { is_expected.to allow_value(attributes_for(:ecdsa_key_256)[:key]).for(:key) } it { is_expected.to allow_value(attributes_for(:ed25519_key_256)[:key]).for(:key) } @@ -72,16 +75,35 @@ describe Key, :mailer do expect(build(:key)).to be_valid end - it 'accepts a key with newline charecters after stripping them' do - key = build(:key) - key.key = key.key.insert(100, "\n") - key.key = key.key.insert(40, "\r\n") - expect(key).to be_valid - end - it 'rejects the unfingerprintable key (not a key)' do expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end + + where(:factory, :chars, :expected_sections) do + [ + [:key, ["\n", "\r\n"], 3], + [:key, [' ', ' '], 3], + [:key_without_comment, [' ', ' '], 2] + ] + end + + with_them do + let!(:key) { create(factory) } + let!(:original_fingerprint) { key.fingerprint } + + it 'accepts a key with blank space characters after stripping them' do + modified_key = key.key.insert(100, chars.first).insert(40, chars.last) + _, content = modified_key.split + + key.update!(key: modified_key) + + expect(key).to be_valid + expect(key.key.split.size).to eq(expected_sections) + + expect(content).not_to match(/\s/) + expect(original_fingerprint).to eq(key.fingerprint) + end + end end context 'validate it meets key restrictions' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a6d48e369ac..0bc07dc7a85 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -873,6 +873,18 @@ describe Repository do expect(repository.license_key).to be_nil end + it 'returns nil when the commit SHA does not exist' do + allow(repository.head_commit).to receive(:sha).and_return('1' * 40) + + expect(repository.license_key).to be_nil + end + + it 'returns nil when master does not exist' do + repository.rm_branch(user, 'master') + + expect(repository.license_key).to be_nil + end + it 'returns the license key' do repository.create_file(user, 'LICENSE', Licensee::License.new('mit').content, diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 76a6aef39cc..3531de244bd 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -496,6 +496,14 @@ describe User do user2.update_tracked_fields!(request) end.to change { user2.reload.current_sign_in_at } end + + it 'does not write if the DB is in read-only mode' do + expect(Gitlab::Database).to receive(:read_only?).and_return(true) + + expect do + user.update_tracked_fields!(request) + end.not_to change { user.reload.current_sign_in_at } + end end shared_context 'user keys' do @@ -893,6 +901,14 @@ describe User do end end + describe '.find_for_database_authentication' do + it 'strips whitespace from login' do + user = create(:user) + + expect(described_class.find_for_database_authentication({ login: " #{user.username} " })).to eq user + end + end + describe '.find_by_any_email' do it 'finds by primary email' do user = create(:user, email: 'foo@example.com') diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index ff5f207487b..ad3eec88952 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -465,6 +465,72 @@ describe API::Commits do end end + describe 'GET /projects/:id/repository/commits/:sha/refs' do + let(:project) { create(:project, :public, :repository) } + let(:tag) { project.repository.find_tag('v1.1.0') } + let(:commit_id) { tag.dereferenced_target.id } + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/refs" } + + context 'when ref does not exist' do + let(:commit_id) { 'unknown' } + + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 Commit Not Found' } + end + end + + context 'when repository is disabled' do + include_context 'disabled repository' + + it_behaves_like '403 response' do + let(:request) { get api(route, current_user) } + end + end + + context 'for a valid commit' do + it 'returns all refs with no scope' do + get api(route, current_user), per_page: 100 + + refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]} + refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]}) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs) + end + + it 'returns all refs' do + get api(route, current_user), type: 'all', per_page: 100 + + refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]} + refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]}) + + expect(response).to have_gitlab_http_status(200) + expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs) + end + + it 'returns the branch refs' do + get api(route, current_user), type: 'branch', per_page: 100 + + refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]} + + expect(response).to have_gitlab_http_status(200) + expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs) + end + + it 'returns the tag refs' do + get api(route, current_user), type: 'tag', per_page: 100 + + refs = project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]} + + expect(response).to have_gitlab_http_status(200) + expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs) + end + end + end + describe 'GET /projects/:id/repository/commits/:sha' do let(:commit) { project.repository.commit } let(:commit_id) { commit.id } @@ -632,6 +698,7 @@ describe API::Commits do get api(route, current_user) expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers expect(json_response.size).to be >= 1 expect(json_response.first.keys).to include 'diff' end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index c7df6251d74..827f4c04324 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Internal do let(:user) { create(:user) } let(:key) { create(:key, user: user) } - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, :wiki_repo) } let(:secret_token) { Gitlab::Shell.secret_token } let(:gl_repository) { "project-#{project.id}" } let(:reference_counter) { double('ReferenceCounter') } diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 13db40d21a5..e6d7b9fde02 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::Issues, :mailer do +describe API::Issues do set(:user) { create(:user) } set(:project) do create(:project, :public, creator_id: user.id, namespace: user.namespace) @@ -932,18 +932,6 @@ describe API::Issues, :mailer do expect(json_response['error']).to eq('confidential is invalid') end - it "sends notifications for subscribers of newly added labels" do - label = project.labels.first - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - post api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: label.title - end - - should_email(user2) - end - it "returns a 400 bad request if title not given" do post api("/projects/#{project.id}/issues", user), labels: 'label, label2' expect(response).to have_gitlab_http_status(400) @@ -1246,18 +1234,6 @@ describe API::Issues, :mailer do expect(json_response['labels']).to eq([label.title]) end - it "sends notifications for subscribers of newly added labels when issue is updated" do - label = create(:label, title: 'foo', color: '#FFAABB', project: project) - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - put api("/projects/#{project.id}/issues/#{issue.iid}", user), - title: 'updated title', labels: label.title - end - - should_email(user2) - end - it 'removes all labels' do put api("/projects/#{project.id}/issues/#{issue.iid}", user), labels: '' @@ -1380,7 +1356,7 @@ describe API::Issues, :mailer do end describe '/projects/:id/issues/:issue_iid/move' do - let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) } let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } it 'moves an issue' do diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb index 5d01dc37f0e..025165622b7 100644 --- a/spec/requests/api/pages_domains_spec.rb +++ b/spec/requests/api/pages_domains_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' describe API::PagesDomains do - set(:project) { create(:project) } + set(:project) { create(:project, path: 'my.project') } set(:user) { create(:user) } set(:admin) { create(:admin) } @@ -16,6 +16,7 @@ describe API::PagesDomains do let(:route) { "/projects/#{project.id}/pages/domains" } let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" } + let(:route_domain_path) { "/projects/#{project.path_with_namespace.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" } let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" } let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" } let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" } @@ -144,6 +145,16 @@ describe API::PagesDomains do expect(json_response['certificate']).to be_nil end + it 'returns pages domain with project path' do + get api(route_domain_path, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/pages_domain/detail') + expect(json_response['domain']).to eq(pages_domain.domain) + expect(json_response['url']).to eq(pages_domain.url) + expect(json_response['certificate']).to be_nil + end + it 'returns pages domain with a certificate' do get api(route_secure_domain, user) diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb new file mode 100644 index 00000000000..987f6e26971 --- /dev/null +++ b/spec/requests/api/project_import_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' + +describe API::ProjectImport do + let(:export_path) { "#{Dir.tmpdir}/project_export_spec" } + let(:user) { create(:user) } + let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } + let(:namespace) { create(:group) } + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + + namespace.add_owner(user) + end + + after do + FileUtils.rm_rf(export_path, secure: true) + end + + describe 'POST /projects/import' do + it 'schedules an import using a namespace' do + stub_import(namespace) + + post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.id + + expect(response).to have_gitlab_http_status(201) + end + + it 'schedules an import using the namespace path' do + stub_import(namespace) + + post api('/projects/import', user), path: 'test-import', file: fixture_file_upload(file), namespace: namespace.full_path + + expect(response).to have_gitlab_http_status(201) + end + + it 'schedules an import at the user namespace level' do + stub_import(user.namespace) + + post api('/projects/import', user), path: 'test-import2', file: fixture_file_upload(file) + + expect(response).to have_gitlab_http_status(201) + end + + it 'schedules an import at the user namespace level' do + expect_any_instance_of(Project).not_to receive(:import_schedule) + expect(::Projects::CreateService).not_to receive(:new) + + post api('/projects/import', user), namespace: 'nonexistent', path: 'test-import2', file: fixture_file_upload(file) + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Namespace Not Found') + end + + it 'does not schedule an import if the user has no permission to the namespace' do + expect_any_instance_of(Project).not_to receive(:import_schedule) + + post(api('/projects/import', create(:user)), + path: 'test-import3', + file: fixture_file_upload(file), + namespace: namespace.full_path) + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Namespace Not Found') + end + + it 'does not schedule an import if the user uploads no valid file' do + expect_any_instance_of(Project).not_to receive(:import_schedule) + + post api('/projects/import', user), path: 'test-import3', file: './random/test' + + expect(response).to have_gitlab_http_status(400) + expect(json_response['error']).to eq('file is invalid') + end + + def stub_import(namespace) + expect_any_instance_of(Project).to receive(:import_schedule) + expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original + end + end + + describe 'GET /projects/:id/import' do + it 'returns the import status' do + project = create(:project, import_status: 'started') + project.add_master(user) + + get api("/projects/#{project.id}/import", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include('import_status' => 'started') + end + + it 'returns the import status and the error if failed' do + project = create(:project, import_status: 'failed', import_error: 'error') + project.add_master(user) + + get api("/projects/#{project.id}/import", user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to include('import_status' => 'failed', + 'import_error' => 'error') + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 00dd8897e6a..cee93f6ed14 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -7,7 +7,7 @@ describe API::Projects do let(:user3) { create(:user) } let(:admin) { create(:admin) } let(:project) { create(:project, namespace: user.namespace) } - let(:project2) { create(:project, path: 'project2', namespace: user.namespace) } + let(:project2) { create(:project, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } @@ -315,7 +315,7 @@ describe API::Projects do context 'and with all query parameters' do let!(:project5) { create(:project, :public, path: 'gitlab5', namespace: create(:namespace)) } - let!(:project6) { create(:project, :public, path: 'project6', namespace: user.namespace) } + let!(:project6) { create(:project, :public, namespace: user.namespace) } let!(:project7) { create(:project, :public, path: 'gitlab7', namespace: user.namespace) } let!(:project8) { create(:project, path: 'gitlab8', namespace: user.namespace) } let!(:project9) { create(:project, :public, path: 'gitlab9') } diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index ddda5752f0c..9052a18c60b 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -295,7 +295,7 @@ describe API::Search do get api("/projects/#{repo_project.id}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6' end - it_behaves_like 'response is correct', schema: 'public_api/v4/commits' + it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details' end context 'for commits scope with project path as id' do @@ -303,7 +303,7 @@ describe API::Search do get api("/projects/#{CGI.escape(repo_project.full_path)}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6' end - it_behaves_like 'response is correct', schema: 'public_api/v4/commits' + it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details' end context 'for blobs scope' do diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb index 0dd6d673625..0e745c82395 100644 --- a/spec/requests/api/v3/issues_spec.rb +++ b/spec/requests/api/v3/issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::V3::Issues, :mailer do +describe API::V3::Issues do set(:user) { create(:user) } set(:user2) { create(:user) } set(:non_member) { create(:user) } @@ -780,18 +780,6 @@ describe API::V3::Issues, :mailer do expect(json_response['error']).to eq('confidential is invalid') end - it "sends notifications for subscribers of newly added labels" do - label = project.labels.first - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - post v3_api("/projects/#{project.id}/issues", user), - title: 'new issue', labels: label.title - end - - should_email(user2) - end - it "returns a 400 bad request if title not given" do post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2' @@ -1045,18 +1033,6 @@ describe API::V3::Issues, :mailer do expect(json_response['labels']).to eq([label.title]) end - it "sends notifications for subscribers of newly added labels when issue is updated" do - label = create(:label, title: 'foo', color: '#FFAABB', project: project) - label.toggle_subscription(user2, project) - - perform_enqueued_jobs do - put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), - title: 'updated title', labels: label.title - end - - should_email(user2) - end - it 'removes all labels' do put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' @@ -1191,7 +1167,7 @@ describe API::V3::Issues, :mailer do end describe '/projects/:id/issues/:issue_id/move' do - let!(:target_project) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) } let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) } it 'moves an issue' do diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index bf36d3e245a..4c25bd935c6 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -6,7 +6,7 @@ describe API::V3::Projects do let(:user3) { create(:user) } let(:admin) { create(:admin) } let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } + let(:project2) { create(:project, creator_id: user.id, namespace: user.namespace) } let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } let(:project_member) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 942e5b2bb1b..c6fdda203ad 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -150,7 +150,7 @@ describe 'Git HTTP requests' do let(:path) { "/#{wiki.repository.full_path}.git" } context "when the project is public" do - let(:project) { create(:project, :repository, :public, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :public, :wiki_enabled) } it_behaves_like 'pushes require Basic HTTP Authentication' @@ -177,7 +177,7 @@ describe 'Git HTTP requests' do end context 'but the repo is disabled' do - let(:project) { create(:project, :repository, :public, :repository_disabled, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :public, :repository_disabled, :wiki_enabled) } it_behaves_like 'pulls are allowed' it_behaves_like 'pushes are allowed' @@ -198,7 +198,7 @@ describe 'Git HTTP requests' do end context "when the project is private" do - let(:project) { create(:project, :repository, :private, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :private, :wiki_enabled) } it_behaves_like 'pulls require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication' @@ -210,7 +210,7 @@ describe 'Git HTTP requests' do end context 'but the repo is disabled' do - let(:project) { create(:project, :repository, :private, :repository_disabled, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) } it 'allows clones' do download(path, user: user.username, password: user.password) do |response| diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index 5d349f45a33..de829011e58 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -81,7 +81,7 @@ describe 'OpenID Connect requests' do it 'includes all user information and group memberships' do request_user_info - expect(json_response).to eq({ + expect(json_response).to match(a_hash_including({ 'sub' => hashed_subject, 'name' => 'Alice', 'nickname' => 'alice', @@ -90,13 +90,12 @@ describe 'OpenID Connect requests' do 'website' => 'https://example.com', 'profile' => 'http://localhost/alice', 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png", - 'groups' => - if Group.supports_nested_groups? - ['group1', 'group2/group3', 'group2/group3/group4'] - else - ['group1', 'group2/group3'] - end - }) + 'groups' => anything + })) + + expected_groups = %w[group1 group2/group3] + expected_groups << 'group2/group3/group4' if Group.supports_nested_groups? + expect(json_response['groups']).to match_array(expected_groups) end end diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb index 0fec14d0cce..b18e922b063 100644 --- a/spec/requests/rack_attack_global_spec.rb +++ b/spec/requests/rack_attack_global_spec.rb @@ -22,6 +22,7 @@ describe 'Rack Attack global throttles' do let(:url_that_does_not_require_authentication) { '/users/sign_in' } let(:url_that_requires_authentication) { '/dashboard/snippets' } + let(:url_api_internal) { '/api/v4/internal/check' } let(:api_partial_url) { '/todos' } around do |example| @@ -172,6 +173,15 @@ describe 'Rack Attack global throttles' do get url_that_does_not_require_authentication expect(response).to have_http_status 200 end + + context 'when the request is to the api internal endpoints' do + it 'allows requests over the rate limit' do + (1 + requests_per_period).times do + get url_api_internal, secret_token: Gitlab::Shell.secret_token + expect(response).to have_http_status 200 + end + end + end end context 'when the throttle is disabled' do diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 91aefa84d0e..56d025f0176 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -37,6 +37,22 @@ describe UsersController, "routing" do it "to #calendar_activities" do expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') end + + describe 'redirect alias routes' do + include RSpec::Rails::RequestExampleGroup + + it '/u/user1 redirects to /user1' do + expect(get("/u/user1")).to redirect_to('/user1') + end + + it '/u/user1/groups redirects to /user1/groups' do + expect(get("/u/user1/groups")).to redirect_to('/users/user1/groups') + end + + it '/u/user1/projects redirects to /user1/projects' do + expect(get("/u/user1/projects")).to redirect_to('/users/user1/projects') + end + end end # search GET /search(.:format) search#show diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 322c91065e7..c148a98569b 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -232,6 +232,28 @@ describe Issues::MoveService do end end + context 'issue with assignee' do + let(:assignee) { create(:user) } + + before do + old_issue.assignees = [assignee] + end + + it 'preserves assignee with access to the new issue' do + new_project.add_reporter(assignee) + + new_issue = move_service.execute(old_issue, new_project) + + expect(new_issue.assignees).to eq([assignee]) + end + + it 'ignores assignee without access to the new issue' do + new_issue = move_service.execute(old_issue, new_project) + + expect(new_issue.assignees).to be_empty + end + end + context 'notes with references' do before do create(:merge_request, source_project: old_project) diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb index 757c45708b9..9cf6f64a078 100644 --- a/spec/services/members/authorized_destroy_service_spec.rb +++ b/spec/services/members/authorized_destroy_service_spec.rb @@ -21,6 +21,15 @@ describe Members::AuthorizedDestroyService do .to change { Member.count }.from(3).to(2) end + it "doesn't destroy invited project member notification_settings" do + project.add_developer(member_user) + + member = create :project_member, :invited, project: project + + expect { described_class.new(member, member_user).execute } + .not_to change { NotificationSetting.count } + end + it 'destroys invited group member' do group.add_developer(member_user) @@ -29,38 +38,73 @@ describe Members::AuthorizedDestroyService do expect { described_class.new(member, member_user).execute } .to change { Member.count }.from(2).to(1) end + + it "doesn't destroy invited group member notification_settings" do + group.add_developer(member_user) + + member = create :group_member, :invited, group: group + + expect { described_class.new(member, member_user).execute } + .not_to change { NotificationSetting.count } + end + end + + context 'Requested user' do + it "doesn't destroy member notification_settings" do + member = create(:project_member, user: member_user, requested_at: Time.now) + + expect { described_class.new(member, member_user).execute } + .not_to change { NotificationSetting.count } + end end context 'Group member' do - it "unassigns issues and merge requests" do + let(:member) { group.members.find_by(user_id: member_user.id) } + + before do group.add_developer(member_user) + end + it "unassigns issues and merge requests" do issue = create :issue, project: group_project, assignees: [member_user] create :issue, assignees: [member_user] merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user create :merge_request, target_project: project, source_project: project, assignee: member_user - member = group.members.find_by(user_id: member_user.id) - expect { described_class.new(member, member_user).execute } .to change { number_of_assigned_issuables(member_user) }.from(4).to(2) expect(issue.reload.assignee_ids).to be_empty expect(merge_request.reload.assignee_id).to be_nil end + + it 'destroys member notification_settings' do + group.add_developer(member_user) + member = group.members.find_by(user_id: member_user.id) + + expect { described_class.new(member, member_user).execute } + .to change { member_user.notification_settings.count }.by(-1) + end end context 'Project member' do - it "unassigns issues and merge requests" do + let(:member) { project.members.find_by(user_id: member_user.id) } + + before do project.add_developer(member_user) + end + it "unassigns issues and merge requests" do create :issue, project: project, assignees: [member_user] create :merge_request, target_project: project, source_project: project, assignee: member_user - member = project.members.find_by(user_id: member_user.id) - expect { described_class.new(member, member_user).execute } .to change { number_of_assigned_issuables(member_user) }.from(2).to(0) end + + it 'destroys member notification_settings' do + expect { described_class.new(member, member_user).execute } + .to change { member_user.notification_settings.count }.by(-1) + end end end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index a0d0a4fd81b..3a935d98540 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe MergeRequests::BuildService do + using RSpec::Parameterized::TableSyntax include RepoHelpers let(:project) { create(:project, :repository) } @@ -111,6 +112,7 @@ describe MergeRequests::BuildService do context 'one commit in the diff' do let(:commits) { Commit.decorate([commit_1], project) } + let(:commit_description) { commit_1.safe_message.split(/\n+/, 2).last } before do stub_compare @@ -125,7 +127,7 @@ describe MergeRequests::BuildService do end it 'uses the description of the commit as the description of the merge request' do - expect(merge_request.description).to eq(commit_1.safe_message.split(/\n+/, 2).last) + expect(merge_request.description).to eq(commit_description) end context 'merge request already has a description set' do @@ -148,68 +150,32 @@ describe MergeRequests::BuildService do end end - context 'branch starts with issue IID followed by a hyphen' do - let(:source_branch) { "#{issue.iid}-fix-issue" } - - it 'appends "Closes #$issue-iid" to the description' do - expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\n\nCloses ##{issue.iid}") + context 'when the source branch matches an issue' do + where(:issue_tracker, :source_branch, :closing_message) do + :jira | 'FOO-123-fix-issue' | 'Closes FOO-123' + :jira | 'fix-issue' | nil + :custom_issue_tracker | '123-fix-issue' | 'Closes #123' + :custom_issue_tracker | 'fix-issue' | nil + :internal | '123-fix-issue' | 'Closes #123' + :internal | 'fix-issue' | nil end - context 'merge request already has a description set' do - let(:description) { 'Merge request description' } - - it 'appends "Closes #$issue-iid" to the description' do - expect(merge_request.description).to eq("#{description}\n\nCloses ##{issue.iid}") + with_them do + before do + if issue_tracker == :internal + issue.update!(iid: 123) + else + create(:"#{issue_tracker}_service", project: project) + end end - end - context 'commit has no description' do - let(:commits) { Commit.decorate([commit_2], project) } + it 'appends the closing description' do + expected_description = [commit_description, closing_message].compact.join("\n\n") - it 'sets the description to "Closes #$issue-iid"' do - expect(merge_request.description).to eq("Closes ##{issue.iid}") + expect(merge_request.description).to eq(expected_description) end end end - - context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do - let(:source_branch) { '12345-fix-issue' } - - before do - allow(project).to receive(:external_issue_tracker).and_return(false) - allow(project).to receive(:issues_enabled?).and_return(false) - end - - it 'uses the title of the commit as the title of the merge request' do - expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first) - end - - it 'uses the description of the commit as the description of the merge request' do - commit_description = commit_1.safe_message.split(/\n+/, 2).last - - expect(merge_request.description).to eq("#{commit_description}") - end - end - - context 'branch starts with JIRA-formatted external issue IID followed by a hyphen' do - let(:source_branch) { 'EXMPL-12345-fix-issue' } - - before do - allow(project).to receive(:external_issue_tracker).and_return(true) - allow(project).to receive(:issues_enabled?).and_return(false) - allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern) - end - - it 'uses the title of the commit as the title of the merge request' do - expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first) - end - - it 'uses the description of the commit as the description of the merge request and appends the closes text' do - commit_description = commit_1.safe_message.split(/\n+/, 2).last - - expect(merge_request.description).to eq("#{commit_description}\n\nCloses EXMPL-12345") - end - end end context 'more than one commit in the diff' do @@ -239,90 +205,62 @@ describe MergeRequests::BuildService do end end - context 'branch starts with GitLab issue IID followed by a hyphen' do - let(:source_branch) { "#{issue.iid}-fix-issue" } - - it 'sets the title to: Resolves "$issue-title"' do - expect(merge_request.title).to eq("Resolve \"#{issue.title}\"") + context 'when the source branch matches an issue' do + where(:issue_tracker, :source_branch, :title, :closing_message) do + :jira | 'FOO-123-fix-issue' | 'Resolve FOO-123 "Fix issue"' | 'Closes FOO-123' + :jira | 'fix-issue' | 'Fix issue' | nil + :custom_issue_tracker | '123-fix-issue' | 'Resolve #123 "Fix issue"' | 'Closes #123' + :custom_issue_tracker | 'fix-issue' | 'Fix issue' | nil + :internal | '123-fix-issue' | 'Resolve "A bug"' | 'Closes #123' + :internal | 'fix-issue' | 'Fix issue' | nil + :internal | '124-fix-issue' | '124 fix issue' | nil end - context 'when issue is not accessible to user' do + with_them do before do - project.team.truncate - end - - it 'uses branch title as the merge request title' do - expect(merge_request.title).to eq("#{issue.iid} fix issue") + if issue_tracker == :internal + issue.update!(iid: 123) + else + create(:"#{issue_tracker}_service", project: project) + end end - end - - context 'issue does not exist' do - let(:source_branch) { "#{issue.iid.succ}-fix-issue" } - it 'uses the title of the branch as the merge request title' do - expect(merge_request.title).to eq("#{issue.iid.succ} fix issue") + it 'sets the correct title' do + expect(merge_request.title).to eq(title) end - end - - context 'issue is confidential' do - let(:issue_confidential) { true } - it 'uses the title of the branch as the merge request title' do - expect(merge_request.title).to eq("#{issue.iid} fix issue") + it 'sets the closing description' do + expect(merge_request.description).to eq(closing_message) end end end - context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do - let(:source_branch) { '12345-fix-issue' } + context 'when the issue is not accessible to user' do + let(:source_branch) { "#{issue.iid}-fix-issue" } before do - allow(project).to receive(:external_issue_tracker).and_return(false) - allow(project).to receive(:issues_enabled?).and_return(false) + project.team.truncate end - it 'sets the title to the humanized branch title' do - expect(merge_request.title).to eq('12345 fix issue') + it 'uses branch title as the merge request title' do + expect(merge_request.title).to eq("#{issue.iid} fix issue") end - end - describe 'with JIRA enabled' do - before do - allow(project).to receive(:external_issue_tracker).and_return(true) - allow(project).to receive(:issues_enabled?).and_return(false) - allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern) + it 'does not set a description' do + expect(merge_request.description).to be_nil end + end - context 'branch does not start with JIRA-formatted external issue IID' do - let(:source_branch) { 'test-branch' } + context 'when the issue is confidential' do + let(:source_branch) { "#{issue.iid}-fix-issue" } + let(:issue_confidential) { true } - it 'sets the title to the humanized branch title' do - expect(merge_request.title).to eq('Test branch') - end + it 'uses the title of the branch as the merge request title' do + expect(merge_request.title).to eq("#{issue.iid} fix issue") end - context 'branch starts with JIRA-formatted external issue IID' do - let(:source_branch) { 'EXMPL-12345' } - - it 'sets the title to the humanized branch title' do - expect(merge_request.title).to eq('Resolve EXMPL-12345') - end - - it 'appends the closes text' do - expect(merge_request.description).to eq('Closes EXMPL-12345') - end - - context 'followed by hyphenated text' do - let(:source_branch) { 'EXMPL-12345-fix-issue' } - - it 'sets the title to the humanized branch title' do - expect(merge_request.title).to eq('Resolve EXMPL-12345 "Fix issue"') - end - - it 'appends the closes text' do - expect(merge_request.description).to eq('Closes EXMPL-12345') - end - end + it 'does not set a description' do + expect(merge_request.description).to be_nil end end end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 35eb84e5e88..836ffb7cea0 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1307,6 +1307,33 @@ describe NotificationService, :mailer do end describe 'GroupMember' do + let(:added_user) { create(:user) } + + describe '#new_access_request' do + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:developer) { create(:user) } + let!(:group) do + create(:group, :public, :access_requestable) do |group| + group.add_owner(owner) + group.add_master(master) + group.add_developer(developer) + end + end + + before do + reset_delivered_emails! + end + + it 'sends notification to group owners_and_masters' do + group.request_access(added_user) + + should_email(owner) + should_email(master) + should_not_email(developer) + end + end + describe '#decline_group_invite' do let(:creator) { create(:user) } let(:group) { create(:group) } @@ -1328,18 +1355,9 @@ describe NotificationService, :mailer do describe '#new_group_member' do let(:group) { create(:group) } - let(:added_user) { create(:user) } - - def create_member! - GroupMember.create( - group: group, - user: added_user, - access_level: Gitlab::Access::GUEST - ) - end it 'sends a notification' do - create_member! + group.add_guest(added_user) should_only_email(added_user) end @@ -1349,7 +1367,7 @@ describe NotificationService, :mailer do end it 'does not send a notification' do - create_member! + group.add_guest(added_user) should_not_email_anyone end end @@ -1357,8 +1375,42 @@ describe NotificationService, :mailer do end describe 'ProjectMember' do + let(:project) { create(:project) } + set(:added_user) { create(:user) } + + describe '#new_access_request' do + context 'for a project in a user namespace' do + let(:project) do + create(:project, :public, :access_requestable) do |project| + project.add_master(project.owner) + end + end + + it 'sends notification to project owners_and_masters' do + project.request_access(added_user) + + should_only_email(project.owner) + end + end + + context 'for a project in a group' do + let(:group_owner) { create(:user) } + let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } } + let!(:project) { create(:project, :public, :access_requestable, namespace: group) } + + before do + reset_delivered_emails! + end + + it 'sends notification to group owners_and_masters' do + project.request_access(added_user) + + should_only_email(group_owner) + end + end + end + describe '#decline_group_invite' do - let(:project) { create(:project) } let(:member) { create(:user) } before do @@ -1375,19 +1427,12 @@ describe NotificationService, :mailer do end describe '#new_project_member' do - let(:project) { create(:project) } - let(:added_user) { create(:user) } - - def create_member! - create(:project_member, user: added_user, project: project) - end - it do create_member! should_only_email(added_user) end - describe 'when notifications are disabled' do + context 'when notifications are disabled' do before do create_global_setting_for(added_user, :disabled) end @@ -1398,6 +1443,10 @@ describe NotificationService, :mailer do end end end + + def create_member! + create(:project_member, user: added_user, project: project) + end end context 'guest user in private project' do diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb index 9919ec254c6..609d678caea 100644 --- a/spec/services/projects/create_from_template_service_spec.rb +++ b/spec/services/projects/create_from_template_service_spec.rb @@ -4,8 +4,10 @@ describe Projects::CreateFromTemplateService do let(:user) { create(:user) } let(:project_params) do { - path: user.to_param, - template_name: 'rails' + path: user.to_param, + template_name: 'rails', + description: 'project description', + visibility_level: Gitlab::VisibilityLevel::PRIVATE } end @@ -22,5 +24,7 @@ describe Projects::CreateFromTemplateService do expect(project).to be_saved expect(project.scheduled?).to be(true) + expect(project.description).to match('project description') + expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 85de0a14631..5600c9c6ad5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -154,6 +154,22 @@ RSpec.configure do |config| Sidekiq.redis(&:flushall) end + # The :each scope runs "inside" the example, so this hook ensures the DB is in the + # correct state before any examples' before hooks are called. This prevents a + # problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends + # on background migrations being run inline during test setup) can be broken by + # altering Sidekiq behavior in an unrelated spec like so: + # + # around do |example| + # Sidekiq::Testing.fake! do + # example.run + # end + # end + config.before(:context, :migration) do + schema_migrate_down! + end + + # Each example may call `migrate!`, so we must ensure we are migrated down every time config.before(:each, :migration) do schema_migrate_down! end diff --git a/spec/support/factory_girl.rb b/spec/support/factory_bot.rb index c7890e49c66..c7890e49c66 100644 --- a/spec/support/factory_girl.rb +++ b/spec/support/factory_bot.rb diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb index 4315bf5d037..0d8f7a7aae6 100644 --- a/spec/support/features/variable_list_shared_examples.rb +++ b/spec/support/features/variable_list_shared_examples.rb @@ -263,7 +263,7 @@ shared_examples 'variable list' do # We check the first row because it re-sorts to alphabetical order on refresh page.within('.js-ci-variable-list-section') do - expect(find('.js-ci-variable-error-box')).to have_content('Validation failed Variables Duplicate variables: samekey') + expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/) end end end diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb index 128aaaf25fe..8854382dc6b 100644 --- a/spec/support/fixture_helpers.rb +++ b/spec/support/fixture_helpers.rb @@ -1,12 +1,12 @@ module FixtureHelpers - def fixture_file(filename) + def fixture_file(filename, dir: '') return '' if filename.blank? - File.read(expand_fixture_path(filename)) + File.read(expand_fixture_path(filename, dir: dir)) end - def expand_fixture_path(filename) - File.expand_path(Rails.root.join('spec/fixtures/', filename)) + def expand_fixture_path(filename, dir: '') + File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename)) end end diff --git a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb new file mode 100644 index 00000000000..016bcfa9b1b --- /dev/null +++ b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb @@ -0,0 +1,128 @@ +module MigrationsHelpers + module TrackUntrackedUploadsHelpers + PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public') + UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads') + SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system') + UPLOAD_FILENAME = 'image.png'.freeze + FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png') + FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze + + def create_or_update_appearance(logo: false, header_logo: false) + appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo)) + + add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo + add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo + + appearance + end + + def create_group(avatar: false) + index = unique_index(:group) + group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar)) + + add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar + + group + end + + def create_note(attachment: false) + note = notes.create(attachment: (UPLOAD_FILENAME if attachment)) + + add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment + + note + end + + def create_project(avatar: false) + group = create_group + project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar)) + routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works + + add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar + + project + end + + def create_user(avatar: false) + user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100) + + add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar + + user + end + + def unique_index(name = :unnamed) + @unique_index ||= {} + @unique_index[name] ||= 0 + @unique_index[name] += 1 + end + + def add_upload(model, model_type, attachment_type, uploader) + file_path = upload_file_path(model, model_type, attachment_type) + path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '') + create_file(file_path) + + uploads.create!( + size: 1062, + path: path_relative_to_public, + model_id: model.id, + model_type: model_type == 'Group' ? 'Namespace' : model_type, + uploader: uploader, + checksum: FIXTURE_CHECKSUM + ) + end + + def add_markdown_attachment(project, hashed_storage: false) + project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project) + attachment_dir = File.join(project_dir, SecureRandom.hex) + attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME) + project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '') + create_file(attachment_file_path) + + uploads.create!( + size: 1062, + path: project_attachment_path_relative_to_project, + model_id: project.id, + model_type: 'Project', + uploader: 'FileUploader', + checksum: FIXTURE_CHECKSUM + ) + end + + def legacy_project_uploads_dir(project) + namespace = namespaces.find_by(id: project.namespace_id) + File.join(UPLOADS_DIR, namespace.path, project.path) + end + + def hashed_project_uploads_dir(project) + File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa') + end + + def upload_file_path(model, model_type, attachment_type) + dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s) + File.join(dir, UPLOAD_FILENAME) + end + + def upload_dir(model_type, attachment_type) + File.join(SYSTEM_DIR, model_type, attachment_type) + end + + def create_file(path) + File.delete(path) if File.exist?(path) + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.cp(FIXTURE_FILE_PATH, path) + end + + def get_uploads(model, model_type) + uploads.where(model_type: model_type, model_id: model.id) + end + + def get_full_path(project) + routes.find_by(source_id: project.id, source_type: 'Project').path + end + + def ensure_temporary_tracking_table_exists + Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) + end + end +end diff --git a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb index 4e18804b937..9fc2fbef449 100644 --- a/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/custom_attributes_shared_examples.rb @@ -17,12 +17,88 @@ shared_examples 'custom attributes endpoints' do |attributable_name| end end - it 'filters by custom attributes' do - get api("/#{attributable_name}", admin), custom_attributes: { foo: 'foo', bar: 'bar' } + context 'with an authorized user' do + it 'filters by custom attributes' do + get api("/#{attributable_name}", admin), custom_attributes: { foo: 'foo', bar: 'bar' } - expect(response).to have_gitlab_http_status(200) - expect(json_response.size).to be 1 - expect(json_response.first['id']).to eq attributable.id + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to be 1 + expect(json_response.first['id']).to eq attributable.id + end + end + end + + describe "GET /#{attributable_name} with custom attributes" do + before do + other_attributable + end + + context 'with an unauthorized user' do + it 'does not include custom attributes' do + get api("/#{attributable_name}", user), with_custom_attributes: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to be 2 + expect(json_response.first).not_to include 'custom_attributes' + end + end + + context 'with an authorized user' do + it 'does not include custom attributes by default' do + get api("/#{attributable_name}", admin) + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to be 2 + expect(json_response.first).not_to include 'custom_attributes' + expect(json_response.second).not_to include 'custom_attributes' + end + + it 'includes custom attributes if requested' do + get api("/#{attributable_name}", admin), with_custom_attributes: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response.size).to be 2 + + attributable_response = json_response.find { |r| r['id'] == attributable.id } + other_attributable_response = json_response.find { |r| r['id'] == other_attributable.id } + + expect(attributable_response['custom_attributes']).to contain_exactly( + { 'key' => 'foo', 'value' => 'foo' }, + { 'key' => 'bar', 'value' => 'bar' } + ) + + expect(other_attributable_response['custom_attributes']).to eq [] + end + end + end + + describe "GET /#{attributable_name}/:id with custom attributes" do + context 'with an unauthorized user' do + it 'does not include custom attributes' do + get api("/#{attributable_name}/#{attributable.id}", user), with_custom_attributes: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'custom_attributes' + end + end + + context 'with an authorized user' do + it 'does not include custom attributes by default' do + get api("/#{attributable_name}/#{attributable.id}", admin) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).not_to include 'custom_attributes' + end + + it 'includes custom attributes if requested' do + get api("/#{attributable_name}/#{attributable.id}", admin), with_custom_attributes: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response['custom_attributes']).to contain_exactly( + { 'key' => 'foo', 'value' => 'foo' }, + { 'key' => 'bar', 'value' => 'bar' } + ) + end end end @@ -33,14 +109,16 @@ shared_examples 'custom attributes endpoints' do |attributable_name| it_behaves_like 'an unauthorized API user' end - it 'returns all custom attributes' do - get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin) + context 'with an authorized user' do + it 'returns all custom attributes' do + get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to contain_exactly( - { 'key' => 'foo', 'value' => 'foo' }, - { 'key' => 'bar', 'value' => 'bar' } - ) + expect(response).to have_gitlab_http_status(200) + expect(json_response).to contain_exactly( + { 'key' => 'foo', 'value' => 'foo' }, + { 'key' => 'bar', 'value' => 'bar' } + ) + end end end @@ -51,11 +129,13 @@ shared_examples 'custom attributes endpoints' do |attributable_name| it_behaves_like 'an unauthorized API user' end - it 'returns a single custom attribute' do - get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) + context 'with an authorized user' do + it'returns a single custom attribute' do + get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' }) + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' }) + end end end @@ -66,24 +146,26 @@ shared_examples 'custom attributes endpoints' do |attributable_name| it_behaves_like 'an unauthorized API user' end - it 'creates a new custom attribute' do - expect do - put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), value: 'new' - end.to change { attributable.custom_attributes.count }.by(1) + context 'with an authorized user' do + it 'creates a new custom attribute' do + expect do + put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), value: 'new' + end.to change { attributable.custom_attributes.count }.by(1) - expect(response).to have_gitlab_http_status(200) - expect(json_response).to eq({ 'key' => 'new', 'value' => 'new' }) - expect(attributable.custom_attributes.find_by(key: 'new').value).to eq 'new' - end + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq({ 'key' => 'new', 'value' => 'new' }) + expect(attributable.custom_attributes.find_by(key: 'new').value).to eq 'new' + end - it 'updates an existing custom attribute' do - expect do - put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin), value: 'new' - end.not_to change { attributable.custom_attributes.count } + it 'updates an existing custom attribute' do + expect do + put api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin), value: 'new' + end.not_to change { attributable.custom_attributes.count } - expect(response).to have_gitlab_http_status(200) - expect(json_response).to eq({ 'key' => 'foo', 'value' => 'new' }) - expect(custom_attribute1.reload.value).to eq 'new' + expect(response).to have_gitlab_http_status(200) + expect(json_response).to eq({ 'key' => 'foo', 'value' => 'new' }) + expect(custom_attribute1.reload.value).to eq 'new' + end end end @@ -94,13 +176,15 @@ shared_examples 'custom attributes endpoints' do |attributable_name| it_behaves_like 'an unauthorized API user' end - it 'deletes an existing custom attribute' do - expect do - delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) - end.to change { attributable.custom_attributes.count }.by(-1) + context 'with an authorized user' do + it 'deletes an existing custom attribute' do + expect do + delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin) + end.to change { attributable.custom_attributes.count }.by(-1) - expect(response).to have_gitlab_http_status(204) - expect(attributable.custom_attributes.find_by(key: 'foo')).to be_nil + expect(response).to have_gitlab_http_status(204) + expect(attributable.custom_attributes.find_by(key: 'foo')).to be_nil + end end end end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index c275522159c..01321989f01 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -1,5 +1,5 @@ require 'rspec/mocks' -require 'toml' +require 'toml-rb' module TestEnv extend self diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb deleted file mode 100644 index 5752078d2a0..00000000000 --- a/spec/support/track_untracked_uploads_helpers.rb +++ /dev/null @@ -1,20 +0,0 @@ -module TrackUntrackedUploadsHelpers - def uploaded_file - fixture_path = Rails.root.join('spec/fixtures/rails_sample.jpg') - fixture_file_upload(fixture_path) - end - - def ensure_temporary_tracking_table_exists - Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) - end - - def drop_temp_table_if_exists - ActiveRecord::Base.connection.drop_table(:untracked_files_for_uploads) if ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads) - end - - def create_or_update_appearance(attrs) - a = Appearance.first_or_initialize(title: 'foo', description: 'bar') - a.update!(attrs) - a - end -end diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb index b37d6ac831f..1f4053ff9ad 100644 --- a/spec/tasks/gitlab/gitaly_rake_spec.rb +++ b/spec/tasks/gitlab/gitaly_rake_spec.rb @@ -132,7 +132,7 @@ describe 'gitlab:gitaly namespace rake task' do expect { run_rake_task('gitlab:gitaly:storage_config')} .to output(expected_output).to_stdout - parsed_output = TOML.parse(expected_output) + parsed_output = TomlRB.parse(expected_output) config.each do |name, params| expect(parsed_output['storage']).to include({ 'name' => name, 'path' => params['path'] }) end diff --git a/spec/validators/variable_duplicates_validator_spec.rb b/spec/validators/variable_duplicates_validator_spec.rb new file mode 100644 index 00000000000..0b71a67f94d --- /dev/null +++ b/spec/validators/variable_duplicates_validator_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe VariableDuplicatesValidator do + let(:validator) { described_class.new(attributes: [:variables], **options) } + + describe '#validate_each' do + let(:project) { build(:project) } + + subject { validator.validate_each(project, :variables, project.variables) } + + context 'with no scope' do + let(:options) { {} } + let(:variables) { build_list(:ci_variable, 2, project: project) } + + before do + project.variables << variables + end + + it 'does not have any errors' do + subject + + expect(project.errors.empty?).to be true + end + + context 'with duplicates' do + before do + project.variables.build(key: variables.first.key, value: 'dummy_value') + end + + it 'has a duplicate key error' do + subject + + expect(project.errors).to have_key(:variables) + end + end + end + + context 'with a scope attribute' do + let(:options) { { scope: :environment_scope } } + let(:first_variable) { build(:ci_variable, key: 'test_key', environment_scope: '*', project: project) } + let(:second_variable) { build(:ci_variable, key: 'test_key', environment_scope: 'prod', project: project) } + + before do + project.variables << first_variable + project.variables << second_variable + end + + it 'does not have any errors' do + subject + + expect(project.errors.empty?).to be true + end + + context 'with duplicates' do + before do + project.variables.build(key: second_variable.key, value: 'dummy_value', environment_scope: second_variable.environment_scope) + end + + it 'has a duplicate key error' do + subject + + expect(project.errors).to have_key(:variables) + end + end + end + end +end diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb index 7b7a7c1bc44..526ecf75921 100644 --- a/spec/workers/check_gcp_project_billing_worker_spec.rb +++ b/spec/workers/check_gcp_project_billing_worker_spec.rb @@ -6,6 +6,11 @@ describe CheckGcpProjectBillingWorker do subject { described_class.new.perform('token_key') } + before do + allow(described_class).to receive(:get_billing_state) + allow_any_instance_of(described_class).to receive(:update_billing_change_counter) + end + context 'when there is a token in redis' do before do allow(described_class).to receive(:get_session_token).and_return(token) @@ -23,11 +28,8 @@ describe CheckGcpProjectBillingWorker do end it 'stores billing status in redis' do - redis_double = double - expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) - expect(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) - expect(redis_double).to receive(:set).with(described_class.redis_shared_state_key_for(token), anything, anything) + expect(described_class).to receive(:set_billing_state).with(token, true) subject end @@ -48,7 +50,7 @@ describe CheckGcpProjectBillingWorker do context 'when there is no token in redis' do before do - allow_any_instance_of(described_class).to receive(:get_session_token).and_return(nil) + allow(described_class).to receive(:get_session_token).and_return(nil) end it 'does not call the service' do @@ -58,4 +60,57 @@ describe CheckGcpProjectBillingWorker do end end end + + describe 'billing change counter' do + subject { described_class.new.perform('token_key') } + + before do + allow(described_class).to receive(:get_session_token).and_return('bogustoken') + allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid') + allow(described_class).to receive(:set_billing_state) + end + + context 'when previous state was false' do + before do + expect(described_class).to receive(:get_billing_state).and_return(false) + end + + context 'when the current state is false' do + before do + expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([]) + end + + it 'increments the billing change counter' do + expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) + + subject + end + end + + context 'when the current state is true' do + before do + expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) + end + + it 'increments the billing change counter' do + expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) + + subject + end + end + end + + context 'when previous state was true' do + before do + expect(described_class).to receive(:get_billing_state).and_return(true) + expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double]) + end + + it 'increment the billing change counter' do + expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment) + + subject + end + end + end end diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb index 24f8ca67594..76ef57b6b1e 100644 --- a/spec/workers/process_commit_worker_spec.rb +++ b/spec/workers/process_commit_worker_spec.rb @@ -20,6 +20,32 @@ describe ProcessCommitWorker do worker.perform(project.id, -1, commit.to_hash) end + context 'when commit is a merge request merge commit' do + let(:merge_request) do + create(:merge_request, + description: "Closes #{issue.to_reference}", + source_branch: 'feature-merged', + target_branch: 'master', + source_project: project) + end + + let(:commit) do + project.repository.create_branch('feature-merged', 'feature') + + sha = project.repository.merge(user, + merge_request.diff_head_sha, + merge_request, + "Closes #{issue.to_reference}") + project.repository.commit(sha) + end + + it 'it does not close any issues from the commit message' do + expect(worker).not_to receive(:close_issues) + + worker.perform(project.id, user.id, commit.to_hash) + end + end + it 'processes the commit message' do expect(worker).to receive(:process_commit_message).and_call_original @@ -48,11 +74,9 @@ describe ProcessCommitWorker do describe '#process_commit_message' do context 'when pushing to the default branch' do it 'closes issues that should be closed per the commit message' do - allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}") - expect(worker).to receive(:close_issues) - .with(project, user, user, commit, [issue]) + expect(worker).to receive(:close_issues).with(project, user, user, commit, [issue]) worker.process_commit_message(project, commit, user, user, true) end @@ -60,8 +84,7 @@ describe ProcessCommitWorker do context 'when pushing to a non-default branch' do it 'does not close any issues' do - allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}") expect(worker).not_to receive(:close_issues) @@ -102,8 +125,7 @@ describe ProcessCommitWorker do describe '#update_issue_metrics' do it 'updates any existing issue metrics' do - allow(commit).to receive(:safe_message) - .and_return("Closes #{issue.to_reference}") + allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}") worker.update_issue_metrics(commit, user) @@ -113,10 +135,10 @@ describe ProcessCommitWorker do end it "doesn't execute any queries with false conditions" do - allow(commit).to receive(:safe_message) - .and_return("Lorem Ipsum") + allow(commit).to receive(:safe_message).and_return("Lorem Ipsum") - expect { worker.update_issue_metrics(commit, user) }.not_to make_queries_matching(/WHERE (?:1=0|0=1)/) + expect { worker.update_issue_metrics(commit, user) } + .not_to make_queries_matching(/WHERE (?:1=0|0=1)/) end end @@ -128,8 +150,9 @@ describe ProcessCommitWorker do end it 'parses date strings into Time instances' do - commit = worker - .build_commit(project, id: '123', authored_date: Time.now.to_s) + commit = worker.build_commit(project, + id: '123', + authored_date: Time.now.to_s) expect(commit.authored_date).to be_an_instance_of(Time) end |