diff options
author | Felipe Artur <felipefac@gmail.com> | 2019-02-19 11:35:29 -0300 |
---|---|---|
committer | Felipe Artur <felipefac@gmail.com> | 2019-02-19 11:35:29 -0300 |
commit | 648b87315d12d18c92ea14b14ae827480ab3093a (patch) | |
tree | 8c6bab95481bec1c65f0af2fece58b000c3396b7 /spec | |
parent | 52155d8cf8374e9184c2ae834cab761b7520db93 (diff) | |
parent | 0aa64cf80ccd7fda10641af0cd43c4c0a7f3e133 (diff) | |
download | gitlab-ce-648b87315d12d18c92ea14b14ae827480ab3093a.tar.gz |
Merge branch 'master' into issue_51789_part_1
Diffstat (limited to 'spec')
126 files changed, 3008 insertions, 923 deletions
diff --git a/spec/config/application_spec.rb b/spec/config/application_spec.rb new file mode 100644 index 00000000000..01ed81964c3 --- /dev/null +++ b/spec/config/application_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Application do # rubocop:disable RSpec/FilePath + using RSpec::Parameterized::TableSyntax + + FILTERED_PARAM = ActionDispatch::Http::ParameterFilter::FILTERED + + context 'when parameters are logged' do + describe 'rails does not leak confidential parameters' do + def request_for_url(input_url) + env = Rack::MockRequest.env_for(input_url) + env['action_dispatch.parameter_filter'] = described_class.config.filter_parameters + + ActionDispatch::Request.new(env) + end + + where(:input_url, :output_query) do + '/' | {} + '/?safe=1' | { 'safe' => '1' } + '/?private_token=secret' | { 'private_token' => FILTERED_PARAM } + '/?mixed=1&private_token=secret' | { 'mixed' => '1', 'private_token' => FILTERED_PARAM } + '/?note=secret¬eable=1&prefix_note=2' | { 'note' => FILTERED_PARAM, 'noteable' => '1', 'prefix_note' => '2' } + '/?note[note]=secret&target_type=1' | { 'note' => FILTERED_PARAM, 'target_type' => '1' } + '/?safe[note]=secret&target_type=1' | { 'safe' => { 'note' => FILTERED_PARAM }, 'target_type' => '1' } + end + + with_them do + it { expect(request_for_url(input_url).filtered_parameters).to eq(output_query) } + end + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 6b66cbd2651..c934db9e237 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -8,6 +8,17 @@ describe Admin::UsersController do sign_in(admin) end + describe 'GET :id' do + it 'finds a user case-insensitively' do + user = create(:user, username: 'CaseSensitive') + + get :show, params: { id: user.username.downcase } + + expect(response).to be_redirect + expect(response.location).to end_with(user.username) + end + end + describe 'DELETE #user with projects' do let(:project) { create(:project, namespace: user.namespace) } let!(:issue) { create(:issue, author: user) } diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index a07113a6156..cf3b24f50a3 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -52,6 +52,23 @@ describe SendFileUpload do end end + context 'with inline image' do + let(:filename) { 'test.png' } + let(:params) { { disposition: 'inline', attachment: filename } } + + it 'sends a file with inline disposition' do + # Notice the filename= is omitted from the disposition; this is because + # Rails 5 will append this header in send_file + expected_params = { + filename: 'test.png', + disposition: "inline; filename*=UTF-8''test.png" + } + expect(controller).to receive(:send_file).with(uploader.path, expected_params) + + subject + end + end + context 'with attachment' do let(:filename) { 'test.js' } let(:params) { { attachment: filename } } diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 8b176e07bc8..4b164d0aa6b 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -3,11 +3,9 @@ require 'spec_helper' describe Dashboard::MilestonesController do let(:project) { create(:project) } let(:group) { create(:group) } - let(:public_group) { create(:group, :public) } let(:user) { create(:user) } let(:project_milestone) { create(:milestone, project: project) } let(:group_milestone) { create(:milestone, group: group) } - let!(:public_milestone) { create(:milestone, group: public_group) } let(:milestone) do DashboardMilestone.build( [project], @@ -45,6 +43,9 @@ describe Dashboard::MilestonesController do end describe "#index" do + let(:public_group) { create(:group, :public) } + let!(:public_milestone) { create(:milestone, group: public_group) } + render_views it 'returns group and project milestones to which the user belongs' do @@ -74,10 +75,10 @@ describe Dashboard::MilestonesController do expect(response.body).not_to include(project_milestone.title) end - it 'should contain group and project milestones to which the user belongs to' do + it 'should show counts of group and project milestones to which the user belongs to' do get :index - expect(response.body).to include("Open\n<span class=\"badge badge-pill\">3</span>") + expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>") expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>") end end diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb index 360030102e0..ef23ffaa843 100644 --- a/spec/controllers/groups/clusters_controller_spec.rb +++ b/spec/controllers/groups/clusters_controller_spec.rb @@ -453,7 +453,7 @@ describe Groups::ClustersController do end context 'when domain is invalid' do - let(:domain) { 'not-a-valid-domain' } + let(:domain) { 'http://not-a-valid-domain' } it 'should not update cluster attributes' do go diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 5cb284e7e2d..dca67c18caa 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -37,6 +37,46 @@ describe HelpController do expect(assigns[:help_index]).to eq '[external](https://some.external.link)' end end + + context 'when relative url with external on same line' do + it 'prefix it with /help/' do + stub_readme("[API](api/README.md) [external](https://some.external.link)") + + get :index + + expect(assigns[:help_index]).to eq '[API](/help/api/README.md) [external](https://some.external.link)' + end + end + + context 'when relative url with http:// in query' do + it 'prefix it with /help/' do + stub_readme("[API](api/README.md?go=https://example.com/)") + + get :index + + expect(assigns[:help_index]).to eq '[API](/help/api/README.md?go=https://example.com/)' + end + end + + context 'when mailto URL' do + it 'do not change it' do + stub_readme("[report bug](mailto:bugs@example.com)") + + get :index + + expect(assigns[:help_index]).to eq '[report bug](mailto:bugs@example.com)' + end + end + + context 'when protocol-relative link' do + it 'do not change it' do + stub_readme("[protocol-relative](//example.com)") + + get :index + + expect(assigns[:help_index]).to eq '[protocol-relative](//example.com)' + end + end end describe 'GET #show' do diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb index 5ba64ab3eed..8cbec79095f 100644 --- a/spec/controllers/import/gitea_controller_spec.rb +++ b/spec/controllers/import/gitea_controller_spec.rb @@ -40,4 +40,12 @@ describe Import::GiteaController do end end end + + describe "GET realtime_changes" do + it_behaves_like 'a GitHub-ish import controller: GET realtime_changes' do + before do + assign_host_url + end + end + end end diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index bca5f3f6589..162dff98ec5 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -60,4 +60,8 @@ describe Import::GithubController do describe "POST create" do it_behaves_like 'a GitHub-ish import controller: POST create' end + + describe "GET realtime_changes" do + it_behaves_like 'a GitHub-ish import controller: GET realtime_changes' + end end diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index a6017d8e5e6..e85f32d6e30 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -4,10 +4,11 @@ describe Projects::MergeRequests::DiffsController do include ProjectForksHelper let(:project) { create(:project, :repository) } - let(:user) { project.owner } + let(:user) { create(:user) } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } before do + project.add_maintainer(user) sign_in(user) end @@ -114,16 +115,6 @@ describe Projects::MergeRequests::DiffsController do expect(paths).to include(existing_path) end end - - context 'when the path does not exist in the diff' do - before do - diff_for_path(old_path: 'files/ruby/nopen.rb', new_path: 'files/ruby/nopen.rb') - end - - it 'returns a 404' do - expect(response).to have_gitlab_http_status(404) - end - end end context 'when the user cannot view the merge request' do diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb index 8b7f7587701..ffb9867a203 100644 --- a/spec/controllers/projects/pages_domains_controller_spec.rb +++ b/spec/controllers/projects/pages_domains_controller_spec.rb @@ -23,12 +23,27 @@ describe Projects::PagesDomainsController do end describe 'GET show' do - it "displays the 'show' page" do + def make_request get(:show, params: request_params.merge(id: pages_domain.domain)) + end + it "displays the 'show' page" do + make_request expect(response).to have_gitlab_http_status(200) expect(response).to render_template('show') end + + context 'when user is developer' do + before do + project.add_developer(user) + end + + it 'renders 404 page' do + make_request + + expect(response).to have_gitlab_http_status(404) + end + end end describe 'GET new' do diff --git a/spec/factories/import_state.rb b/spec/factories/import_states.rb index d6de26dccbc..d6de26dccbc 100644 --- a/spec/factories/import_state.rb +++ b/spec/factories/import_states.rb diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 0d8c26a2ee9..70c34f8640b 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -62,10 +62,4 @@ FactoryBot.define do project_key: 'jira-key' ) end - - factory :hipchat_service do - project - type 'HipchatService' - token 'test_token' - end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index a47bd7cafca..1d2b724a5e5 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -73,11 +73,16 @@ FactoryBot.define do end after(:create) do |user, evaluator| - user.identities << create( - :identity, + identity_attrs = { provider: evaluator.provider, extern_uid: evaluator.extern_uid - ) + } + + if evaluator.respond_to?(:saml_provider) + identity_attrs[:saml_provider] = evaluator.saml_provider + end + + user.identities << create(:identity, identity_attrs) end end diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 51f158d3045..fd8677feab5 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -126,7 +126,7 @@ describe 'Dashboard Todos' do it 'shows you added a todo message' do page.within('.js-todos-all') do - expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") + expect(page).to have_content("You added a todo for issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end end @@ -140,7 +140,7 @@ describe 'Dashboard Todos' do it 'shows you mentioned yourself message' do page.within('.js-todos-all') do - expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") + expect(page).to have_content("You mentioned yourself on issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end end @@ -154,7 +154,7 @@ describe 'Dashboard Todos' do it 'shows you directly addressed yourself message' do page.within('.js-todos-all') do - expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") + expect(page).to have_content("You directly addressed yourself on issue #{issue.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end end @@ -170,7 +170,7 @@ describe 'Dashboard Todos' do it 'shows you set yourself as an approver message' do page.within('.js-todos-all') do - expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") + expect(page).to have_content("You set yourself as an approver for merge request #{merge_request.to_reference(full: true)}") expect(page).not_to have_content('to yourself') end end diff --git a/spec/features/groups/labels/create_spec.rb b/spec/features/groups/labels/create_spec.rb new file mode 100644 index 00000000000..f5062a65321 --- /dev/null +++ b/spec/features/groups/labels/create_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Create a group label' do + let(:user) { create(:user) } + let(:group) { create(:group) } + + before do + group.add_owner(user) + sign_in(user) + visit group_labels_path(group) + end + + it 'creates a new label' do + click_link 'New label' + fill_in 'Title', with: 'test-label' + click_button 'Create label' + + expect(page).to have_content 'test-label' + expect(current_path).to eq(group_labels_path(group)) + end +end diff --git a/spec/features/groups/labels/index_spec.rb b/spec/features/groups/labels/index_spec.rb index 0ce7dad4040..62308d3b518 100644 --- a/spec/features/groups/labels/index_spec.rb +++ b/spec/features/groups/labels/index_spec.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + require 'spec_helper' describe 'Group labels' do let(:user) { create(:user) } let(:group) { create(:group) } let!(:label) { create(:group_label, group: group) } + let!(:label2) { create(:group_label) } before do group.add_owner(user) @@ -11,7 +14,16 @@ describe 'Group labels' do visit group_labels_path(group) end - it 'label has edit button', :js do + it 'shows labels that belong to the group' do + expect(page).to have_content(label.name) + expect(page).not_to have_content(label2.name) + end + + it 'shows a new label button' do + expect(page).to have_link('New label') + end + + it 'shows an edit label button', :js do expect(page).to have_selector('.label-action.edit') end end diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb index d19408ee87f..c837a6752f9 100644 --- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb +++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb @@ -222,6 +222,11 @@ describe 'Merge request > User creates image diff notes', :js do end def create_image_diff_note + expand_text = 'Click to expand it.' + page.all('a', text: expand_text).each do |element| + element.click + end + find('.js-add-image-diff-note-button', match: :first).click find('.diff-content .note-textarea').native.send_keys('image diff test comment') click_button 'Comment' diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb index 38b4e4a6d1b..ea2bb1503bb 100644 --- a/spec/features/merge_request/user_creates_merge_request_spec.rb +++ b/spec/features/merge_request/user_creates_merge_request_spec.rb @@ -8,6 +8,8 @@ describe "User creates a merge request", :js do let(:user) { create(:user) } before do + stub_feature_flags(approval_rules: false) + project.add_maintainer(user) sign_in(user) end diff --git a/spec/features/projects/branches/user_views_branches_spec.rb b/spec/features/projects/branches/user_views_branches_spec.rb index 62ae793151c..777d30fdffd 100644 --- a/spec/features/projects/branches/user_views_branches_spec.rb +++ b/spec/features/projects/branches/user_views_branches_spec.rb @@ -15,6 +15,8 @@ describe "User views branches" do it "shows branches" do expect(page).to have_content("Branches").and have_content("master") + + expect(page.all(".graph-side")).to all( have_content(/\d+/) ) end end diff --git a/spec/features/projects/services/disable_triggers_spec.rb b/spec/features/projects/services/disable_triggers_spec.rb index 1a13fe03a67..65b597da269 100644 --- a/spec/features/projects/services/disable_triggers_spec.rb +++ b/spec/features/projects/services/disable_triggers_spec.rb @@ -14,10 +14,11 @@ describe 'Disable individual triggers' do end context 'service has multiple supported events' do - let(:service_name) { 'HipChat' } + let(:service_name) { 'JIRA' } it 'shows trigger checkboxes' do - event_count = HipchatService.supported_events.count + event_count = JiraService.supported_events.count + expect(event_count).to be > 1 expect(page).to have_content "Trigger" expect(page).to have_css(checkbox_selector, count: event_count) diff --git a/spec/features/projects/services/user_activates_hipchat_spec.rb b/spec/features/projects/services/user_activates_hipchat_spec.rb deleted file mode 100644 index 2f5313c91f9..00000000000 --- a/spec/features/projects/services/user_activates_hipchat_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require 'spec_helper' - -describe 'User activates HipChat' do - let(:project) { create(:project) } - let(:user) { create(:user) } - - before do - project.add_maintainer(user) - sign_in(user) - - visit(project_settings_integrations_path(project)) - - click_link('HipChat') - end - - context 'with standart settings' do - it 'activates service' do - check('Active') - fill_in('Room', with: 'gitlab') - fill_in('Token', with: 'verySecret') - click_button('Save') - - expect(page).to have_content('HipChat activated.') - end - end - - context 'with custom settings' do - it 'activates service' do - check('Active') - fill_in('Room', with: 'gitlab_custom') - fill_in('Token', with: 'secretCustom') - fill_in('Server', with: 'https://chat.example.com') - click_button('Save') - - expect(page).to have_content('HipChat activated.') - end - end -end diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb index e9c8cf0fe34..b0a838a7d2b 100644 --- a/spec/features/projects/services/user_views_services_spec.rb +++ b/spec/features/projects/services/user_views_services_spec.rb @@ -14,7 +14,6 @@ describe 'User views services' do it 'shows the list of available services' do expect(page).to have_content('Project services') expect(page).to have_content('Campfire') - expect(page).to have_content('HipChat') expect(page).to have_content('Assembla') expect(page).to have_content('Pushover') expect(page).to have_content('Atlassian Bamboo') @@ -22,5 +21,7 @@ describe 'User views services' do expect(page).to have_content('Asana') expect(page).to have_content('Irker (IRC gateway)') expect(page).to have_content('Packagist') + expect(page).to have_content('Mattermost') + expect(page).to have_content('Slack') end end diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb index df33d215602..dc0278370aa 100644 --- a/spec/features/projects/settings/forked_project_settings_spec.rb +++ b/spec/features/projects/settings/forked_project_settings_spec.rb @@ -7,6 +7,7 @@ describe 'Projects > Settings > For a forked project', :js do let(:forked_project) { fork_project(original_project, user) } before do + stub_feature_flags(approval_rules: false) original_project.add_maintainer(user) forked_project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 3b469fee867..49244c53a91 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe 'Projects > Wiki > User previews markdown changes', :js do let(:user) { create(:user) } let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } + let(:wiki_page) { create(:wiki_page, wiki: project.wiki, attrs: { title: 'home', content: '[some link](other-page)' }) } let(:wiki_content) do <<-HEREDOC [regular link](regular) @@ -18,9 +19,7 @@ describe 'Projects > Wiki > User previews markdown changes', :js do sign_in(user) - visit project_path(project) - find('.shortcuts-wiki').click - click_link "Create your first page" + visit project_wiki_path(project, wiki_page) end context "while creating a new wiki page" do diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 48a0d675f2d..b1a7f167977 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -44,13 +44,7 @@ describe "User creates wiki page" do end it "shows non-escaped link in the pages list", :js do - click_link("New page") - - page.within("#modal-new-wiki") do - fill_in(:new_wiki_path, with: "one/two/three-test") - - click_on("Create page") - end + fill_in(:wiki_title, with: "one/two/three-test") page.within(".wiki-form") do fill_in(:wiki_content, with: "wiki content") diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index f76e577b0d6..dbf8af3e5bb 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -26,12 +26,7 @@ describe 'User updates wiki page' do end it 'updates a page that has a path', :js do - click_on('New page') - - page.within('#modal-new-wiki') do - fill_in(:new_wiki_path, with: 'one/two/three-test') - click_on('Create page') - end + fill_in(:wiki_title, with: 'one/two/three-test') page.within '.wiki-form' do fill_in(:wiki_content, with: 'wiki content') diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb index d4691b669c1..6e28ec0d7b2 100644 --- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -22,12 +22,7 @@ describe 'User views a wiki page' do visit(project_wikis_path(project)) click_link "Create your first page" - click_on('New page') - - page.within('#modal-new-wiki') do - fill_in(:new_wiki_path, with: 'one/two/three-test') - click_on('Create page') - end + fill_in(:wiki_title, with: 'one/two/three-test') page.within('.wiki-form') do fill_in(:wiki_content, with: 'wiki content') diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index bc36c6f948f..dbf0d427976 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -4,6 +4,10 @@ describe 'Project' do include ProjectForksHelper include MobileHelpers + before do + stub_feature_flags(approval_rules: false) + end + describe 'creating from template' do let(:user) { create(:user) } let(:template) { Gitlab::ProjectTemplate.find(:rails) } diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index bfe11ddf673..957c3cfc583 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -49,6 +49,34 @@ describe 'Signup' do expect(page).to have_content("Please create a username with only alphanumeric characters.") end + + it 'shows an error border if the username contains emojis' do + simulate_input('#new_user_username', 'ehsan😀') + + expect(find('.username')).to have_css '.gl-field-error-outline' + end + + it 'shows an error message if the username contains emojis' do + simulate_input('#new_user_username', 'ehsan😀') + + expect(page).to have_content("Invalid input, please avoid emojis") + end + end + + describe 'user\'s full name validation', :js do + before do + visit root_path + click_link 'Register' + simulate_input('#new_user_name', 'Ehsan 🦋') + end + + it 'shows an error border if the user\'s fullname contains an emoji' do + expect(find('.name')).to have_css '.gl-field-error-outline' + end + + it 'shows an error message if the username contains emojis' do + expect(page).to have_content("Invalid input, please avoid emojis") + end end context 'with no errors' do diff --git a/spec/fixtures/api/schemas/entities/diff_viewer.json b/spec/fixtures/api/schemas/entities/diff_viewer.json index 81325cd86c6..ae0fb32d3ac 100644 --- a/spec/fixtures/api/schemas/entities/diff_viewer.json +++ b/spec/fixtures/api/schemas/entities/diff_viewer.json @@ -14,6 +14,17 @@ "string", "null" ] + }, + "error_message": { + "type": [ + "string", + "null" + ] + }, + "collapsed": { + "type": [ + "boolean" + ] } }, "additionalProperties": false diff --git a/spec/fixtures/api/schemas/public_api/v4/group_labels.json b/spec/fixtures/api/schemas/public_api/v4/group_labels.json index f6c327abfdd..fbde45f2904 100644 --- a/spec/fixtures/api/schemas/public_api/v4/group_labels.json +++ b/spec/fixtures/api/schemas/public_api/v4/group_labels.json @@ -6,6 +6,7 @@ "id" : { "type": "integer" }, "name" : { "type": "string "}, "color" : { "type": "string "}, + "text_color" : { "type": "string "}, "description" : { "type": "string "}, "open_issues_count" : { "type": "integer "}, "closed_issues_count" : { "type": "integer "}, diff --git a/spec/fixtures/security-reports/master/gl-container-scanning-report.json b/spec/fixtures/security-reports/master/gl-container-scanning-report.json index 68c6099836b..03dfc647162 100644 --- a/spec/fixtures/security-reports/master/gl-container-scanning-report.json +++ b/spec/fixtures/security-reports/master/gl-container-scanning-report.json @@ -1,11 +1,14 @@ { "image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff", "unapproved": [ - "CVE-2017-18018", - "CVE-2016-2781", - "CVE-2017-12424", - "CVE-2007-5686", - "CVE-2013-4235" + "CVE-2017-18269", + "CVE-2017-16997", + "CVE-2018-1000001", + "CVE-2016-10228", + "CVE-2018-18520", + "CVE-2010-4052", + "CVE-2018-16869", + "CVE-2018-18311" ], "vulnerabilities": [ { @@ -87,6 +90,16 @@ "link": "https://security-tracker.debian.org/tracker/CVE-2018-18311", "severity": "Unknown", "fixedby": "5.24.1-3+deb9u5" + }, + { + "featurename": "foo", + "featureversion": "1.3", + "vulnerability": "CVE-2018-666", + "namespace": "debian:9", + "description": "Foo has a vulnerability nobody cares about and whitelist.", + "link": "https://security-tracker.debian.org/tracker/CVE-2018-666", + "severity": "Unknown", + "fixedby": "1.4" } ] } diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb new file mode 100644 index 00000000000..e3a34762b62 --- /dev/null +++ b/spec/graphql/resolvers/base_resolver_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Resolvers::BaseResolver do + include GraphqlHelpers + + let(:resolver) do + Class.new(described_class) do + def resolve(**args) + [args, args] + end + end + end + + describe '.single' do + it 'returns a subclass from the resolver' do + expect(resolver.single.superclass).to eq(resolver) + end + + it 'returns the same subclass every time' do + expect(resolver.single.object_id).to eq(resolver.single.object_id) + end + + it 'returns a resolver that gives the first result from the original resolver' do + result = resolve(resolver.single, args: { test: 1 }) + + expect(result).to eq(test: 1) + end + end +end diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb index 1a54ab540fc..66de372e9fe 100644 --- a/spec/graphql/resolvers/issues_resolver_spec.rb +++ b/spec/graphql/resolvers/issues_resolver_spec.rb @@ -33,6 +33,10 @@ describe Resolvers::IssuesResolver do expect(resolve_issues).to contain_exactly(issue, issue2) end + it 'finds a specific issue with iid' do + expect(resolve_issues(iid: issue.iid)).to contain_exactly(issue) + end + it 'finds a specific issue with iids' do expect(resolve_issues(iids: issue.iid)).to contain_exactly(issue) end diff --git a/spec/graphql/resolvers/merge_request_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index 73993b3a039..ab3c426b2cd 100644 --- a/spec/graphql/resolvers/merge_request_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Resolvers::MergeRequestResolver do +describe Resolvers::MergeRequestsResolver do include GraphqlHelpers set(:project) { create(:project, :repository) } @@ -16,9 +16,17 @@ describe Resolvers::MergeRequestResolver do let(:other_iid) { other_merge_request.iid } describe '#resolve' do - it 'batch-resolves merge requests by target project full path and IID' do + it 'batch-resolves by target project full path and individual IID' do result = batch(max_queries: 2) do - [resolve_mr(project, iid_1), resolve_mr(project, iid_2)] + resolve_mr(project, iid: iid_1) + resolve_mr(project, iid: iid_2) + end + + expect(result).to contain_exactly(merge_request_1, merge_request_2) + end + + it 'batch-resolves by target project full path and IIDS' do + result = batch(max_queries: 2) do + resolve_mr(project, iids: [iid_1, iid_2]) end expect(result).to contain_exactly(merge_request_1, merge_request_2) @@ -26,20 +34,28 @@ describe Resolvers::MergeRequestResolver do it 'can batch-resolve merge requests from different projects' do result = batch(max_queries: 3) do - [resolve_mr(project, iid_1), resolve_mr(project, iid_2), resolve_mr(other_project, other_iid)] + resolve_mr(project, iid: iid_1) + + resolve_mr(project, iid: iid_2) + + resolve_mr(other_project, iid: other_iid) end expect(result).to contain_exactly(merge_request_1, merge_request_2, other_merge_request) end - it 'resolves an unknown iid to nil' do - result = batch { resolve_mr(project, -1) } + it 'resolves an unknown iid to be empty' do + result = batch { resolve_mr(project, iid: -1) } + + expect(result).to be_empty + end + + it 'resolves empty iids to be empty' do + result = batch { resolve_mr(project, iids: []) } - expect(result).to be_nil + expect(result).to be_empty end end - def resolve_mr(project, iid) - resolve(described_class, obj: project, args: { iid: iid }) + def resolve_mr(project, args) + resolve(described_class, obj: project, args: args) end end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 01d71abfac9..e8f1c84f8d6 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -6,12 +6,18 @@ describe GitlabSchema.types['Project'] do it { expect(described_class.graphql_name).to eq('Project') } describe 'nested merge request' do + it { expect(described_class).to have_graphql_field(:merge_requests) } it { expect(described_class).to have_graphql_field(:merge_request) } it 'authorizes the merge request' do expect(described_class.fields['mergeRequest']) .to require_graphql_authorizations(:read_merge_request) end + + it 'authorizes the merge requests' do + expect(described_class.fields['mergeRequests']) + .to require_graphql_authorizations(:read_merge_request) + end end describe 'nested issues' do diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb index af4931e3370..6e8c13db9fe 100644 --- a/spec/helpers/import_helper_spec.rb +++ b/spec/helpers/import_helper_spec.rb @@ -39,59 +39,12 @@ describe ImportHelper do end end - describe '#provider_project_link' do - context 'when provider is "github"' do - let(:github_server_url) { nil } - let(:provider) { OpenStruct.new(name: 'github', url: github_server_url) } + describe '#provider_project_link_url' do + let(:full_path) { '/repo/path' } + let(:host_url) { 'http://provider.com/' } - before do - stub_omniauth_setting(providers: [provider]) - end - - context 'when provider does not specify a custom URL' do - it 'uses default GitHub URL' do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.com/octocat/Hello-World"') - end - end - - context 'when provider specify a custom URL' do - let(:github_server_url) { 'https://github.company.com' } - - it 'uses custom URL' do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.company.com/octocat/Hello-World"') - end - end - - context "when custom URL contains a '/' char at the end" do - let(:github_server_url) { 'https://github.company.com/' } - - it "doesn't render double slash" do - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.company.com/octocat/Hello-World"') - end - end - - context 'when provider is missing' do - it 'uses the default URL' do - allow(Gitlab.config.omniauth).to receive(:providers).and_return([]) - - expect(helper.provider_project_link('github', 'octocat/Hello-World')) - .to include('href="https://github.com/octocat/Hello-World"') - end - end - end - - context 'when provider is "gitea"' do - before do - assign(:gitea_host_url, 'https://try.gitea.io/') - end - - it 'uses given host' do - expect(helper.provider_project_link('gitea', 'octocat/Hello-World')) - .to include('href="https://try.gitea.io/octocat/Hello-World"') - end + it 'appends repo full path to provider host url' do + expect(helper.provider_project_link_url(host_url, full_path)).to match('http://provider.com/repo/path') end end end diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index a2cbc0f3c72..5abdfe695d0 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -68,6 +68,32 @@ describe('diffs/components/app', () => { }); }); + describe('resizable', () => { + afterEach(() => { + localStorage.removeItem('mr_tree_list_width'); + }); + + it('sets initial width when no localStorage has been set', () => { + createComponent(); + + expect(vm.vm.treeWidth).toEqual(320); + }); + + it('sets initial width to localStorage size', () => { + localStorage.setItem('mr_tree_list_width', '200'); + + createComponent(); + + expect(vm.vm.treeWidth).toEqual(200); + }); + + it('sets width of tree list', () => { + createComponent(); + + expect(vm.find('.js-diff-tree-list').element.style.width).toEqual('320px'); + }); + }); + describe('empty state', () => { it('renders empty state when no diff files exist', () => { createComponent(); diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js index 9e158327a77..a1bb51963d6 100644 --- a/spec/javascripts/diffs/components/diff_content_spec.js +++ b/spec/javascripts/diffs/components/diff_content_spec.js @@ -6,6 +6,7 @@ import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants'; import '~/behaviors/markdown/render_gfm'; import diffFileMockData from '../mock_data/diff_file'; import discussionsMockData from '../mock_data/diff_discussions'; +import { diffViewerModes } from '~/ide/constants'; describe('DiffContent', () => { const Component = Vue.extend(DiffContentComponent); @@ -52,26 +53,39 @@ describe('DiffContent', () => { describe('empty files', () => { beforeEach(() => { - vm.diffFile.empty = true; vm.diffFile.highlighted_diff_lines = []; vm.diffFile.parallel_diff_lines = []; }); - it('should render a message', done => { + it('should render a no preview message if viewer returns no preview', done => { + vm.diffFile.viewer.name = diffViewerModes.no_preview; vm.$nextTick(() => { const block = vm.$el.querySelector('.diff-viewer .nothing-here-block'); expect(block).not.toBe(null); - expect(block.textContent.trim()).toContain('Empty file'); + expect(block.textContent.trim()).toContain('No preview for this file type'); + + done(); + }); + }); + + it('should render a not diffable message if viewer returns not diffable', done => { + vm.diffFile.viewer.name = diffViewerModes.not_diffable; + vm.$nextTick(() => { + const block = vm.$el.querySelector('.diff-viewer .nothing-here-block'); + + expect(block).not.toBe(null); + expect(block.textContent.trim()).toContain( + 'This diff was suppressed by a .gitattributes entry', + ); done(); }); }); it('should not render multiple messages', done => { - vm.diffFile.mode_changed = true; vm.diffFile.b_mode = '100755'; - vm.diffFile.viewer.name = 'mode_changed'; + vm.diffFile.viewer.name = diffViewerModes.mode_changed; vm.$nextTick(() => { expect(vm.$el.querySelectorAll('.nothing-here-block').length).toBe(1); @@ -81,6 +95,7 @@ describe('DiffContent', () => { }); it('should not render diff table', done => { + vm.diffFile.viewer.name = diffViewerModes.no_preview; vm.$nextTick(() => { expect(vm.$el.querySelector('table')).toBe(null); @@ -157,6 +172,7 @@ describe('DiffContent', () => { vm.diffFile.new_sha = 'DEF'; vm.diffFile.old_path = 'test.abc'; vm.diffFile.old_sha = 'ABC'; + vm.diffFile.viewer.name = diffViewerModes.added; vm.$nextTick(() => { expect(el.querySelectorAll('.js-diff-inline-view').length).toEqual(0); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 787a81fd88f..005a4751ea1 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -4,15 +4,15 @@ import diffsModule from '~/diffs/store/modules'; import notesModule from '~/notes/stores/modules'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import diffDiscussionsMockData from '../mock_data/diff_discussions'; +import { diffViewerModes } from '~/ide/constants'; Vue.use(Vuex); -const discussionFixture = 'merge_requests/diff_discussion.json'; - describe('diff_file_header', () => { let vm; let props; - const diffDiscussionMock = getJSONFixture(discussionFixture)[0]; + const diffDiscussionMock = diffDiscussionsMockData; const Component = Vue.extend(DiffFileHeader); const store = new Vuex.Store({ @@ -303,13 +303,13 @@ describe('diff_file_header', () => { }); it('displays old and new path if the file was renamed', () => { - props.diffFile.renamed_file = true; + props.diffFile.viewer.name = diffViewerModes.renamed; vm = mountComponentWithStore(Component, { props, store }); expect(filePaths()).toHaveLength(2); - expect(filePaths()[0]).toHaveText(props.diffFile.old_path); - expect(filePaths()[1]).toHaveText(props.diffFile.new_path); + expect(filePaths()[0]).toHaveText(props.diffFile.old_path_html); + expect(filePaths()[1]).toHaveText(props.diffFile.new_path_html); }); }); @@ -319,14 +319,12 @@ describe('diff_file_header', () => { const button = vm.$el.querySelector('.btn-clipboard'); expect(button).not.toBe(null); - expect(button.dataset.clipboardText).toBe( - '{"text":"files/ruby/popen.rb","gfm":"`files/ruby/popen.rb`"}', - ); + expect(button.dataset.clipboardText).toBe('{"text":"CHANGELOG.rb","gfm":"`CHANGELOG.rb`"}'); }); describe('file mode', () => { it('it displays old and new file mode if it changed', () => { - props.diffFile.mode_changed = true; + props.diffFile.viewer.name = diffViewerModes.mode_changed; vm = mountComponentWithStore(Component, { props, store }); @@ -338,7 +336,7 @@ describe('diff_file_header', () => { }); it('does not display the file mode if it has not changed', () => { - props.diffFile.mode_changed = false; + props.diffFile.viewer.name = diffViewerModes.text; vm = mountComponentWithStore(Component, { props, store }); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 1af49282c36..65a1c9b8f15 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import DiffFileComponent from '~/diffs/components/diff_file.vue'; +import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; import store from '~/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import diffFileMockData from '../mock_data/diff_file'; @@ -27,7 +28,6 @@ describe('DiffFile', () => { expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); - expect(vm.file.renderIt).toEqual(false); vm.file.renderIt = true; vm.$nextTick(() => { @@ -38,8 +38,8 @@ describe('DiffFile', () => { describe('collapsed', () => { it('should not have file content', done => { expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1); - expect(vm.file.collapsed).toEqual(false); - vm.file.collapsed = true; + expect(vm.isCollapsed).toEqual(false); + vm.isCollapsed = true; vm.file.renderIt = true; vm.$nextTick(() => { @@ -50,9 +50,8 @@ describe('DiffFile', () => { }); it('should have collapsed text and link', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; - vm.file.highlighted_diff_lines = null; + vm.renderIt = true; + vm.isCollapsed = true; vm.$nextTick(() => { expect(vm.$el.innerText).toContain('This diff is collapsed'); @@ -63,8 +62,8 @@ describe('DiffFile', () => { }); it('should have collapsed text and link even before rendered', done => { - vm.file.renderIt = false; - vm.file.collapsed = true; + vm.renderIt = false; + vm.isCollapsed = true; vm.$nextTick(() => { expect(vm.$el.innerText).toContain('This diff is collapsed'); @@ -75,10 +74,10 @@ describe('DiffFile', () => { }); it('should be collapsed for renamed files', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; + vm.renderIt = true; + vm.isCollapsed = false; vm.file.highlighted_diff_lines = null; - vm.file.renamed_file = true; + vm.file.viewer.name = diffViewerModes.renamed; vm.$nextTick(() => { expect(vm.$el.innerText).not.toContain('This diff is collapsed'); @@ -88,10 +87,10 @@ describe('DiffFile', () => { }); it('should be collapsed for mode changed files', done => { - vm.file.renderIt = true; - vm.file.collapsed = false; + vm.renderIt = true; + vm.isCollapsed = false; vm.file.highlighted_diff_lines = null; - vm.file.mode_changed = true; + vm.file.viewer.name = diffViewerModes.mode_changed; vm.$nextTick(() => { expect(vm.$el.innerText).not.toContain('This diff is collapsed'); @@ -101,7 +100,7 @@ describe('DiffFile', () => { }); it('should have loading icon while loading a collapsed diffs', done => { - vm.file.collapsed = true; + vm.isCollapsed = true; vm.isLoadingCollapsedDiff = true; vm.$nextTick(() => { @@ -116,7 +115,7 @@ describe('DiffFile', () => { describe('too large diff', () => { it('should have too large warning and blob link', done => { const BLOB_LINK = '/file/view/path'; - vm.file.too_large = true; + vm.file.viewer.error = diffViewerErrors.too_large; vm.file.view_path = BLOB_LINK; vm.$nextTick(() => { @@ -140,11 +139,11 @@ describe('DiffFile', () => { vm.file.highlighted_diff_lines = undefined; vm.file.parallel_diff_lines = []; - vm.file.collapsed = true; + vm.isCollapsed = true; vm.$nextTick() .then(() => { - vm.file.collapsed = false; + vm.isCollapsed = false; return vm.$nextTick(); }) diff --git a/spec/javascripts/diffs/components/tree_list_spec.js b/spec/javascripts/diffs/components/tree_list_spec.js index 9e556698f34..cd7bf6405e5 100644 --- a/spec/javascripts/diffs/components/tree_list_spec.js +++ b/spec/javascripts/diffs/components/tree_list_spec.js @@ -28,7 +28,7 @@ describe('Diffs tree list component', () => { localStorage.removeItem('mr_diff_tree_list'); - vm = mountComponentWithStore(Component, { store }); + vm = mountComponentWithStore(Component, { store, props: { hideFileStats: false } }); }); afterEach(() => { @@ -77,6 +77,16 @@ describe('Diffs tree list component', () => { expect(vm.$el.querySelectorAll('.file-row')[1].textContent).toContain('app'); }); + it('hides file stats', done => { + vm.hideFileStats = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.file-row-stats')).toBe(null); + + done(); + }); + }); + it('calls toggleTreeOpen when clicking folder', () => { spyOn(vm.$store, 'dispatch').and.stub(); diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js index c1e9f791925..4a091b4580b 100644 --- a/spec/javascripts/diffs/mock_data/diff_discussions.js +++ b/spec/javascripts/diffs/mock_data/diff_discussions.js @@ -266,7 +266,7 @@ export default { blob_name: 'CHANGELOG', blob_icon: '<i aria-hidden="true" data-hidden="true" class="fa fa-file-text-o fa-fw"></i>', file_hash: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a', - file_path: 'CHANGELOG', + file_path: 'CHANGELOG.rb', new_file: false, deleted_file: false, renamed_file: false, @@ -286,7 +286,7 @@ export default { content_sha: 'c48ee0d1bf3b30453f5b32250ce03134beaa6d13', stored_externally: null, external_storage: null, - old_path_html: ['CHANGELOG', 'CHANGELOG'], + old_path_html: 'CHANGELOG_OLD', new_path_html: 'CHANGELOG', context_lines_path: '/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff', @@ -485,6 +485,10 @@ export default { }, }, ], + viewer: { + name: 'text', + error: null, + }, }, diff_discussion: true, truncated_diff_lines: [ diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js index 031c9842f2f..32af9ea8ddd 100644 --- a/spec/javascripts/diffs/mock_data/diff_file.js +++ b/spec/javascripts/diffs/mock_data/diff_file.js @@ -25,6 +25,8 @@ export default { text: true, viewer: { name: 'text', + error: null, + collapsed: false, }, added_lines: 2, removed_lines: 0, diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index b53ae4cecfd..acff80bca62 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -29,6 +29,7 @@ import actions, { renderFileForDiscussionId, setRenderTreeList, setShowWhitespace, + setRenderIt, } from '~/diffs/store/actions'; import eventHub from '~/notes/event_hub'; import * as types from '~/diffs/store/mutation_types'; @@ -262,12 +263,16 @@ describe('DiffsStoreActions', () => { { id: 1, renderIt: false, - collapsed: false, + viewer: { + collapsed: false, + }, }, { id: 2, renderIt: false, - collapsed: false, + viewer: { + collapsed: false, + }, }, ], }; @@ -766,7 +771,9 @@ describe('DiffsStoreActions', () => { diffFiles: [ { file_hash: 'HASH', - collapsed, + viewer: { + collapsed, + }, renderIt, }, ], @@ -849,4 +856,10 @@ describe('DiffsStoreActions', () => { expect(window.history.pushState).toHaveBeenCalled(); }); }); + + describe('setRenderIt', () => { + it('commits RENDER_FILE', done => { + testAction(setRenderIt, 'file', {}, [{ type: types.RENDER_FILE, payload: 'file' }], [], done); + }); + }); }); diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js index 4f69dc92ab8..0ab88e6b2aa 100644 --- a/spec/javascripts/diffs/store/getters_spec.js +++ b/spec/javascripts/diffs/store/getters_spec.js @@ -51,13 +51,13 @@ describe('Diffs Module Getters', () => { describe('hasCollapsedFile', () => { it('returns true when all files are collapsed', () => { - localState.diffFiles = [{ collapsed: true }, { collapsed: true }]; + localState.diffFiles = [{ viewer: { collapsed: true } }, { viewer: { collapsed: true } }]; expect(getters.hasCollapsedFile(localState)).toEqual(true); }); it('returns true when at least one file is collapsed', () => { - localState.diffFiles = [{ collapsed: false }, { collapsed: true }]; + localState.diffFiles = [{ viewer: { collapsed: false } }, { viewer: { collapsed: true } }]; expect(getters.hasCollapsedFile(localState)).toEqual(true); }); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index a6f3f9b9dc3..09ee691b602 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -121,8 +121,14 @@ describe('DiffsStoreMutations', () => { describe('ADD_COLLAPSED_DIFFS', () => { it('should update the state with the given data for the given file hash', () => { const fileHash = 123; - const state = { diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }] }; - const data = { diff_files: [{ file_hash: fileHash, extra_field: 1, existing_field: 1 }] }; + const state = { + diffFiles: [{}, { file_hash: fileHash, existing_field: 0 }], + }; + const data = { + diff_files: [ + { file_hash: fileHash, extra_field: 1, existing_field: 1, viewer: { name: 'text' } }, + ], + }; mutations[types.ADD_COLLAPSED_DIFFS](state, { file: state.diffFiles[1], data }); diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index c5e413a29d8..599ea9cd420 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -14,7 +14,7 @@ import { MERGE_REQUEST_NOTEABLE_TYPE } from '~/notes/constants'; import diffFileMockData from '../mock_data/diff_file'; import { noteableDataMock } from '../../notes/mock_data'; -const getDiffFileMock = () => Object.assign({}, diffFileMockData); +const getDiffFileMock = () => JSON.parse(JSON.stringify(diffFileMockData)); describe('DiffsStoreUtils', () => { describe('findDiffFile', () => { @@ -80,30 +80,44 @@ describe('DiffsStoreUtils', () => { }); describe('addContextLines', () => { - it('should add context lines properly with bottom parameter', () => { + it('should add context lines', () => { const diffFile = getDiffFileMock(); const inlineLines = diffFile.highlighted_diff_lines; const parallelLines = diffFile.parallel_diff_lines; const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; - const contextLines = [{ lineNumber: 42 }]; - const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true }; + const contextLines = [{ lineNumber: 42, line_code: '123' }]; + const options = { inlineLines, parallelLines, contextLines, lineNumbers }; const inlineIndex = utils.findIndexInInlineLines(inlineLines, lineNumbers); const parallelIndex = utils.findIndexInParallelLines(parallelLines, lineNumbers); const normalizedParallelLine = { left: options.contextLines[0], right: options.contextLines[0], + line_code: '123', }; utils.addContextLines(options); - expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]); - expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine); + expect(inlineLines[inlineIndex]).toEqual(contextLines[0]); + expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine); + }); + + it('should add context lines properly with bottom parameter', () => { + const diffFile = getDiffFileMock(); + const inlineLines = diffFile.highlighted_diff_lines; + const parallelLines = diffFile.parallel_diff_lines; + const lineNumbers = { oldLineNumber: 3, newLineNumber: 5 }; + const contextLines = [{ lineNumber: 42, line_code: '123' }]; + const options = { inlineLines, parallelLines, contextLines, lineNumbers, bottom: true }; + const normalizedParallelLine = { + left: options.contextLines[0], + right: options.contextLines[0], + line_code: '123', + }; - delete options.bottom; utils.addContextLines(options); - expect(inlineLines[inlineIndex]).toEqual(contextLines[0]); - expect(parallelLines[parallelIndex]).toEqual(normalizedParallelLine); + expect(inlineLines[inlineLines.length - 1]).toEqual(contextLines[0]); + expect(parallelLines[parallelLines.length - 1]).toEqual(normalizedParallelLine); }); }); @@ -587,7 +601,7 @@ describe('DiffsStoreUtils', () => { it('returns mode_changed if key has no match', () => { expect( utils.getDiffMode({ - mode_changed: true, + viewer: { name: 'mode_changed' }, }), ).toBe('mode_changed'); }); 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 9aa3cbaa231..6230da77f49 100644 --- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js @@ -755,6 +755,17 @@ describe('Filtered Search Visual Tokens', () => { expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); }); + it('does not update user token appearance for `None` filter', () => { + const { tokenNameElement } = findElements(authorToken); + + const tokenName = tokenNameElement.innerText; + const tokenValue = 'None'; + + subject.renderVisualTokenValue(authorToken, tokenName, tokenValue); + + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + it('does not update user token appearance for `none` filter', () => { const { tokenNameElement } = findElements(authorToken); diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js index 1972408356e..88652202a8e 100644 --- a/spec/javascripts/helpers/vuex_action_helper.js +++ b/spec/javascripts/helpers/vuex_action_helper.js @@ -84,7 +84,10 @@ export default ( done(); }; - const result = action({ commit, state, dispatch, rootState: state, rootGetters: state }, payload); + const result = action( + { commit, state, dispatch, rootState: state, rootGetters: state, getters: state }, + payload, + ); return new Promise(resolve => { setImmediate(resolve); diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js index 595a2f927e9..d94cc1a8faa 100644 --- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js +++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js @@ -41,6 +41,15 @@ describe('new file modal component', () => { expect(vm.$el.querySelector('.label-bold').textContent.trim()).toBe('Name'); }); + it(`${type === 'tree' ? 'does not show' : 'shows'} file templates`, () => { + const templateFilesEl = vm.$el.querySelector('.file-templates'); + if (type === 'tree') { + expect(templateFilesEl).toBeNull(); + } else { + expect(templateFilesEl instanceof Element).toBeTruthy(); + } + }); + describe('createEntryInStore', () => { it('$emits create', () => { spyOn(vm, 'createTempEntry'); diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js new file mode 100644 index 00000000000..a1ff84ce259 --- /dev/null +++ b/spec/javascripts/import_projects/components/import_projects_table_spec.js @@ -0,0 +1,186 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import store from '~/import_projects/store'; +import importProjectsTable from '~/import_projects/components/import_projects_table.vue'; +import STATUS_MAP from '~/import_projects/constants'; +import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; + +describe('ImportProjectsTable', () => { + let vm; + let mock; + const reposPath = '/repos-path'; + const jobsPath = '/jobs-path'; + const providerTitle = 'THE PROVIDER'; + const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' }; + const importedProject = { + id: 1, + fullPath: 'fullPath', + importStatus: 'started', + providerLink: 'providerLink', + importSource: 'importSource', + }; + + function createComponent() { + const ImportProjectsTable = Vue.extend(importProjectsTable); + + const component = new ImportProjectsTable({ + store, + propsData: { + providerTitle, + }, + }).$mount(); + + component.$store.dispatch('stopJobsPolling'); + + return component; + } + + beforeEach(() => { + store.dispatch('setInitialData', { reposPath }); + mock = new MockAdapter(axios); + }); + + afterEach(() => { + vm.$destroy(); + mock.restore(); + }); + + it('renders a loading icon whilst repos are loading', done => { + mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('renders a table with imported projects and provider repos', done => { + const response = { + importedProjects: [importedProject], + providerRepos: [providerRepo], + namespaces: [{ path: 'path' }], + }; + mock.onGet(reposPath).reply(200, response); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull(); + expect(vm.$el.querySelector('.table')).not.toBeNull(); + expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch( + `From ${providerTitle}`, + ); + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('renders an empty state if there are no imported projects or provider repos', done => { + const response = { + importedProjects: [], + providerRepos: [], + namespaces: [], + }; + mock.onGet(reposPath).reply(200, response); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull(); + expect(vm.$el.querySelector('.table')).toBeNull(); + expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('imports provider repos if bulk import button is clicked', done => { + const importPath = '/import-path'; + const response = { + importedProjects: [], + providerRepos: [providerRepo], + namespaces: [{ path: 'path' }], + }; + + mock.onGet(reposPath).replyOnce(200, response); + mock.onPost(importPath).replyOnce(200, importedProject); + + store.dispatch('setInitialData', { importPath }); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + expect(vm.$el.querySelector('.js-imported-project')).toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull(); + + vm.$el.querySelector('.js-import-all').click(); + }) + .then(() => setTimeoutPromise()) + .then(() => { + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector('.js-provider-repo')).toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); + + it('polls to update the status of imported projects', done => { + const importPath = '/import-path'; + const response = { + importedProjects: [importedProject], + providerRepos: [], + namespaces: [{ path: 'path' }], + }; + const updatedProjects = [ + { + id: importedProject.id, + importStatus: 'finished', + }, + ]; + + mock.onGet(reposPath).replyOnce(200, response); + + store.dispatch('setInitialData', { importPath, jobsPath }); + + vm = createComponent(); + + setTimeoutPromise() + .then(() => { + const statusObject = STATUS_MAP[importedProject.importStatus]; + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + + mock.onGet(jobsPath).replyOnce(200, updatedProjects); + return vm.$store.dispatch('restartJobsPolling'); + }) + .then(() => setTimeoutPromise()) + .then(() => { + const statusObject = STATUS_MAP[updatedProjects[0].importStatus]; + + expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull(); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + }) + .then(() => done()) + .catch(() => done.fail()); + }); +}); diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js new file mode 100644 index 00000000000..8af3b5954a9 --- /dev/null +++ b/spec/javascripts/import_projects/components/imported_project_table_row_spec.js @@ -0,0 +1,50 @@ +import Vue from 'vue'; +import store from '~/import_projects/store'; +import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue'; +import STATUS_MAP from '~/import_projects/constants'; + +describe('ImportedProjectTableRow', () => { + let vm; + const project = { + id: 1, + fullPath: 'fullPath', + importStatus: 'finished', + providerLink: 'providerLink', + importSource: 'importSource', + }; + + function createComponent() { + const ImportedProjectTableRow = Vue.extend(importedProjectTableRow); + + return new ImportedProjectTableRow({ + store, + propsData: { + project: { + ...project, + }, + }, + }).$mount(); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders an imported project table row', () => { + vm = createComponent(); + + const providerLink = vm.$el.querySelector('.js-provider-link'); + const statusObject = STATUS_MAP[project.importStatus]; + + expect(vm.$el.classList.contains('js-imported-project')).toBe(true); + expect(providerLink.href).toMatch(project.providerLink); + expect(providerLink.textContent).toMatch(project.importSource); + expect(vm.$el.querySelector('.js-full-path').textContent).toMatch(project.fullPath); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + expect(vm.$el.querySelector('.js-go-to-project').href).toMatch(project.fullPath); + }); +}); diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js new file mode 100644 index 00000000000..7191fc923ce --- /dev/null +++ b/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js @@ -0,0 +1,78 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import store from '~/import_projects/store'; +import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue'; +import STATUS_MAP, { STATUSES } from '~/import_projects/constants'; +import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; + +describe('ProviderRepoTableRow', () => { + let vm; + const repo = { + id: 10, + sanitizedName: 'sanitizedName', + fullName: 'fullName', + providerLink: 'providerLink', + }; + + function createComponent() { + const ProviderRepoTableRow = Vue.extend(providerRepoTableRow); + + return new ProviderRepoTableRow({ + store, + propsData: { + repo: { + ...repo, + }, + }, + }).$mount(); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a provider repo table row', () => { + vm = createComponent(); + + const providerLink = vm.$el.querySelector('.js-provider-link'); + const statusObject = STATUS_MAP[STATUSES.NONE]; + + expect(vm.$el.classList.contains('js-provider-repo')).toBe(true); + expect(providerLink.href).toMatch(repo.providerLink); + expect(providerLink.textContent).toMatch(repo.fullName); + expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch( + statusObject.text, + ); + + expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull(); + expect(vm.$el.querySelector('.js-import-button')).not.toBeNull(); + }); + + it('imports repo when clicking import button', done => { + const importPath = '/import-path'; + const defaultTargetNamespace = 'user'; + const ciCdOnly = true; + const mock = new MockAdapter(axios); + + store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly }); + mock.onPost(importPath).replyOnce(200); + spyOn(store, 'dispatch').and.returnValue(new Promise(() => {})); + + vm = createComponent(); + + vm.$el.querySelector('.js-import-button').click(); + + setTimeoutPromise() + .then(() => { + expect(store.dispatch).toHaveBeenCalledWith('fetchImport', { + repo, + newName: repo.sanitizedName, + targetNamespace: defaultTargetNamespace, + }); + }) + .then(() => mock.restore()) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/javascripts/import_projects/store/actions_spec.js new file mode 100644 index 00000000000..77850ee3283 --- /dev/null +++ b/spec/javascripts/import_projects/store/actions_spec.js @@ -0,0 +1,284 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import { + SET_INITIAL_DATA, + REQUEST_REPOS, + RECEIVE_REPOS_SUCCESS, + RECEIVE_REPOS_ERROR, + REQUEST_IMPORT, + RECEIVE_IMPORT_SUCCESS, + RECEIVE_IMPORT_ERROR, + RECEIVE_JOBS_SUCCESS, +} from '~/import_projects/store/mutation_types'; +import { + setInitialData, + requestRepos, + receiveReposSuccess, + receiveReposError, + fetchRepos, + requestImport, + receiveImportSuccess, + receiveImportError, + fetchImport, + receiveJobsSuccess, + fetchJobs, + clearJobsEtagPoll, + stopJobsPolling, +} from '~/import_projects/store/actions'; +import state from '~/import_projects/store/state'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; + +describe('import_projects store actions', () => { + let localState; + const repoId = 1; + const repos = [{ id: 1 }, { id: 2 }]; + const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } }; + + beforeEach(() => { + localState = state(); + }); + + describe('setInitialData', () => { + it(`commits ${SET_INITIAL_DATA} mutation`, done => { + const initialData = { + reposPath: 'reposPath', + provider: 'provider', + jobsPath: 'jobsPath', + importPath: 'impapp/assets/javascripts/vue_shared/components/select2_select.vueortPath', + defaultTargetNamespace: 'defaultTargetNamespace', + ciCdOnly: 'ciCdOnly', + canSelectNamespace: 'canSelectNamespace', + }; + + testAction( + setInitialData, + initialData, + localState, + [{ type: SET_INITIAL_DATA, payload: initialData }], + [], + done, + ); + }); + }); + + describe('requestRepos', () => { + it(`requestRepos commits ${REQUEST_REPOS} mutation`, done => { + testAction( + requestRepos, + null, + localState, + [{ type: REQUEST_REPOS, payload: null }], + [], + done, + ); + }); + }); + + describe('receiveReposSuccess', () => { + it(`commits ${RECEIVE_REPOS_SUCCESS} mutation`, done => { + testAction( + receiveReposSuccess, + repos, + localState, + [{ type: RECEIVE_REPOS_SUCCESS, payload: repos }], + [], + done, + ); + }); + }); + + describe('receiveReposError', () => { + it(`commits ${RECEIVE_REPOS_ERROR} mutation`, done => { + testAction(receiveReposError, repos, localState, [{ type: RECEIVE_REPOS_ERROR }], [], done); + }); + }); + + describe('fetchRepos', () => { + let mock; + + beforeEach(() => { + localState.reposPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestRepos and receiveReposSuccess actions on a successful request', done => { + const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] }; + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload); + + testAction( + fetchRepos, + null, + localState, + [], + [ + { type: 'requestRepos' }, + { + type: 'receiveReposSuccess', + payload: convertObjectPropsToCamelCase(payload, { deep: true }), + }, + { + type: 'fetchJobs', + }, + ], + done, + ); + }); + + it('dispatches requestRepos and receiveReposSuccess actions on an unsuccessful request', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + + testAction( + fetchRepos, + null, + localState, + [], + [{ type: 'requestRepos' }, { type: 'receiveReposError' }], + done, + ); + }); + }); + + describe('requestImport', () => { + it(`commits ${REQUEST_IMPORT} mutation`, done => { + testAction( + requestImport, + repoId, + localState, + [{ type: REQUEST_IMPORT, payload: repoId }], + [], + done, + ); + }); + }); + + describe('receiveImportSuccess', () => { + it(`commits ${RECEIVE_IMPORT_SUCCESS} mutation`, done => { + const payload = { importedProject: { name: 'imported/project' }, repoId: 2 }; + + testAction( + receiveImportSuccess, + payload, + localState, + [{ type: RECEIVE_IMPORT_SUCCESS, payload }], + [], + done, + ); + }); + }); + + describe('receiveImportError', () => { + it(`commits ${RECEIVE_IMPORT_ERROR} mutation`, done => { + testAction( + receiveImportError, + repoId, + localState, + [{ type: RECEIVE_IMPORT_ERROR, payload: repoId }], + [], + done, + ); + }); + }); + + describe('fetchImport', () => { + let mock; + + beforeEach(() => { + localState.importPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestImport and receiveImportSuccess actions on a successful request', done => { + const importedProject = { name: 'imported/project' }; + const importRepoId = importPayload.repo.id; + mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject); + + testAction( + fetchImport, + importPayload, + localState, + [], + [ + { type: 'requestImport', payload: importRepoId }, + { + type: 'receiveImportSuccess', + payload: { + importedProject: convertObjectPropsToCamelCase(importedProject, { deep: true }), + repoId: importRepoId, + }, + }, + ], + done, + ); + }); + + it('dispatches requestImport and receiveImportSuccess actions on an unsuccessful request', done => { + mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500); + + testAction( + fetchImport, + importPayload, + localState, + [], + [ + { type: 'requestImport', payload: importPayload.repo.id }, + { type: 'receiveImportError', payload: { repoId: importPayload.repo.id } }, + ], + done, + ); + }); + }); + + describe('receiveJobsSuccess', () => { + it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, done => { + testAction( + receiveJobsSuccess, + repos, + localState, + [{ type: RECEIVE_JOBS_SUCCESS, payload: repos }], + [], + done, + ); + }); + }); + + describe('fetchJobs', () => { + let mock; + + beforeEach(() => { + localState.jobsPath = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + stopJobsPolling(); + clearJobsEtagPoll(); + }); + + afterEach(() => mock.restore()); + + it('dispatches requestJobs and receiveJobsSuccess actions on a successful request', done => { + const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }]; + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects); + + testAction( + fetchJobs, + null, + localState, + [], + [ + { + type: 'receiveJobsSuccess', + payload: convertObjectPropsToCamelCase(updatedProjects, { deep: true }), + }, + ], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/import_projects/store/getters_spec.js b/spec/javascripts/import_projects/store/getters_spec.js new file mode 100644 index 00000000000..e5e4a95f473 --- /dev/null +++ b/spec/javascripts/import_projects/store/getters_spec.js @@ -0,0 +1,83 @@ +import { + namespaceSelectOptions, + isImportingAnyRepo, + hasProviderRepos, + hasImportedProjects, +} from '~/import_projects/store/getters'; +import state from '~/import_projects/store/state'; + +describe('import_projects store getters', () => { + let localState; + + beforeEach(() => { + localState = state(); + }); + + describe('namespaceSelectOptions', () => { + const namespaces = [{ fullPath: 'namespace-0' }, { fullPath: 'namespace-1' }]; + const defaultTargetNamespace = 'current-user'; + + it('returns an options array with a "Users" and "Groups" optgroups', () => { + localState.namespaces = namespaces; + localState.defaultTargetNamespace = defaultTargetNamespace; + + const optionsArray = namespaceSelectOptions(localState); + const groupsGroup = optionsArray[0]; + const usersGroup = optionsArray[1]; + + expect(groupsGroup.text).toBe('Groups'); + expect(usersGroup.text).toBe('Users'); + + groupsGroup.children.forEach((child, index) => { + expect(child.id).toBe(namespaces[index].fullPath); + expect(child.text).toBe(namespaces[index].fullPath); + }); + + expect(usersGroup.children.length).toBe(1); + expect(usersGroup.children[0].id).toBe(defaultTargetNamespace); + expect(usersGroup.children[0].text).toBe(defaultTargetNamespace); + }); + }); + + describe('isImportingAnyRepo', () => { + it('returns true if there are any reposBeingImported', () => { + localState.reposBeingImported = new Array(1); + + expect(isImportingAnyRepo(localState)).toBe(true); + }); + + it('returns false if there are no reposBeingImported', () => { + localState.reposBeingImported = []; + + expect(isImportingAnyRepo(localState)).toBe(false); + }); + }); + + describe('hasProviderRepos', () => { + it('returns true if there are any providerRepos', () => { + localState.providerRepos = new Array(1); + + expect(hasProviderRepos(localState)).toBe(true); + }); + + it('returns false if there are no providerRepos', () => { + localState.providerRepos = []; + + expect(hasProviderRepos(localState)).toBe(false); + }); + }); + + describe('hasImportedProjects', () => { + it('returns true if there are any importedProjects', () => { + localState.importedProjects = new Array(1); + + expect(hasImportedProjects(localState)).toBe(true); + }); + + it('returns false if there are no importedProjects', () => { + localState.importedProjects = []; + + expect(hasImportedProjects(localState)).toBe(false); + }); + }); +}); diff --git a/spec/javascripts/import_projects/store/mutations_spec.js b/spec/javascripts/import_projects/store/mutations_spec.js new file mode 100644 index 00000000000..8db8e9819ba --- /dev/null +++ b/spec/javascripts/import_projects/store/mutations_spec.js @@ -0,0 +1,34 @@ +import * as types from '~/import_projects/store/mutation_types'; +import mutations from '~/import_projects/store/mutations'; + +describe('import_projects store mutations', () => { + describe(types.RECEIVE_IMPORT_SUCCESS, () => { + it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => { + const repoId = 1; + const state = { + reposBeingImported: [repoId], + providerRepos: [{ id: repoId }], + importedProjects: [], + }; + const importedProject = { id: repoId }; + + mutations[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId }); + + expect(state.reposBeingImported.includes(repoId)).toBe(false); + expect(state.providerRepos.some(repo => repo.id === repoId)).toBe(false); + expect(state.importedProjects.some(repo => repo.id === repoId)).toBe(true); + }); + }); + + describe(types.RECEIVE_JOBS_SUCCESS, () => { + it('updates importStatus of existing importedProjects', () => { + const repoId = 1; + const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] }; + const updatedProjects = [{ id: repoId, importStatus: 'finished' }]; + + mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects); + + expect(state.importedProjects[0].importStatus).toBe(updatedProjects[0].importStatus); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 3eff3f655ee..c02e37950f8 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -855,6 +855,7 @@ describe('common_utils', () => { }); it('returns true when provided `el` is in viewport', () => { + el.setAttribute('style', `position: absolute; right: ${window.innerWidth + 0.2};`); document.body.appendChild(el); expect(commonUtils.isInViewport(el)).toBe(true); diff --git a/spec/javascripts/notes/components/note_actions/reply_button_spec.js b/spec/javascripts/notes/components/note_actions/reply_button_spec.js index 11e1664a3f4..11fb89808d9 100644 --- a/spec/javascripts/notes/components/note_actions/reply_button_spec.js +++ b/spec/javascripts/notes/components/note_actions/reply_button_spec.js @@ -3,27 +3,14 @@ import { createLocalVue, mount } from '@vue/test-utils'; import ReplyButton from '~/notes/components/note_actions/reply_button.vue'; describe('ReplyButton', () => { - const noteId = 'dummy-note-id'; - let wrapper; - let convertToDiscussion; beforeEach(() => { const localVue = createLocalVue(); - convertToDiscussion = jasmine.createSpy('convertToDiscussion'); localVue.use(Vuex); - const store = new Vuex.Store({ - actions: { - convertToDiscussion, - }, - }); wrapper = mount(ReplyButton, { - propsData: { - noteId, - }, - store, sync: false, localVue, }); @@ -33,14 +20,13 @@ describe('ReplyButton', () => { wrapper.destroy(); }); - it('dispatches convertToDiscussion with note ID on click', () => { + it('emits startReplying on click', () => { const button = wrapper.find({ ref: 'button' }); button.trigger('click'); - expect(convertToDiscussion).toHaveBeenCalledTimes(1); - const [, payload] = convertToDiscussion.calls.argsFor(0); - - expect(payload).toBe(noteId); + expect(wrapper.emitted()).toEqual({ + startReplying: [[]], + }); }); }); diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index 0c1962912b4..d604e90b529 100644 --- a/spec/javascripts/notes/components/note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount, createLocalVue, createWrapper } from '@vue/test-utils'; import createStore from '~/notes/stores'; import noteActions from '~/notes/components/note_actions.vue'; import { TEST_HOST } from 'spec/test_constants'; @@ -10,7 +10,7 @@ describe('noteActions', () => { let store; let props; - const createWrapper = propsData => { + const shallowMountNoteActions = propsData => { const localVue = createLocalVue(); return shallowMount(noteActions, { store, @@ -44,7 +44,7 @@ describe('noteActions', () => { beforeEach(() => { store.dispatch('setUserData', userDataMock); - wrapper = createWrapper(props); + wrapper = shallowMountNoteActions(props); }); it('should render access level badge', () => { @@ -90,13 +90,27 @@ describe('noteActions', () => { it('should be possible to delete comment', () => { expect(wrapper.find('.js-note-delete').exists()).toBe(true); }); + + it('closes tooltip when dropdown opens', done => { + wrapper.find('.more-actions-toggle').trigger('click'); + + const rootWrapper = createWrapper(wrapper.vm.$root); + Vue.nextTick() + .then(() => { + const emitted = Object.keys(rootWrapper.emitted()); + + expect(emitted).toEqual(['bv::hide::tooltip']); + done(); + }) + .catch(done.fail); + }); }); }); describe('user is not logged in', () => { beforeEach(() => { store.dispatch('setUserData', {}); - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, canDelete: false, canEdit: false, @@ -127,7 +141,7 @@ describe('noteActions', () => { describe('for showReply = true', () => { beforeEach(() => { - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, showReply: true, }); @@ -142,7 +156,7 @@ describe('noteActions', () => { describe('for showReply = false', () => { beforeEach(() => { - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, showReply: false, }); @@ -169,7 +183,7 @@ describe('noteActions', () => { describe('for showReply = true', () => { beforeEach(() => { - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, showReply: true, }); @@ -184,7 +198,7 @@ describe('noteActions', () => { describe('for showReply = false', () => { beforeEach(() => { - wrapper = createWrapper({ + wrapper = shallowMountNoteActions({ ...props, showReply: false, }); diff --git a/spec/javascripts/notes/components/noteable_note_spec.js b/spec/javascripts/notes/components/noteable_note_spec.js index 8ade6fc2ced..9420713ceca 100644 --- a/spec/javascripts/notes/components/noteable_note_spec.js +++ b/spec/javascripts/notes/components/noteable_note_spec.js @@ -1,86 +1,138 @@ -import $ from 'jquery'; import _ from 'underscore'; -import Vue from 'vue'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; import createStore from '~/notes/stores'; import issueNote from '~/notes/components/noteable_note.vue'; +import NoteHeader from '~/notes/components/note_header.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import NoteActions from '~/notes/components/note_actions.vue'; +import NoteBody from '~/notes/components/note_body.vue'; import { noteableDataMock, notesDataMock, note } from '../mock_data'; describe('issue_note', () => { let store; - let vm; + let wrapper; beforeEach(() => { - const Component = Vue.extend(issueNote); - store = createStore(); store.dispatch('setNoteableData', noteableDataMock); store.dispatch('setNotesData', notesDataMock); - vm = new Component({ + const localVue = createLocalVue(); + wrapper = shallowMount(issueNote, { store, propsData: { note, }, - }).$mount(); + sync: false, + localVue, + }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); it('should render user information', () => { - expect(vm.$el.querySelector('.user-avatar-link img').getAttribute('src')).toEqual( - note.author.avatar_url, - ); + const { author } = note; + const avatar = wrapper.find(UserAvatarLink); + const avatarProps = avatar.props(); + + expect(avatarProps.linkHref).toBe(author.path); + expect(avatarProps.imgSrc).toBe(author.avatar_url); + expect(avatarProps.imgAlt).toBe(author.name); + expect(avatarProps.imgSize).toBe(40); }); it('should render note header content', () => { - const el = vm.$el.querySelector('.note-header .note-header-author-name'); + const noteHeader = wrapper.find(NoteHeader); + const noteHeaderProps = noteHeader.props(); - expect(el.textContent.trim()).toEqual(note.author.name); + expect(noteHeaderProps.author).toEqual(note.author); + expect(noteHeaderProps.createdAt).toEqual(note.created_at); + expect(noteHeaderProps.noteId).toEqual(note.id); }); it('should render note actions', () => { - expect(vm.$el.querySelector('.note-actions')).toBeDefined(); + const { author } = note; + const noteActions = wrapper.find(NoteActions); + const noteActionsProps = noteActions.props(); + + expect(noteActionsProps.authorId).toBe(author.id); + expect(noteActionsProps.noteId).toBe(note.id); + expect(noteActionsProps.noteUrl).toBe(note.noteable_note_url); + expect(noteActionsProps.accessLevel).toBe(note.human_access); + expect(noteActionsProps.canEdit).toBe(note.current_user.can_edit); + expect(noteActionsProps.canAwardEmoji).toBe(note.current_user.can_award_emoji); + expect(noteActionsProps.canDelete).toBe(note.current_user.can_edit); + expect(noteActionsProps.canReportAsAbuse).toBe(true); + expect(noteActionsProps.canResolve).toBe(false); + expect(noteActionsProps.reportAbusePath).toBe(note.report_abuse_path); + expect(noteActionsProps.resolvable).toBe(false); + expect(noteActionsProps.isResolved).toBe(false); + expect(noteActionsProps.isResolving).toBe(false); + expect(noteActionsProps.resolvedBy).toEqual({}); }); it('should render issue body', () => { - expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html); + const noteBody = wrapper.find(NoteBody); + const noteBodyProps = noteBody.props(); + + expect(noteBodyProps.note).toEqual(note); + expect(noteBodyProps.line).toBe(null); + expect(noteBodyProps.canEdit).toBe(note.current_user.can_edit); + expect(noteBodyProps.isEditing).toBe(false); + expect(noteBodyProps.helpPagePath).toBe(''); }); it('prevents note preview xss', done => { const imgSrc = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; const noteBody = `<img src="${imgSrc}" onload="alert(1)" />`; const alertSpy = spyOn(window, 'alert'); - vm.updateNote = () => new Promise($.noop); + store.hotUpdate({ + actions: { + updateNote() {}, + }, + }); + const noteBodyComponent = wrapper.find(NoteBody); - vm.formUpdateHandler(noteBody, null, $.noop); + noteBodyComponent.vm.$emit('handleFormUpdate', noteBody, null, () => {}); setTimeout(() => { expect(alertSpy).not.toHaveBeenCalled(); - expect(vm.note.note_html).toEqual(_.escape(noteBody)); + expect(wrapper.vm.note.note_html).toEqual(_.escape(noteBody)); done(); }, 0); }); describe('cancel edit', () => { it('restores content of updated note', done => { - const noteBody = 'updated note text'; - vm.updateNote = () => Promise.resolve(); - - vm.formUpdateHandler(noteBody, null, $.noop); - - setTimeout(() => { - expect(vm.note.note_html).toEqual(noteBody); - - vm.formCancelHandler(); - - setTimeout(() => { - expect(vm.note.note_html).toEqual(noteBody); - - done(); - }); + const updatedText = 'updated note text'; + store.hotUpdate({ + actions: { + updateNote() {}, + }, }); + const noteBody = wrapper.find(NoteBody); + noteBody.vm.resetAutoSave = () => {}; + + noteBody.vm.$emit('handleFormUpdate', updatedText, null, () => {}); + + wrapper.vm + .$nextTick() + .then(() => { + const noteBodyProps = noteBody.props(); + + expect(noteBodyProps.note.note_html).toBe(updatedText); + noteBody.vm.$emit('cancelForm'); + }) + .then(() => wrapper.vm.$nextTick()) + .then(() => { + const noteBodyProps = noteBody.props(); + + expect(noteBodyProps.note.note_html).toBe(note.note_html); + }) + .then(done) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 73f960dd21e..94ce6d8e222 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -1,8 +1,11 @@ import Vue from 'vue'; import $ from 'jquery'; import _ from 'underscore'; +import { TEST_HOST } from 'spec/test_constants'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import * as actions from '~/notes/stores/actions'; +import * as mutationTypes from '~/notes/stores/mutation_types'; +import * as notesConstants from '~/notes/constants'; import createStore from '~/notes/stores'; import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; import testAction from '../../helpers/vuex_action_helper'; @@ -599,4 +602,153 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('updateOrCreateNotes', () => { + let commit; + let dispatch; + let state; + + beforeEach(() => { + commit = jasmine.createSpy('commit'); + dispatch = jasmine.createSpy('dispatch'); + state = {}; + }); + + afterEach(() => { + commit.calls.reset(); + dispatch.calls.reset(); + }); + + it('Updates existing note', () => { + const note = { id: 1234 }; + const getters = { notesById: { 1234: note } }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.UPDATE_NOTE, note]]); + }); + + it('Creates a new note if none exisits', () => { + const note = { id: 1234 }; + const getters = { notesById: {} }; + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [note]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, note]]); + }); + + describe('Discussion notes', () => { + let note; + let getters; + + beforeEach(() => { + note = { id: 1234 }; + getters = { notesById: {} }; + }); + + it('Adds a reply to an existing discussion', () => { + state = { discussions: [note] }; + const discussionNote = { + ...note, + type: notesConstants.DISCUSSION_NOTE, + discussion_id: 1234, + }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]); + + expect(commit.calls.allArgs()).toEqual([ + [mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, discussionNote], + ]); + }); + + it('fetches discussions for diff notes', () => { + state = { discussions: [], notesData: { discussionsPath: 'Hello world' } }; + const diffNote = { ...note, type: notesConstants.DIFF_NOTE, discussion_id: 1234 }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [diffNote]); + + expect(dispatch.calls.allArgs()).toEqual([ + ['fetchDiscussions', { path: state.notesData.discussionsPath }], + ]); + }); + + it('Adds a new note', () => { + state = { discussions: [] }; + const discussionNote = { + ...note, + type: notesConstants.DISCUSSION_NOTE, + discussion_id: 1234, + }; + + actions.updateOrCreateNotes({ commit, state, getters, dispatch }, [discussionNote]); + + expect(commit.calls.allArgs()).toEqual([[mutationTypes.ADD_NEW_NOTE, discussionNote]]); + }); + }); + }); + + describe('replyToDiscussion', () => { + let res = { discussion: { notes: [] } }; + const payload = { endpoint: TEST_HOST, data: {} }; + const interceptor = (request, next) => { + next( + request.respondWith(JSON.stringify(res), { + status: 200, + }), + ); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('updates discussion if response contains disussion', done => { + testAction( + actions.replyToDiscussion, + payload, + { + notesById: {}, + }, + [{ type: mutationTypes.UPDATE_DISCUSSION, payload: res.discussion }], + [ + { type: 'updateMergeRequestWidget' }, + { type: 'startTaskList' }, + { type: 'updateResolvableDiscussonsCounts' }, + ], + done, + ); + }); + + it('adds a reply to a discussion', done => { + res = {}; + + testAction( + actions.replyToDiscussion, + payload, + { + notesById: {}, + }, + [{ type: mutationTypes.ADD_NEW_REPLY_TO_DISCUSSION, payload: res }], + [], + done, + ); + }); + }); + + describe('removeConvertedDiscussion', () => { + it('commits CONVERT_TO_DISCUSSION with noteId', done => { + const noteId = 'dummy-id'; + testAction( + actions.removeConvertedDiscussion, + noteId, + {}, + [{ type: 'REMOVE_CONVERTED_DISCUSSION', payload: noteId }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 4f8d3069bb5..fcad1f245b6 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -527,17 +527,32 @@ describe('Notes Store mutations', () => { id: 42, individual_note: true, }; - state = { discussions: [discussion] }; + state = { convertedDisscussionIds: [] }; }); - it('toggles individual_note', () => { + it('adds a disucssion to convertedDisscussionIds', () => { mutations.CONVERT_TO_DISCUSSION(state, discussion.id); - expect(discussion.individual_note).toBe(false); + expect(state.convertedDisscussionIds).toContain(discussion.id); }); + }); + + describe('REMOVE_CONVERTED_DISCUSSION', () => { + let discussion; + let state; + + beforeEach(() => { + discussion = { + id: 42, + individual_note: true, + }; + state = { convertedDisscussionIds: [41, 42] }; + }); + + it('removes a disucssion from convertedDisscussionIds', () => { + mutations.REMOVE_CONVERTED_DISCUSSION(state, discussion.id); - it('throws if discussion was not found', () => { - expect(() => mutations.CONVERT_TO_DISCUSSION(state, 99)).toThrow(); + expect(state.convertedDisscussionIds).not.toContain(discussion.id); }); }); }); 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 ff08a46b922..3e8f73646c8 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -21,6 +21,7 @@ describe('mrWidgetOptions', () => { const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch'; beforeEach(() => { + gon.features = { approvalRules: false }; // Prevent component mounting delete mrWidgetOptions.el; @@ -31,6 +32,7 @@ describe('mrWidgetOptions', () => { }); afterEach(() => { + gon.features = null; vm.$destroy(); }); diff --git a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js index 5b1038840c7..634ba8403d5 100644 --- a/spec/javascripts/vue_shared/components/changed_file_icon_spec.js +++ b/spec/javascripts/vue_shared/components/changed_file_icon_spec.js @@ -5,27 +5,40 @@ import createComponent from 'spec/helpers/vue_mount_component_helper'; describe('Changed file icon', () => { let vm; - beforeEach(() => { + function factory(props = {}) { const component = Vue.extend(changedFileIcon); vm = createComponent(component, { + ...props, file: { tempFile: false, changed: true, }, }); - }); + } afterEach(() => { vm.$destroy(); }); + it('centers icon', () => { + factory({ + isCentered: true, + }); + + expect(vm.$el.classList).toContain('ml-auto'); + }); + describe('changedIcon', () => { it('equals file-modified when not a temp file and has changes', () => { + factory(); + expect(vm.changedIcon).toBe('file-modified'); }); it('equals file-addition when a temp file', () => { + factory(); + vm.file.tempFile = true; expect(vm.changedIcon).toBe('file-addition'); @@ -34,10 +47,14 @@ describe('Changed file icon', () => { describe('changedIconClass', () => { it('includes file-modified when not a temp file', () => { + factory(); + expect(vm.changedIconClass).toContain('file-modified'); }); it('includes file-addition when a temp file', () => { + factory(); + vm.file.tempFile = true; expect(vm.changedIconClass).toContain('file-addition'); diff --git a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js index 6add6cdac4d..660eaddf01f 100644 --- a/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/javascripts/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -22,6 +22,7 @@ describe('DiffViewer', () => { createComponent({ diffMode: 'replaced', + diffViewerMode: 'image', newPath: GREEN_BOX_IMAGE_URL, newSha: 'ABC', oldPath: RED_BOX_IMAGE_URL, @@ -45,6 +46,7 @@ describe('DiffViewer', () => { it('renders fallback download diff display', done => { createComponent({ diffMode: 'replaced', + diffViewerMode: 'added', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', @@ -72,6 +74,7 @@ describe('DiffViewer', () => { it('renders renamed component', () => { createComponent({ diffMode: 'renamed', + diffViewerMode: 'renamed', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', @@ -84,6 +87,7 @@ describe('DiffViewer', () => { it('renders mode changed component', () => { createComponent({ diffMode: 'mode_changed', + diffViewerMode: 'image', newPath: 'test.abc', newSha: 'ABC', oldPath: 'testold.abc', diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js index 49a580be06b..caabe3aa260 100644 --- a/spec/javascripts/vue_shared/components/panel_resizer_spec.js +++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js @@ -44,7 +44,10 @@ describe('Panel Resizer component', () => { }); expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-left'); + expect(vm.$el.getAttribute('class')).toBe( + 'position-absolute position-top-0 position-bottom-0 drag-handle position-left-0', + ); + expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;'); }); @@ -55,7 +58,9 @@ describe('Panel Resizer component', () => { }); expect(vm.$el.tagName).toEqual('DIV'); - expect(vm.$el.getAttribute('class')).toBe('drag-handle drag-right'); + expect(vm.$el.getAttribute('class')).toBe( + 'position-absolute position-top-0 position-bottom-0 drag-handle position-right-0', + ); }); it('drag the resizer', () => { diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js index f2472fd377c..80aa75847ae 100644 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js +++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js @@ -1,13 +1,14 @@ import _ from 'underscore'; import Vue from 'vue'; import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { TEST_HOST } from 'spec/test_constants'; describe('User Avatar Link Component', function() { beforeEach(function() { this.propsData = { - linkHref: 'myavatarurl.com', + linkHref: `${TEST_HOST}/myavatarurl.com`, imgSize: 99, - imgSrc: 'myavatarurl.com', + imgSrc: `${TEST_HOST}/myavatarurl.com`, imgAlt: 'mydisplayname', imgCssClasses: 'myextraavatarclass', tooltipText: 'tooltip text', @@ -37,11 +38,18 @@ describe('User Avatar Link Component', function() { }); it('should render <a> as a child element', function() { - expect(this.userAvatarLink.$el.tagName).toBe('A'); + const link = this.userAvatarLink.$el; + + expect(link.tagName).toBe('A'); + expect(link.href).toBe(this.propsData.linkHref); }); - it('should have <img> as a child element', function() { - expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); + it('renders imgSrc with imgSize as image', function() { + const { imgSrc, imgSize } = this.propsData; + const image = this.userAvatarLink.$el.querySelector('img'); + + expect(image).not.toBeNull(); + expect(image.src).toBe(`${imgSrc}?width=${imgSize}`); }); it('should return necessary props as defined', function() { diff --git a/spec/lib/banzai/filter/footnote_filter_spec.rb b/spec/lib/banzai/filter/footnote_filter_spec.rb index 2e50e4e2351..c6dcb4e46fd 100644 --- a/spec/lib/banzai/filter/footnote_filter_spec.rb +++ b/spec/lib/banzai/filter/footnote_filter_spec.rb @@ -11,6 +11,7 @@ describe Banzai::Filter::FootnoteFilter do let(:footnote) do <<~EOF <p>first<sup><a href="#fn1" id="fnref1">1</a></sup> and second<sup><a href="#fn2" id="fnref2">2</a></sup></p> + <p>same reference<sup><a href="#fn1" id="fnref1">1</a></sup></p> <ol> <li id="fn1"> <p>one <a href="#fnref1">↩</a></p> @@ -25,6 +26,7 @@ describe Banzai::Filter::FootnoteFilter do let(:filtered_footnote) do <<~EOF <p>first<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup> and second<sup class="footnote-ref"><a href="#fn2-#{identifier}" id="fnref2-#{identifier}">2</a></sup></p> + <p>same reference<sup class="footnote-ref"><a href="#fn1-#{identifier}" id="fnref1-#{identifier}">1</a></sup></p> <section class="footnotes"><ol> <li id="fn1-#{identifier}"> <p>one <a href="#fnref1-#{identifier}" class="footnote-backref">↩</a></p> diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 630732614b2..a7163048370 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -186,13 +186,14 @@ describe Feature do describe Feature::Target do describe '#targets' do let(:project) { create(:project) } + let(:group) { create(:group) } let(:user_name) { project.owner.username } - subject { described_class.new(user: user_name, project: project.full_path) } + subject { described_class.new(user: user_name, project: project.full_path, group: group.full_path) } it 'returns all found targets' do expect(subject.targets).to be_an(Array) - expect(subject.targets).to eq([project.owner, project]) + expect(subject.targets).to eq([project.owner, project, group]) end end end diff --git a/spec/lib/gitlab/ci/build/policy/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb index 5fee37bb43e..dc3329061d1 100644 --- a/spec/lib/gitlab/ci/build/policy/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb @@ -73,9 +73,9 @@ describe Gitlab::Ci::Build::Policy::Changes do expect(policy).not_to be_satisfied_by(pipeline, seed) end - context 'when pipelines does not run for a branch update' do + context 'when modified paths can not be evaluated' do before do - pipeline.before_sha = Gitlab::Git::BLANK_SHA + allow(pipeline).to receive(:modified_paths) { nil } end it 'is always satisfied' do @@ -115,5 +115,57 @@ describe Gitlab::Ci::Build::Policy::Changes do expect(policy).not_to be_satisfied_by(pipeline, seed) end end + + context 'when branch is created' do + let(:pipeline) do + create(:ci_empty_pipeline, project: project, + ref: 'feature', + source: source, + sha: '0b4bc9a4', + before_sha: Gitlab::Git::BLANK_SHA, + merge_request: merge_request) + end + + let(:ci_build) do + build(:ci_build, pipeline: pipeline, project: project, ref: 'feature') + end + + let(:seed) { double('build seed', to_resource: ci_build) } + + context 'when source is merge request' do + let(:source) { :merge_request } + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'is satified by changes in the merge request' do + policy = described_class.new(%w[files/ruby/feature.rb]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is not satified by changes not in the merge request' do + policy = described_class.new(%w[foo.rb]) + + expect(policy).not_to be_satisfied_by(pipeline, seed) + end + end + + context 'when source is push' do + let(:source) { :push } + let(:merge_request) { nil } + + it 'is always satified' do + policy = described_class.new(%w[foo.rb]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + end + end end end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb new file mode 100644 index 00000000000..793cac593a2 --- /dev/null +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -0,0 +1,300 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rspec-parameterized' +require 'webmock/rspec' + +require 'gitlab/danger/helper' + +describe Gitlab::Danger::Helper do + using RSpec::Parameterized::TableSyntax + + class FakeDanger + include Gitlab::Danger::Helper + + attr_reader :git + + def initialize(git:) + @git = git + end + end + + let(:teammate_json) do + <<~JSON + [ + { + "username": "in-gitlab-ce", + "name": "CE maintainer", + "projects":{ "gitlab-ce": "maintainer backend" } + }, + { + "username": "in-gitlab-ee", + "name": "EE reviewer", + "projects":{ "gitlab-ee": "reviewer frontend" } + } + ] + JSON + end + + let(:ce_teammate_matcher) do + satisfy do |teammate| + teammate.username == 'in-gitlab-ce' && + teammate.name == 'CE maintainer' && + teammate.projects == { 'gitlab-ce' => 'maintainer backend' } + end + end + + let(:ee_teammate_matcher) do + satisfy do |teammate| + teammate.username == 'in-gitlab-ee' && + teammate.name == 'EE reviewer' && + teammate.projects == { 'gitlab-ee' => 'reviewer frontend' } + end + end + + let(:fake_git) { double('fake-git') } + + subject(:helper) { FakeDanger.new(git: fake_git) } + + describe '#all_changed_files' do + subject { helper.all_changed_files } + + it 'interprets a list of changes from the danger git plugin' do + expect(fake_git).to receive(:added_files) { %w[a b c.old] } + expect(fake_git).to receive(:modified_files) { %w[d e] } + expect(fake_git) + .to receive(:renamed_files) + .at_least(:once) + .and_return([{ before: 'c.old', after: 'c.new' }]) + + is_expected.to contain_exactly('a', 'b', 'c.new', 'd', 'e') + end + end + + describe '#ee?' do + subject { helper.ee? } + + it 'returns true if CI_PROJECT_NAME if set to gitlab-ee' do + stub_env('CI_PROJECT_NAME', 'gitlab-ee') + expect(File).not_to receive(:exist?) + + is_expected.to be_truthy + end + + it 'delegates to CHANGELOG-EE.md existence if CI_PROJECT_NAME is set to something else' do + stub_env('CI_PROJECT_NAME', 'something else') + expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { true } + + is_expected.to be_truthy + end + + it 'returns true if CHANGELOG-EE.md exists' do + stub_env('CI_PROJECT_NAME', nil) + expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { true } + + is_expected.to be_truthy + end + + it "returns false if CHANGELOG-EE.md doesn't exist" do + stub_env('CI_PROJECT_NAME', nil) + expect(File).to receive(:exist?).with('../../CHANGELOG-EE.md') { false } + + is_expected.to be_falsy + end + end + + describe '#project_name' do + subject { helper.project_name } + + it 'returns gitlab-ee if ee? returns true' do + expect(helper).to receive(:ee?) { true } + + is_expected.to eq('gitlab-ee') + end + + it 'returns gitlab-ce if ee? returns false' do + expect(helper).to receive(:ee?) { false } + + is_expected.to eq('gitlab-ce') + end + end + + describe '#team' do + subject(:team) { helper.team } + + context 'HTTP failure' do + before do + WebMock + .stub_request(:get, 'https://about.gitlab.com/roulette.json') + .to_return(status: 404) + end + + it 'raises a pretty error' do + expect { team }.to raise_error(/Failed to read/) + end + end + + context 'JSON failure' do + before do + WebMock + .stub_request(:get, 'https://about.gitlab.com/roulette.json') + .to_return(body: 'INVALID JSON') + end + + it 'raises a pretty error' do + expect { team }.to raise_error(/Failed to parse/) + end + end + + context 'success' do + before do + WebMock + .stub_request(:get, 'https://about.gitlab.com/roulette.json') + .to_return(body: teammate_json) + end + + it 'returns an array of teammates' do + is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher) + end + + it 'memoizes the result' do + expect(team.object_id).to eq(helper.team.object_id) + end + end + end + + describe '#project_team' do + subject { helper.project_team } + + before do + WebMock + .stub_request(:get, 'https://about.gitlab.com/roulette.json') + .to_return(body: teammate_json) + end + + it 'filters team by project_name' do + expect(helper) + .to receive(:project_name) + .at_least(:once) + .and_return('gitlab-ce') + + is_expected.to contain_exactly(ce_teammate_matcher) + end + end + + describe '#changes_by_category' do + it 'categorizes changed files' do + expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo ee/changelogs/foo.yml] } + allow(fake_git).to receive(:modified_files) { [] } + allow(fake_git).to receive(:renamed_files) { [] } + + expect(helper.changes_by_category).to eq( + backend: %w[foo.rb], + database: %w[db/foo], + docs: %w[foo.md], + frontend: %w[foo.js], + none: %w[ee/changelogs/foo.yml], + qa: %w[qa/foo], + unknown: %w[foo] + ) + end + end + + describe '#category_for_file' do + where(:path, :expected_category) do + 'doc/foo' | :docs + 'CONTRIBUTING.md' | :docs + 'LICENSE' | :docs + 'MAINTENANCE.md' | :docs + 'PHILOSOPHY.md' | :docs + 'PROCESS.md' | :docs + 'README.md' | :docs + + 'ee/doc/foo' | :unknown + 'ee/README' | :unknown + + 'app/assets/foo' | :frontend + 'app/views/foo' | :frontend + 'public/foo' | :frontend + 'spec/javascripts/foo' | :frontend + 'vendor/assets/foo' | :frontend + 'jest.config.js' | :frontend + 'package.json' | :frontend + 'yarn.lock' | :frontend + + 'ee/app/assets/foo' | :frontend + 'ee/app/views/foo' | :frontend + 'ee/spec/javascripts/foo' | :frontend + + 'app/models/foo' | :backend + 'bin/foo' | :backend + 'config/foo' | :backend + 'danger/foo' | :backend + 'lib/foo' | :backend + 'rubocop/foo' | :backend + 'scripts/foo' | :backend + 'spec/foo' | :backend + 'spec/foo/bar' | :backend + + 'ee/app/foo' | :backend + 'ee/bin/foo' | :backend + 'ee/spec/foo' | :backend + 'ee/spec/foo/bar' | :backend + + 'generator_templates/foo' | :backend + 'vendor/languages.yml' | :backend + 'vendor/licenses.csv' | :backend + + 'Dangerfile' | :backend + 'Gemfile' | :backend + 'Gemfile.lock' | :backend + 'Procfile' | :backend + 'Rakefile' | :backend + '.gitlab-ci.yml' | :backend + 'FOO_VERSION' | :backend + + 'ee/FOO_VERSION' | :unknown + + 'db/foo' | :database + 'qa/foo' | :qa + + 'ee/db/foo' | :database + 'ee/qa/foo' | :qa + + 'changelogs/foo' | :none + 'ee/changelogs/foo' | :none + + 'FOO' | :unknown + 'foo' | :unknown + + 'foo/bar.rb' | :backend + 'foo/bar.js' | :frontend + 'foo/bar.txt' | :docs + 'foo/bar.md' | :docs + end + + with_them do + subject { helper.category_for_file(path) } + + it { is_expected.to eq(expected_category) } + end + end + + describe '#label_for_category' do + where(:category, :expected_label) do + :backend | '~backend' + :database | '~database' + :docs | '~Documentation' + :foo | '~foo' + :frontend | '~frontend' + :none | '' + :qa | '~QA' + end + + with_them do + subject { helper.label_for_category(category) } + + it { is_expected.to eq(expected_label) } + end + end +end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index cf9e0cccc71..8a9e78ba3c3 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -283,6 +283,96 @@ describe Gitlab::Git::Repository, :seed_helper do end end + describe '#diverging_commit_count' do + it 'counts 0 for the same branch' do + expect(repository.diverging_commit_count('master', 'master', max_count: 1000)).to eq([0, 0]) + end + + context 'max count does not truncate results' do + where(:left, :right, :expected) do + 1 | 1 | [1, 1] + 4 | 4 | [4, 4] + 2 | 2 | [2, 2] + 2 | 4 | [2, 4] + 4 | 2 | [4, 2] + 10 | 10 | [10, 10] + end + + with_them do + before do + repository.create_branch('left-branch', 'master') + repository.create_branch('right-branch', 'master') + + left.times do + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff') + end + + right.times do + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff') + end + end + + after do + repository.delete_branch('left-branch') + repository.delete_branch('right-branch') + end + + it 'returns the correct count bounding at max_count' do + branch_a_sha = repository_rugged.branches['left-branch'].target.oid + branch_b_sha = repository_rugged.branches['right-branch'].target.oid + + count = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: 1000) + + expect(count).to eq(expected) + end + end + end + + context 'max count truncates results' do + where(:left, :right, :max_count) do + 1 | 1 | 1 + 4 | 4 | 4 + 2 | 2 | 3 + 2 | 4 | 3 + 4 | 2 | 5 + 10 | 10 | 10 + end + + with_them do + before do + repository.create_branch('left-branch', 'master') + repository.create_branch('right-branch', 'master') + + left.times do + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff') + end + + right.times do + new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff') + end + end + + after do + repository.delete_branch('left-branch') + repository.delete_branch('right-branch') + end + + it 'returns the correct count bounding at max_count' do + branch_a_sha = repository_rugged.branches['left-branch'].target.oid + branch_b_sha = repository_rugged.branches['right-branch'].target.oid + + results = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: max_count) + + expect(results[0] + results[1]).to eq(max_count) + end + end + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :diverging_commit_count do + subject { repository.diverging_commit_count('master', 'master', max_count: 1000) } + end + end + describe '#has_local_branches?' do context 'check for local branches' do it { expect(repository.has_local_branches?).to eq(true) } diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 0f21b8843b6..15e59718dce 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -89,7 +89,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi description: 'This is my pull request', source_project_id: project.id, target_project_id: project.id, - source_branch: 'alice:feature', + source_branch: 'github/fork/alice/feature', target_branch: 'master', state: :merged, milestone_id: milestone.id, @@ -134,7 +134,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi description: "*Created by: alice*\n\nThis is my pull request", source_project_id: project.id, target_project_id: project.id, - source_branch: 'alice:feature', + source_branch: 'github/fork/alice/feature', target_branch: 'master', state: :merged, milestone_id: milestone.id, @@ -259,6 +259,40 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi .and_return(user.id) end + it 'does not create the source branch if merge request is merged' do + mr, exists = importer.create_merge_request + + importer.insert_git_data(mr, exists) + + expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end + + it 'creates the source branch if merge request is open' do + mr, exists = importer.create_merge_request + mr.state = 'opened' + mr.save + + importer.insert_git_data(mr, exists) + + expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end + + it 'ignores Git errors when creating a branch' do + mr, exists = importer.create_merge_request + mr.state = 'opened' + mr.save + + expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError) + expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original + + importer.insert_git_data(mr, exists) + + expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey + expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy + end + it 'creates the merge request diffs' do mr, exists = importer.create_merge_request diff --git a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb index 33f6ff0ae6a..d478e5ae899 100644 --- a/spec/lib/gitlab/github_import/representation/pull_request_spec.rb +++ b/spec/lib/gitlab/github_import/representation/pull_request_spec.rb @@ -238,7 +238,7 @@ describe Gitlab::GithubImport::Representation::PullRequest do target_repository_id: 2 ) - expect(pr.formatted_source_branch).to eq('foo:branch') + expect(pr.formatted_source_branch).to eq('github/fork/foo/branch') end end diff --git a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb new file mode 100644 index 00000000000..cf3a8bcc8b4 --- /dev/null +++ b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Graphql::Authorize::Instrumentation do + describe '#build_checker' do + let(:current_user) { double(:current_user) } + let(:abilities) { [double(:first_ability), double(:last_ability)] } + + let(:checker) do + described_class.new.__send__(:build_checker, current_user, abilities) + end + + it 'returns a checker which checks for a single object' do + object = double(:object) + + abilities.each do |ability| + spy_ability_check_for(ability, object, passed: true) + end + + expect(checker.call(object)).to eq(object) + end + + it 'returns a checker which checks for all objects' do + objects = [double(:first), double(:last)] + + abilities.each do |ability| + objects.each do |object| + spy_ability_check_for(ability, object, passed: true) + end + end + + expect(checker.call(objects)).to eq(objects) + end + + context 'when some objects would not pass the check' do + it 'returns nil when it is single object' do + disallowed = double(:object) + + spy_ability_check_for(abilities.first, disallowed, passed: false) + + expect(checker.call(disallowed)).to be_nil + end + + it 'returns only objects which passed when there are more than one' do + allowed = double(:allowed) + disallowed = double(:disallowed) + + spy_ability_check_for(abilities.first, disallowed, passed: false) + + abilities.each do |ability| + spy_ability_check_for(ability, allowed, passed: true) + end + + expect(checker.call([disallowed, allowed])) + .to contain_exactly(allowed) + end + end + + def spy_ability_check_for(ability, object, passed: true) + expect(Ability) + .to receive(:allowed?) + .with(current_user, ability, object) + .and_return(passed) + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 6897ac8a3a8..c15b360b563 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -219,7 +219,6 @@ project: - packagist_service - pivotaltracker_service - prometheus_service -- hipchat_service - flowdock_service - assembla_service - asana_service diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 58949f76bd6..1327f414498 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6775,28 +6775,6 @@ "wiki_page_events": true }, { - "id": 93, - "title": "HipChat", - "project_id": 5, - "created_at": "2016-06-14T15:01:51.219Z", - "updated_at": "2016-06-14T15:01:51.219Z", - "active": false, - "properties": { - "notify_only_broken_pipelines": true - }, - "template": false, - "push_events": true, - "issues_events": true, - "merge_requests_events": true, - "tag_push_events": true, - "note_events": true, - "pipeline_events": true, - "type": "HipchatService", - "category": "common", - "default": false, - "wiki_page_events": true - }, - { "id": 91, "title": "Flowdock", "project_id": 5, diff --git a/spec/lib/gitlab/import_export/shared_spec.rb b/spec/lib/gitlab/import_export/shared_spec.rb index f2d750c6595..2c288cff6ef 100644 --- a/spec/lib/gitlab/import_export/shared_spec.rb +++ b/spec/lib/gitlab/import_export/shared_spec.rb @@ -14,6 +14,16 @@ describe Gitlab::ImportExport::Shared do expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]']) end + it 'updates the import JID' do + import_state = create(:import_state, project: project, jid: 'jid-test') + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:error).with(hash_including(import_jid: import_state.jid)) + end + + subject.error(error) + end + it 'calls the error logger with the full message' do expect(subject).to receive(:log_error).with(hash_including(message: error.message)) diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb index 1ec1ba19e39..8961ecc4be0 100644 --- a/spec/lib/gitlab/lfs_token_spec.rb +++ b/spec/lib/gitlab/lfs_token_spec.rb @@ -4,10 +4,8 @@ require 'spec_helper' describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do describe '#token' do - shared_examples 'an LFS token generator' do + shared_examples 'a valid LFS token' do it 'returns a computed token' do - expect(Settings).to receive(:attr_encrypted_db_key_base).and_return('fbnbv6hdjweo53qka7kza8v8swxc413c05pb51qgtfte0bygh1p2e508468hfsn5ntmjcyiz7h1d92ashpet5pkdyejg7g8or3yryhuso4h8o5c73h429d9c3r6bjnet').twice - token = lfs_token.token expect(token).not_to be_nil @@ -20,11 +18,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:user, username: 'test_user_lfs_1') } let(:lfs_token) { described_class.new(actor) } - before do - allow(actor).to receive(:encrypted_password).and_return('$2a$04$ETfzVS5spE9Hexn9wh6NUenCHG1LyZ2YdciOYxieV1WLSa8DHqOFO') - end - - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq(actor.username) @@ -40,11 +34,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:key, user: user) } let(:lfs_token) { described_class.new(actor) } - before do - allow(user).to receive(:encrypted_password).and_return('$2a$04$C1GAMKsOKouEbhKy2JQoe./3LwOfQAZc.hC8zW2u/wt8xgukvnlV.') - end - - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq(user.username) @@ -65,7 +55,7 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do allow(actor).to receive(:id).and_return(actor_id) end - it_behaves_like 'an LFS token generator' + it_behaves_like 'a valid LFS token' it 'returns the correct username' do expect(lfs_token.actor_name).to eq("lfs+deploy-key-#{actor_id}") @@ -87,10 +77,6 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do let(:actor) { create(:user, username: 'test_user_lfs_1') } let(:lfs_token) { described_class.new(actor) } - before do - allow(actor).to receive(:encrypted_password).and_return('$2a$04$ETfzVS5spE9Hexn9wh6NUenCHG1LyZ2YdciOYxieV1WLSa8DHqOFO') - end - context 'for an HMAC token' do before do # We're not interested in testing LegacyRedisDeviseToken here @@ -240,4 +226,18 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do end end end + + describe '#authentication_payload' do + it 'returns a Hash designed for gitlab-shell' do + actor = create(:user) + lfs_token = described_class.new(actor) + repo_http_path = 'http://localhost/user/repo.git' + authentication_payload = lfs_token.authentication_payload(repo_http_path) + + expect(authentication_payload[:username]).to eq(actor.username) + expect(authentication_payload[:repository_http_path]).to eq(repo_http_path) + expect(authentication_payload[:lfs_token]).to be_a String + expect(authentication_payload[:expires_in]).to eq(described_class::DEFAULT_EXPIRE_TIME) + end + end end diff --git a/spec/lib/gitlab/sql/recursive_cte_spec.rb b/spec/lib/gitlab/sql/recursive_cte_spec.rb index 25146860615..7fe39dd5a96 100644 --- a/spec/lib/gitlab/sql/recursive_cte_spec.rb +++ b/spec/lib/gitlab/sql/recursive_cte_spec.rb @@ -31,6 +31,15 @@ describe Gitlab::SQL::RecursiveCTE, :postgresql do expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}") end + + it 'replaces dots with an underscore' do + table = Arel::Table.new('gitlab.kittens') + + source_name = ActiveRecord::Base.connection.quote_table_name(:cte_name) + alias_name = ActiveRecord::Base.connection.quote_table_name(:gitlab_kittens) + + expect(cte.alias_to(table).to_sql).to eq("#{source_name} AS #{alias_name}") + end end describe '#apply_to' do diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 4f5993ba226..d3eae80cc56 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -124,9 +124,15 @@ describe Gitlab::UsageData do todos uploads web_hooks + user_preferences )) end + it 'does not gather user preferences usage data when the feature is disabled' do + stub_feature_flags(group_overview_security_dashboard: false) + expect(subject[:counts].keys).not_to include(:user_preferences) + end + it 'gathers projects data correctly' do count_data = subject[:counts] diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 72a0df96a80..460b5c8cd31 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1172,8 +1172,26 @@ describe Ci::Pipeline, :mailer do pipeline.update_column(:before_sha, Gitlab::Git::BLANK_SHA) end - it 'raises an error' do - expect { pipeline.modified_paths }.to raise_error(ArgumentError) + it 'returns nil' do + expect(pipeline.modified_paths).to be_nil + end + end + + context 'when source is merge request' do + let(:pipeline) do + create(:ci_pipeline, source: :merge_request, merge_request: merge_request) + end + + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master') + end + + it 'returns merge request modified paths' do + expect(pipeline.modified_paths).to match(merge_request.modified_paths) end end end diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index 64f6d9c8bb4..f16eff92167 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -3,16 +3,17 @@ require 'rails_helper' describe Clusters::Applications::Helm do include_examples 'cluster application core specs', :clusters_applications_helm - describe '.installed' do - subject { described_class.installed } + describe '.available' do + subject { described_class.available } let!(:installed_cluster) { create(:clusters_applications_helm, :installed) } + let!(:updated_cluster) { create(:clusters_applications_helm, :updated) } before do create(:clusters_applications_helm, :errored) end - it { is_expected.to contain_exactly(installed_cluster) } + it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } end describe '#issue_client_cert' do diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 03ca18c6943..d5fd42509a3 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -16,18 +16,6 @@ describe Clusters::Applications::Ingress do allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async) end - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_ingress, :installed) } - - before do - create(:clusters_applications_ingress, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe '#make_installed!' do before do application.make_installed! diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index cd29e0d4f53..006b922ab27 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -24,30 +24,6 @@ describe Clusters::Applications::Knative do it { expect(knative_no_rbac).to be_not_installable } end - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_knative, :installed) } - - before do - create(:clusters_applications_knative, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - - describe '#make_installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_knative, :installed) } - - before do - create(:clusters_applications_knative, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe 'make_installed with external_ip' do before do application.make_installed! diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index caf59b0fc31..2250ef301aa 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -9,18 +9,6 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application helm specs', :clusters_applications_prometheus include_examples 'cluster application initial status specs' - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_prometheus, :installed) } - - before do - create(:clusters_applications_prometheus, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe 'transition to installed' do let(:project) { create(:project) } let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) } @@ -192,7 +180,7 @@ describe Clusters::Applications::Prometheus do end context 'with knative installed' do - let(:knative) { create(:clusters_applications_knative, :installed ) } + let(:knative) { create(:clusters_applications_knative, :updated ) } let(:prometheus) { create(:clusters_applications_prometheus, cluster: knative.cluster) } subject { prometheus.install_command } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index 38758ff97bc..5e5b25cbf8a 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -11,18 +11,6 @@ describe Clusters::Applications::Runner do it { is_expected.to belong_to(:runner) } - describe '.installed' do - subject { described_class.installed } - - let!(:cluster) { create(:clusters_applications_runner, :installed) } - - before do - create(:clusters_applications_runner, :errored) - end - - it { is_expected.to contain_exactly(cluster) } - end - describe '#install_command' do let(:kubeclient) { double('kubernetes client') } let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) } diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 92ce2b0999a..3feed4e9718 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -265,12 +265,12 @@ describe Clusters::Cluster do it { is_expected.to be_valid } end - context 'when cluster has an invalid domain' do - let(:cluster) { build(:cluster, domain: 'not-valid-domain') } + context 'when cluster is not a valid hostname' do + let(:cluster) { build(:cluster, domain: 'http://not.a.valid.hostname') } it 'should add an error on domain' do expect(subject).not_to be_valid - expect(subject.errors[:domain].first).to eq('is not a fully qualified domain name') + expect(subject.errors[:domain].first).to eq('contains invalid characters (valid characters: [a-z0-9\\-])') end end diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb index 12e59b35428..0f5d03ff458 100644 --- a/spec/models/commit_collection_spec.rb +++ b/spec/models/commit_collection_spec.rb @@ -12,26 +12,26 @@ describe CommitCollection do end end - describe '.committers' do + describe '.authors' do it 'returns a relation of users when users are found' do - user = create(:user, email: commit.committer_email.upcase) + user = create(:user, email: commit.author_email.upcase) collection = described_class.new(project, [commit]) - expect(collection.committers).to contain_exactly(user) + expect(collection.authors).to contain_exactly(user) end - it 'returns empty array when committers cannot be found' do + it 'returns empty array when authors cannot be found' do collection = described_class.new(project, [commit]) - expect(collection.committers).to be_empty + expect(collection.authors).to be_empty end it 'excludes authors of merge commits' do commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98") - create(:user, email: commit.committer_email.upcase) + create(:user, email: commit.author_email.upcase) collection = described_class.new(project, [commit]) - expect(collection.committers).to be_empty + expect(collection.authors).to be_empty end end diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index 97a4c212f1c..03ae45e6b17 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -25,7 +25,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do def result with_reactive_cache do |data| - data / 2 + data end end end @@ -64,7 +64,7 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do stub_reactive_cache(instance, 4) end - it { is_expected.to eq(2) } + it { is_expected.to eq(4) } it 'does not enqueue a background worker' do expect(ReactiveCachingWorker).not_to receive(:perform_async) @@ -94,6 +94,14 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end end end + + context 'when cache contains non-nil but blank value' do + before do + stub_reactive_cache(instance, false) + end + + it { is_expected.to eq(false) } + end end describe '#clear_reactive_cache!' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index afa87b8a62d..82a853a23b9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -435,6 +435,7 @@ describe MergeRequest do it 'does not cache issues from external trackers' do issue = ExternalIssue.new('JIRA-123', subject.project) commit = double('commit1', safe_message: "Fixes #{issue.to_reference}") + allow(subject).to receive(:commits).and_return([commit]) expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error @@ -1023,23 +1024,23 @@ describe MergeRequest do end end - describe '#committers' do - it 'returns all the committers of every commit in the merge request' do - users = subject.commits.map(&:committer_email).uniq.map do |email| + describe '#commit_authors' do + it 'returns all the authors of every commit in the merge request' do + users = subject.commits.map(&:author_email).uniq.map do |email| create(:user, email: email) end - expect(subject.committers).to match_array(users) + expect(subject.commit_authors).to match_array(users) end - it 'returns an empty array if no committer is associated with a user' do - expect(subject.committers).to be_empty + it 'returns an empty array if no author is associated with a user' do + expect(subject.commit_authors).to be_empty end end describe '#authors' do - it 'returns a list with all the committers in the merge request and author' do - users = subject.commits.map(&:committer_email).uniq.map do |email| + it 'returns a list with all the commit authors in the merge request and author' do + users = subject.commits.map(&:author_email).uniq.map do |email| create(:user, email: email) end @@ -2604,8 +2605,9 @@ describe MergeRequest do let!(:first_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) } let!(:last_pipeline) { create(:ci_pipeline_without_jobs, pipeline_arguments) } + let!(:last_pipeline_with_other_ref) { create(:ci_pipeline_without_jobs, pipeline_arguments.merge(ref: 'other')) } - it 'returns latest pipeline' do + it 'returns latest pipeline for the target branch' do expect(merge_request.base_pipeline).to eq(last_pipeline) end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb deleted file mode 100644 index b0fd2ceead0..00000000000 --- a/spec/models/project_services/hipchat_service_spec.rb +++ /dev/null @@ -1,408 +0,0 @@ -require 'spec_helper' - -describe HipchatService do - describe "Associations" do - it { is_expected.to belong_to :project } - it { is_expected.to have_one :service_hook } - end - - describe 'Validations' do - context 'when service is active' do - before do - subject.active = true - end - - it { is_expected.to validate_presence_of(:token) } - end - - context 'when service is inactive' do - before do - subject.active = false - end - - it { is_expected.not_to validate_presence_of(:token) } - end - end - - describe "Execute" do - let(:hipchat) { described_class.new } - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' } - let(:project_name) { project.full_name.gsub(/\s/, '') } - let(:token) { 'verySecret' } - let(:server_url) { 'https://hipchat.example.com'} - let(:push_sample_data) do - Gitlab::DataBuilder::Push.build_sample(project, user) - end - - before do - allow(hipchat).to receive_messages( - project_id: project.id, - project: project, - room: 123456, - server: server_url, - token: token - ) - WebMock.stub_request(:post, api_url) - end - - it 'tests and return errors' do - allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room') - result = hipchat.test(push_sample_data) - - expect(result[:success]).to be_falsey - expect(result[:result].to_s).to eq('no such room') - end - - it 'uses v1 if version is provided' do - allow(hipchat).to receive(:api_version).and_return('v1') - expect(HipChat::Client).to receive(:new).with( - token, - api_version: 'v1', - server_url: server_url - ).and_return(double(:hipchat_service).as_null_object) - hipchat.execute(push_sample_data) - end - - it 'uses v2 as the version when nothing is provided' do - allow(hipchat).to receive(:api_version).and_return('') - expect(HipChat::Client).to receive(:new).with( - token, - api_version: 'v2', - server_url: server_url - ).and_return(double(:hipchat_service).as_null_object) - hipchat.execute(push_sample_data) - end - - context 'push events' do - it "calls Hipchat API for push events" do - hipchat.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates a push message" do - message = hipchat.send(:create_push_message, push_sample_data) - - push_sample_data[:object_attributes] - branch = push_sample_data[:ref].gsub('refs/heads/', '') - expect(message).to include("#{user.name} pushed to branch " \ - "<a href=\"#{project.web_url}/commits/#{branch}\">#{branch}</a> of " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>") - end - end - - context 'tag_push events' do - let(:push_sample_data) do - Gitlab::DataBuilder::Push.build( - project, - user, - Gitlab::Git::BLANK_SHA, - '1' * 40, - 'refs/tags/test', - []) - end - - it "calls Hipchat API for tag push events" do - hipchat.execute(push_sample_data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates a tag push message" do - message = hipchat.send(:create_push_message, push_sample_data) - - push_sample_data[:object_attributes] - expect(message).to eq("#{user.name} pushed new tag " \ - "<a href=\"#{project.web_url}/commits/test\">test</a> to " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>\n") - end - end - - context 'issue events' do - let(:issue) { create(:issue, title: 'Awesome issue', description: '**please** fix') } - let(:issue_service) { Issues::CreateService.new(project, user) } - let(:issues_sample_data) { issue_service.hook_data(issue, 'open') } - - it "calls Hipchat API for issue events" do - hipchat.execute(issues_sample_data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates an issue message" do - message = hipchat.send(:create_issue_message, issues_sample_data) - - obj_attr = issues_sample_data[:object_attributes] - expect(message).to eq("#{user.name} opened " \ - "<a href=\"#{obj_attr[:url]}\">issue ##{obj_attr["iid"]}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "<b>Awesome issue</b>" \ - "<pre><strong>please</strong> fix</pre>") - end - end - - context 'merge request events' do - let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) } - let(:merge_service) { MergeRequests::CreateService.new(project, user) } - let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') } - - it "calls Hipchat API for merge requests events" do - hipchat.execute(merge_sample_data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates a merge request message" do - message = hipchat.send(:create_merge_request_message, - merge_sample_data) - - obj_attr = merge_sample_data[:object_attributes] - expect(message).to eq("#{user.name} opened " \ - "<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "<b>Awesome merge request</b>" \ - "<pre><strong>please</strong> fix</pre>") - end - end - - context "Note events" do - let(:user) { create(:user) } - let(:project) { create(:project, :repository, creator: user) } - - context 'when commit comment event triggered' do - let(:commit_note) do - create(:note_on_commit, author: user, project: project, - commit_id: project.repository.commit.id, - note: 'a comment on a commit') - end - - it "calls Hipchat API for commit comment events" do - data = Gitlab::DataBuilder::Note.build(commit_note, user) - hipchat.execute(data) - - expect(WebMock).to have_requested(:post, api_url).once - - message = hipchat.send(:create_message, data) - - obj_attr = data[:object_attributes] - commit_id = Commit.truncate_sha(data[:commit][:id]) - title = hipchat.send(:format_title, data[:commit][:message]) - - expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "#{title}" \ - "<pre>a comment on a commit</pre>") - end - end - - context 'when merge request comment event triggered' do - let(:merge_request) do - create(:merge_request, source_project: project, - target_project: project) - end - - let(:merge_request_note) do - create(:note_on_merge_request, noteable: merge_request, - project: project, - note: "merge request **note**") - end - - it "calls Hipchat API for merge request comment events" do - data = Gitlab::DataBuilder::Note.build(merge_request_note, user) - hipchat.execute(data) - - expect(WebMock).to have_requested(:post, api_url).once - - message = hipchat.send(:create_message, data) - - obj_attr = data[:object_attributes] - merge_id = data[:merge_request]['iid'] - title = data[:merge_request]['title'] - - expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "<b>#{title}</b>" \ - "<pre>merge request <strong>note</strong></pre>") - end - end - - context 'when issue comment event triggered' do - let(:issue) { create(:issue, project: project) } - let(:issue_note) do - create(:note_on_issue, noteable: issue, project: project, - note: "issue **note**") - end - - it "calls Hipchat API for issue comment events" do - data = Gitlab::DataBuilder::Note.build(issue_note, user) - hipchat.execute(data) - - message = hipchat.send(:create_message, data) - - obj_attr = data[:object_attributes] - issue_id = data[:issue]['iid'] - title = data[:issue]['title'] - - expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "<b>#{title}</b>" \ - "<pre>issue <strong>note</strong></pre>") - end - - context 'with confidential issue' do - before do - issue.update!(confidential: true) - end - - it 'calls Hipchat API with issue comment' do - data = Gitlab::DataBuilder::Note.build(issue_note, user) - hipchat.execute(data) - - message = hipchat.send(:create_message, data) - - expect(message).to include("<pre>issue <strong>note</strong></pre>") - end - end - end - - context 'when snippet comment event triggered' do - let(:snippet) { create(:project_snippet, project: project) } - let(:snippet_note) do - create(:note_on_project_snippet, noteable: snippet, - project: project, - note: "snippet note") - end - - it "calls Hipchat API for snippet comment events" do - data = Gitlab::DataBuilder::Note.build(snippet_note, user) - hipchat.execute(data) - - expect(WebMock).to have_requested(:post, api_url).once - - message = hipchat.send(:create_message, data) - - obj_attr = data[:object_attributes] - snippet_id = data[:snippet]['id'] - title = data[:snippet]['title'] - - expect(message).to eq("#{user.name} commented on " \ - "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \ - "<a href=\"#{project.web_url}\">#{project_name}</a>: " \ - "<b>#{title}</b>" \ - "<pre>snippet note</pre>") - end - end - end - - context 'pipeline events' do - let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) } - let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } - - context 'for failed' do - before do - pipeline.drop - end - - it "calls Hipchat API" do - hipchat.execute(data) - - expect(WebMock).to have_requested(:post, api_url).once - end - - it "creates a build message" do - message = hipchat.__send__(:create_pipeline_message, data) - - project_url = project.web_url - project_name = project.full_name.gsub(/\s/, '') - pipeline_attributes = data[:object_attributes] - ref = pipeline_attributes[:ref] - ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch' - duration = pipeline_attributes[:duration] - user_name = data[:user][:name] - - expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \ - "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \ - "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \ - "by #{user_name} failed in #{duration} second(s)") - end - end - - context 'for succeeded' do - before do - pipeline.succeed - end - - it "calls Hipchat API" do - hipchat.notify_only_broken_pipelines = false - hipchat.execute(data) - expect(WebMock).to have_requested(:post, api_url).once - end - - it "notifies only broken" do - hipchat.notify_only_broken_pipelines = true - hipchat.execute(data) - expect(WebMock).not_to have_requested(:post, api_url).once - end - end - end - - context "#message_options" do - it "is set to the defaults" do - expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' }) - end - - it "sets notify to true" do - allow(hipchat).to receive(:notify).and_return('1') - - expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' }) - end - - it "sets the color" do - allow(hipchat).to receive(:color).and_return('red') - - expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' }) - end - - context 'with a successful build' do - it 'uses the green color' do - data = { object_kind: 'pipeline', - object_attributes: { status: 'success' } } - - expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' }) - end - end - - context 'with a failed build' do - it 'uses the red color' do - data = { object_kind: 'pipeline', - object_attributes: { status: 'failed' } } - - expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' }) - end - end - end - end - - context 'with UrlBlocker' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:hipchat) { described_class.new(project: project) } - let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) } - - describe '#execute' do - before do - hipchat.server = 'http://localhost:9123' - end - - it 'raises UrlBlocker for localhost' do - expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original - expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError) - end - end - end -end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ac9362339e5..1f9088c2e6b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -41,7 +41,6 @@ describe Project do it { is_expected.to have_one(:pipelines_email_service) } it { is_expected.to have_one(:irker_service) } it { is_expected.to have_one(:pivotaltracker_service) } - it { is_expected.to have_one(:hipchat_service) } it { is_expected.to have_one(:flowdock_service) } it { is_expected.to have_one(:assembla_service) } it { is_expected.to have_one(:slack_slash_commands_service) } @@ -4603,6 +4602,21 @@ describe Project do end end + describe '#has_pool_repsitory?' do + it 'returns false when it does not have a pool repository' do + subject = create(:project, :repository) + + expect(subject.has_pool_repository?).to be false + end + + it 'returns true when it has a pool repository' do + pool = create(:pool_repository, :ready) + subject = create(:project, :repository, pool_repository: pool) + + expect(subject.has_pool_repository?).to be true + end + end + def rugged_config rugged_repo(project.repository).config end diff --git a/spec/models/releases/link_spec.rb b/spec/models/releases/link_spec.rb index 06ed1438688..4dd26c976cc 100644 --- a/spec/models/releases/link_spec.rb +++ b/spec/models/releases/link_spec.rb @@ -77,4 +77,28 @@ describe Releases::Link do it { is_expected.to be_truthy } end + + describe 'supported protocols' do + where(:protocol) do + %w(http https ftp) + end + + with_them do + let(:link) { build(:release_link, url: protocol + '://assets.com/download') } + + it 'will be valid' do + expect(link).to be_valid + end + end + end + + describe 'unsupported protocol' do + context 'for torrent' do + let(:link) { build(:release_link, url: 'torrent://assets.com/download') } + + it 'will be invalid' do + expect(link).to be_invalid + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 78477ab0a5a..1edd8e69b8f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -925,6 +925,21 @@ describe User do expect(user.manageable_groups).to contain_exactly(group, subgroup) end end + + describe '#manageable_groups_with_routes' do + it 'eager loads routes from manageable groups' do + control_count = + ActiveRecord::QueryRecorder.new(skip_cached: false) do + user.manageable_groups_with_routes.map(&:route) + end.count + + create(:group, parent: subgroup) + + expect do + user.manageable_groups_with_routes.map(&:route) + end.not_to exceed_all_query_limit(control_count) + end + end end end diff --git a/spec/policies/board_policy_spec.rb b/spec/policies/board_policy_spec.rb new file mode 100644 index 00000000000..4b76d65ef69 --- /dev/null +++ b/spec/policies/board_policy_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe BoardPolicy do + let(:user) { create(:user) } + let(:project) { create(:project, :private) } + let(:group) { create(:group, :private) } + let(:group_board) { create(:board, group: group) } + let(:project_board) { create(:board, project: project) } + + let(:board_permissions) do + [ + :read_parent, + :read_milestone, + :read_issue + ] + end + + def expect_allowed(*permissions) + permissions.each { |p| is_expected.to be_allowed(p) } + end + + def expect_disallowed(*permissions) + permissions.each { |p| is_expected.not_to be_allowed(p) } + end + + context 'group board' do + subject { described_class.new(user, group_board) } + + context 'user has access' do + before do + group.add_developer(user) + end + + it do + expect_allowed(*board_permissions) + end + end + + context 'user does not have access' do + it do + expect_disallowed(*board_permissions) + end + end + end + + context 'project board' do + subject { described_class.new(user, project_board) } + + context 'user has access' do + before do + project.add_developer(user) + end + + it do + expect_allowed(*board_permissions) + end + end + + context 'user does not have access' do + it do + expect_disallowed(*board_permissions) + end + end + end +end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 6b9bc6eda6a..066f1d6862a 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -237,7 +237,7 @@ describe API::Commits do end describe 'create' do - let(:message) { 'Created file' } + let(:message) { 'Created a new file with a very very looooooooooooooooooooooooooooooooooooooooooooooong commit message' } let(:invalid_c_params) do { branch: 'master', @@ -1457,4 +1457,42 @@ describe API::Commits do expect(response).to have_gitlab_http_status(404) end end + + describe 'GET /projects/:id/repository/commits/:sha/signature' do + let!(:project) { create(:project, :repository, :public) } + let(:project_id) { project.id } + let(:commit_id) { project.repository.commit.id } + let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/signature" } + + context 'when commit 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 'unsigned commit' do + it_behaves_like '404 response' do + let(:request) { get api(route, current_user) } + let(:message) { '404 GPG Signature Not Found'} + end + end + + context 'signed commit' do + let(:commit) { project.repository.commit(GpgHelpers::SIGNED_COMMIT_SHA) } + let(:commit_id) { commit.id } + + it 'returns correct JSON' do + get api(route, current_user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['gpg_key_id']).to eq(commit.signature.gpg_key_id) + expect(json_response['gpg_key_subkey_id']).to eq(commit.signature.gpg_key_subkey_id) + expect(json_response['gpg_key_primary_keyid']).to eq(commit.signature.gpg_key_primary_keyid) + expect(json_response['verification_status']).to eq(commit.signature.verification_status) + end + end + end end diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb index 22a9e36ca31..57a57e69a00 100644 --- a/spec/requests/api/features_spec.rb +++ b/spec/requests/api/features_spec.rb @@ -163,6 +163,40 @@ describe API::Features do end end + context 'when enabling for a group by path' do + context 'when the group exists' do + it 'sets the feature gate' do + group = create(:group) + + post api("/features/#{feature_name}", admin), params: { value: 'true', group: group.full_path } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + 'name' => 'my_feature', + 'state' => 'conditional', + 'gates' => [ + { 'key' => 'boolean', 'value' => false }, + { 'key' => 'actors', 'value' => ["Group:#{group.id}"] } + ]) + end + end + + context 'when the group does not exist' do + it 'sets no new values and keeps the feature disabled' do + post api("/features/#{feature_name}", admin), params: { value: 'true', group: 'not/a/group' } + + expect(response).to have_gitlab_http_status(201) + expect(json_response).to eq( + "name" => "my_feature", + "state" => "off", + "gates" => [ + { "key" => "boolean", "value" => false } + ] + ) + end + end + end + it 'creates a feature with the given percentage if passed an integer' do post api("/features/#{feature_name}", admin), params: { value: '50' } diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb index 355336ad7e2..c2934430821 100644 --- a/spec/requests/api/graphql/project/issues_spec.rb +++ b/spec/requests/api/graphql/project/issues_spec.rb @@ -56,4 +56,38 @@ describe 'getting an issue list for a project' do expect(issues_data).to eq [] end end + + context 'when there is a confidential issue' do + let!(:confidential_issue) do + create(:issue, :confidential, project: project) + end + + context 'when the user cannot see confidential issues' do + it 'returns issues without confidential issues' do + post_graphql(query, current_user: current_user) + + expect(issues_data.size).to eq(2) + + issues_data.each do |issue| + expect(issue.dig('node', 'confidential')).to eq(false) + end + end + end + + context 'when the user can see confidential issues' do + it 'returns issues with confidential issues' do + project.add_developer(current_user) + + post_graphql(query, current_user: current_user) + + expect(issues_data.size).to eq(3) + + confidentials = issues_data.map do |issue| + issue.dig('node', 'confidential') + end + + expect(confidentials).to eq([true, false, false]) + end + end + end end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 6a943b5237a..cd85151ec1b 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -167,6 +167,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response['username']).to eq(user.username) expect(json_response['repository_http_path']).to eq(project.http_url_to_repo) + expect(json_response['expires_in']).to eq(Gitlab::LfsToken::DEFAULT_EXPIRE_TIME) expect(Gitlab::LfsToken.new(key).token_valid?(json_response['lfs_token'])).to be_truthy end @@ -324,6 +325,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') + expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user.reload.last_activity_on).to be_nil end @@ -336,6 +338,7 @@ describe API::Internal do expect(response).to have_gitlab_http_status(200) expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') + expect(json_response["gl_project_path"]).to eq(project.wiki.full_path) expect(json_response["gl_repository"]).to eq("wiki-#{project.id}") expect(user.reload.last_activity_on).to eql(Date.today) end @@ -349,6 +352,7 @@ describe API::Internal do expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gitaly"]).not_to be_nil expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) @@ -368,6 +372,7 @@ describe API::Internal do expect(json_response["status"]).to be_truthy expect(json_response["repository_path"]).to eq('/') expect(json_response["gl_repository"]).to eq("project-#{project.id}") + expect(json_response["gl_project_path"]).to eq(project.full_path) expect(json_response["gitaly"]).not_to be_nil expect(json_response["gitaly"]["repository"]).not_to be_nil expect(json_response["gitaly"]["repository"]["storage_name"]).to eq(project.repository.gitaly_repository.storage_name) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 04908378a24..d10ee6cc320 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -359,10 +359,38 @@ describe API::Issues do expect_paginated_array_response([]) end - it 'sorts by created_at descending by default' do - get api('/issues', user) + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api('/issues', user) - expect_paginated_array_response([issue.id, closed_issue.id]) + expect_paginated_array_response([issue.id, closed_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:closed_issue2) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: closed_issue.created_at, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + + it 'page breaks first page correctly' do + get api('/issues?per_page=2', user) + + expect_paginated_array_response([issue.id, closed_issue2.id]) + end + + it 'page breaks second page correctly' do + get api('/issues?per_page=2&page=2', user) + + expect_paginated_array_response([closed_issue.id]) + end + end end it 'sorts ascending when requested' do @@ -393,6 +421,24 @@ describe API::Issues do expect(response).to have_gitlab_http_status(200) expect(response).to match_response_schema('public_api/v4/issues') end + + it 'returns a related merge request count of 0 if there are no related merge requests' do + get api('/issues', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + expect(json_response.first).to include('merge_requests_count' => 0) + end + + it 'returns a related merge request count > 0 if there are related merge requests' do + create(:merge_requests_closing_issues, issue: issue) + + get api('/issues', user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/issues') + expect(json_response.first).to include('merge_requests_count' => 1) + end end end @@ -613,10 +659,38 @@ describe API::Issues do expect_paginated_array_response(group_confidential_issue.id) end - it 'sorts by created_at descending by default' do - get api(base_url, user) + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api(base_url, user) - expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:group_issue2) do + create :issue, + author: user, + assignees: [user], + project: group_project, + milestone: group_milestone, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description, + created_at: group_issue.created_at + end + + it 'page breaks first page correctly' do + get api("#{base_url}?per_page=3", user) + + expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id]) + end + + it 'page breaks second page correctly' do + get api("#{base_url}?per_page=3&page=2", user) + + expect_paginated_array_response([group_issue.id]) + end + end end it 'sorts ascending when requested' do @@ -828,10 +902,38 @@ describe API::Issues do expect_paginated_array_response([issue.id, closed_issue.id]) end - it 'sorts by created_at descending by default' do - get api("#{base_url}/issues", user) + context 'without sort params' do + it 'sorts by created_at descending by default' do + get api("#{base_url}/issues", user) - expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id]) + end + + context 'with 2 issues with same created_at' do + let!(:closed_issue2) do + create :closed_issue, + author: user, + assignees: [user], + project: project, + milestone: milestone, + created_at: closed_issue.created_at, + updated_at: 1.hour.ago, + title: issue_title, + description: issue_description + end + + it 'page breaks first page correctly' do + get api("#{base_url}/issues?per_page=3", user) + + expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id]) + end + + it 'page breaks second page correctly' do + get api("#{base_url}/issues?per_page=3&page=2", user) + + expect_paginated_array_response([closed_issue.id]) + end + end end it 'sorts ascending when requested' do @@ -1792,7 +1894,7 @@ describe API::Issues do description: "See #{issue.to_reference}" } create(:merge_request, attributes).tap do |merge_request| - create(:note, :system, project: project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) + create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true)) end end @@ -1829,6 +1931,24 @@ describe API::Issues do expect_paginated_array_response(related_mr.id) end + it 'returns merge requests cross-project wide' do + project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace) + merge_request = create_referencing_mr(user, project2, issue) + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response([related_mr.id, merge_request.id]) + end + + it 'does not generate references to projects with no access' do + private_project = create(:project, :private) + create_referencing_mr(private_project.creator, private_project, issue) + + get_related_merge_requests(project.id, issue.iid, user) + + expect_paginated_array_response(related_mr.id) + end + context 'no merge request mentioned a issue' do it 'returns empty array' do get_related_merge_requests(project.id, closed_issue.iid, user) diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb index 49eea2e362b..518181e4d93 100644 --- a/spec/requests/api/labels_spec.rb +++ b/spec/requests/api/labels_spec.rb @@ -20,9 +20,9 @@ describe API::Labels do create(:labeled_merge_request, labels: [priority_label], author: user, source_project: project ) expected_keys = %w( - id name color description + id name color text_color description open_issues_count closed_issues_count open_merge_requests_count - subscribed priority + subscribed priority is_project_label ) get api("/projects/#{project.id}/labels", user) @@ -43,27 +43,33 @@ describe API::Labels do expect(label1_response['open_merge_requests_count']).to eq(0) expect(label1_response['name']).to eq(label1.name) expect(label1_response['color']).to be_present + expect(label1_response['text_color']).to be_present expect(label1_response['description']).to be_nil expect(label1_response['priority']).to be_nil expect(label1_response['subscribed']).to be_falsey + expect(label1_response['is_project_label']).to be_truthy expect(group_label_response['open_issues_count']).to eq(1) expect(group_label_response['closed_issues_count']).to eq(0) expect(group_label_response['open_merge_requests_count']).to eq(0) expect(group_label_response['name']).to eq(group_label.name) expect(group_label_response['color']).to be_present + expect(group_label_response['text_color']).to be_present expect(group_label_response['description']).to be_nil expect(group_label_response['priority']).to be_nil expect(group_label_response['subscribed']).to be_falsey + expect(group_label_response['is_project_label']).to be_falsey expect(priority_label_response['open_issues_count']).to eq(0) expect(priority_label_response['closed_issues_count']).to eq(0) expect(priority_label_response['open_merge_requests_count']).to eq(1) expect(priority_label_response['name']).to eq(priority_label.name) expect(priority_label_response['color']).to be_present + expect(priority_label_response['text_color']).to be_present expect(priority_label_response['description']).to be_nil expect(priority_label_response['priority']).to eq(3) expect(priority_label_response['subscribed']).to be_falsey + expect(priority_label_response['is_project_label']).to be_truthy end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 0f5f6e38819..b8426126bc6 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -372,6 +372,7 @@ describe API::MergeRequests do expect(json_response['force_close_merge_request']).to be_falsy expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size) expect(json_response['merge_error']).to eq(merge_request.merge_error) + expect(json_response['user']['can_merge']).to be_truthy expect(json_response).not_to include('rebase_in_progress') end @@ -499,6 +500,15 @@ describe API::MergeRequests do expect(json_response['allow_maintainer_to_push']).to be_truthy end end + + it 'indicates if a user cannot merge the MR' do + user2 = create(:user) + project.add_reporter(user2) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user2) + + expect(json_response['user']['can_merge']).to be_falsy + end end describe 'GET /projects/:id/merge_requests/:merge_request_iid/participants' do diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index 6109829aad1..d1b58aac104 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -100,6 +100,8 @@ describe API::Wikis do shared_examples_for 'updates wiki page' do it 'updates the wiki page' do + put(api(url, user), params: payload) + expect(response).to have_gitlab_http_status(200) expect(json_response.size).to eq(4) expect(json_response.keys).to match_array(expected_keys_with_content) @@ -107,6 +109,16 @@ describe API::Wikis do expect(json_response['slug']).to eq(payload[:title].tr(' ', '-')) expect(json_response['title']).to eq(payload[:title]) end + + [:title, :content, :format].each do |part| + it "it updates with wiki with missing #{part}" do + payload.delete(part) + + put(api(url, user), params: payload) + + expect(response).to have_gitlab_http_status(200) + end + end end shared_examples_for '403 Forbidden' do @@ -528,8 +540,6 @@ describe API::Wikis do context 'when user is developer' do before do project.add_developer(user) - - put(api(url, user), params: payload) end include_examples 'updates wiki page' @@ -537,6 +547,10 @@ describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + put(api(url, user), params: payload) + end + include_examples '404 Wiki Page Not Found' end end @@ -544,8 +558,6 @@ describe API::Wikis do context 'when user is maintainer' do before do project.add_maintainer(user) - - put(api(url, user), params: payload) end include_examples 'updates wiki page' @@ -553,6 +565,10 @@ describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + put(api(url, user), params: payload) + end + include_examples '404 Wiki Page Not Found' end end @@ -572,8 +588,6 @@ describe API::Wikis do context 'when user is developer' do before do project.add_developer(user) - - put(api(url, user), params: payload) end include_examples 'updates wiki page' @@ -581,6 +595,10 @@ describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + put(api(url, user), params: payload) + end + include_examples '404 Wiki Page Not Found' end end @@ -588,8 +606,6 @@ describe API::Wikis do context 'when user is maintainer' do before do project.add_maintainer(user) - - put(api(url, user), params: payload) end include_examples 'updates wiki page' @@ -597,6 +613,10 @@ describe API::Wikis do context 'when page is not existing' do let(:url) { "/projects/#{project.id}/wikis/unknown" } + before do + put(api(url, user), params: payload) + end + include_examples '404 Wiki Page Not Found' end end @@ -605,10 +625,6 @@ describe API::Wikis do context 'when wiki belongs to a group project' do let(:project) { create(:project, :wiki_repo, namespace: group) } - before do - put(api(url, user), params: payload) - end - include_examples 'updates wiki page' end end diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb index 78ff9c6e6fd..106f92082e4 100644 --- a/spec/routing/import_routing_spec.rb +++ b/spec/routing/import_routing_spec.rb @@ -23,6 +23,11 @@ require 'spec_helper' # end shared_examples 'importer routing' do let(:except_actions) { [] } + let(:is_realtime) { false } + + before do + except_actions.push(is_realtime ? :jobs : :realtime_changes) + end it 'to #create' do expect(post("/import/#{provider}")).to route_to("import/#{provider}#create") unless except_actions.include?(:create) @@ -43,17 +48,22 @@ shared_examples 'importer routing' do it 'to #jobs' do expect(get("/import/#{provider}/jobs")).to route_to("import/#{provider}#jobs") unless except_actions.include?(:jobs) end + + it 'to #realtime_changes' do + expect(get("/import/#{provider}/realtime_changes")).to route_to("import/#{provider}#realtime_changes") unless except_actions.include?(:realtime_changes) + end end # personal_access_token_import_github POST /import/github/personal_access_token(.:format) import/github#personal_access_token # status_import_github GET /import/github/status(.:format) import/github#status # callback_import_github GET /import/github/callback(.:format) import/github#callback -# jobs_import_github GET /import/github/jobs(.:format) import/github#jobs +# realtime_changes_import_github GET /import/github/realtime_changes(.:format) import/github#jobs # import_github POST /import/github(.:format) import/github#create # new_import_github GET /import/github/new(.:format) import/github#new describe Import::GithubController, 'routing' do it_behaves_like 'importer routing' do let(:provider) { 'github' } + let(:is_realtime) { true } end it 'to #personal_access_token' do @@ -63,13 +73,14 @@ end # personal_access_token_import_gitea POST /import/gitea/personal_access_token(.:format) import/gitea#personal_access_token # status_import_gitea GET /import/gitea/status(.:format) import/gitea#status -# jobs_import_gitea GET /import/gitea/jobs(.:format) import/gitea#jobs +# realtime_changes_import_gitea GET /import/gitea/realtime_changes(.:format) import/gitea#jobs # import_gitea POST /import/gitea(.:format) import/gitea#create # new_import_gitea GET /import/gitea/new(.:format) import/gitea#new describe Import::GiteaController, 'routing' do it_behaves_like 'importer routing' do let(:except_actions) { [:callback] } let(:provider) { 'gitea' } + let(:is_realtime) { true } end it 'to #personal_access_token' do diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb index 073c13c2cbb..92b649f5b6c 100644 --- a/spec/serializers/diff_file_entity_spec.rb +++ b/spec/serializers/diff_file_entity_spec.rb @@ -22,7 +22,7 @@ describe DiffFileEntity do let(:request) { EntityRequest.new(project: project, current_user: user) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:entity) { described_class.new(diff_file, request: request, merge_request: merge_request) } - let(:exposed_urls) { %i(load_collapsed_diff_url edit_path view_path context_lines_path) } + let(:exposed_urls) { %i(edit_path view_path context_lines_path) } it_behaves_like 'diff file entity' @@ -38,6 +38,12 @@ describe DiffFileEntity do expect(response[attribute]).to include(merge_request.target_project.to_param) end end + + it 'exposes load_collapsed_diff_url if the file viewer is collapsed' do + allow(diff_file.viewer).to receive(:collapsed?) { true } + + expect(subject).to include(:load_collapsed_diff_url) + end end context '#parallel_diff_lines' do diff --git a/spec/serializers/namespace_basic_entity_spec.rb b/spec/serializers/namespace_basic_entity_spec.rb new file mode 100644 index 00000000000..f8b71ceb9f3 --- /dev/null +++ b/spec/serializers/namespace_basic_entity_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe NamespaceBasicEntity do + set(:group) { create(:group) } + let(:entity) do + described_class.represent(group) + end + + describe '#as_json' do + subject { entity.as_json } + + it 'includes required fields' do + expect(subject).to include :id, :full_path + end + end +end diff --git a/spec/serializers/namespace_serializer_spec.rb b/spec/serializers/namespace_serializer_spec.rb new file mode 100644 index 00000000000..6e5bdd8c52d --- /dev/null +++ b/spec/serializers/namespace_serializer_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe NamespaceSerializer do + it 'represents NamespaceBasicEntity entities' do + expect(described_class.entity_class).to eq(NamespaceBasicEntity) + end +end diff --git a/spec/serializers/project_import_entity_spec.rb b/spec/serializers/project_import_entity_spec.rb new file mode 100644 index 00000000000..e476da82729 --- /dev/null +++ b/spec/serializers/project_import_entity_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectImportEntity do + include ImportHelper + + set(:project) { create(:project, import_status: :started, import_source: 'namespace/project') } + let(:provider_url) { 'https://provider.com' } + let(:entity) { described_class.represent(project, provider_url: provider_url) } + + describe '#as_json' do + subject { entity.as_json } + + it 'includes required fields' do + expect(subject[:import_source]).to eq(project.import_source) + expect(subject[:import_status]).to eq(project.import_status) + expect(subject[:human_import_status_name]).to eq(project.human_import_status_name) + expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, project[:import_source])) + end + end +end diff --git a/spec/serializers/project_serializer_spec.rb b/spec/serializers/project_serializer_spec.rb new file mode 100644 index 00000000000..22f958fc17f --- /dev/null +++ b/spec/serializers/project_serializer_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectSerializer do + set(:project) { create(:project) } + let(:provider_url) { 'http://provider.com' } + + context 'when serializer option is :import' do + subject do + described_class.new.represent(project, serializer: :import, provider_url: provider_url) + end + + before do + allow(ProjectImportEntity).to receive(:represent) + end + + it 'represents with ProjectImportEntity' do + subject + + expect(ProjectImportEntity) + .to have_received(:represent) + .with(project, serializer: :import, provider_url: provider_url, request: an_instance_of(EntityRequest)) + end + end + + context 'when serializer option is omitted' do + subject do + described_class.new.represent(project) + end + + before do + allow(ProjectEntity).to receive(:represent) + end + + it 'represents with ProjectEntity' do + subject + + expect(ProjectEntity) + .to have_received(:represent) + .with(project, request: an_instance_of(EntityRequest)) + end + end +end diff --git a/spec/serializers/provider_repo_entity_spec.rb b/spec/serializers/provider_repo_entity_spec.rb new file mode 100644 index 00000000000..b67115bab10 --- /dev/null +++ b/spec/serializers/provider_repo_entity_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProviderRepoEntity do + include ImportHelper + + let(:provider_repo) { { id: 1, full_name: 'full/name', name: 'name', owner: { login: 'owner' } } } + let(:provider) { :github } + let(:provider_url) { 'https://github.com' } + let(:entity) { described_class.represent(provider_repo, provider: provider, provider_url: provider_url) } + + describe '#as_json' do + subject { entity.as_json } + + it 'includes requried fields' do + expect(subject[:id]).to eq(provider_repo[:id]) + expect(subject[:full_name]).to eq(provider_repo[:full_name]) + expect(subject[:owner_name]).to eq(provider_repo[:owner][:login]) + expect(subject[:sanitized_name]).to eq(sanitize_project_name(provider_repo[:name])) + expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, provider_repo[:full_name])) + end + end +end diff --git a/spec/serializers/provider_repo_serializer_spec.rb b/spec/serializers/provider_repo_serializer_spec.rb new file mode 100644 index 00000000000..f2be30c36d9 --- /dev/null +++ b/spec/serializers/provider_repo_serializer_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProviderRepoSerializer do + it 'represents ProviderRepoEntity entities' do + expect(described_class.entity_class).to eq(ProviderRepoEntity) + end +end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 48f1d696ff6..1645b67c329 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -311,7 +311,14 @@ describe Notes::CreateService do end it 'converts existing note to DiscussionNote' do - expect { subject }.to change { existing_note.reload.type }.from(nil).to('DiscussionNote') + expect do + existing_note + + Timecop.freeze(Time.now + 1.minute) { subject } + + existing_note.reload + end.to change { existing_note.type }.from(nil).to('DiscussionNote') + .and change { existing_note.updated_at } end end end diff --git a/spec/services/task_list_toggle_service_spec.rb b/spec/services/task_list_toggle_service_spec.rb index 7c5480d382f..b1260cf740a 100644 --- a/spec/services/task_list_toggle_service_spec.rb +++ b/spec/services/task_list_toggle_service_spec.rb @@ -12,6 +12,10 @@ describe TaskListToggleService do 1. [X] Item 1 - [ ] Sub-item 1 + + - [ ] loose list + + with an embedded paragraph EOT end @@ -26,16 +30,22 @@ describe TaskListToggleService do </li> </ul> <p data-sourcepos="4:1-4:11" dir="auto">A paragraph</p> - <ol data-sourcepos="6:1-7:19" class="task-list" dir="auto"> - <li data-sourcepos="6:1-7:19" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled checked> Item 1 - <ul data-sourcepos="7:4-7:19" class="task-list"> - <li data-sourcepos="7:4-7:19" class="task-list-item"> - <input type="checkbox" class="task-list-item-checkbox" disabled> Sub-item 1 + <ol data-sourcepos="6:1-8:0" class="task-list" dir="auto"> + <li data-sourcepos="6:1-8:0" class="task-list-item"> + <input type="checkbox" class="task-list-item-checkbox" checked="" disabled=""> Item 1 + <ul data-sourcepos="7:4-8:0" class="task-list"> + <li data-sourcepos="7:4-8:0" class="task-list-item"> + <input type="checkbox" class="task-list-item-checkbox" disabled=""> Sub-item 1 </li> </ul> </li> </ol> + <ul data-sourcepos="9:1-11:28" class="task-list" dir="auto"> + <li data-sourcepos="9:1-11:28" class="task-list-item"> + <p data-sourcepos="9:3-9:16"><input type="checkbox" class="task-list-item-checkbox" disabled=""> loose list</p> + <p data-sourcepos="11:3-11:28">with an embedded paragraph</p> + </li> + </ul> EOT end @@ -59,6 +69,16 @@ describe TaskListToggleService do expect(toggler.updated_markdown_html).to include('disabled> Item 1') end + it 'checks task in loose list' do + toggler = described_class.new(markdown, markdown_html, + toggle_as_checked: true, + line_source: '- [ ] loose list', line_number: 9) + + expect(toggler.execute).to be_truthy + expect(toggler.updated_markdown.lines[8]).to eq "- [x] loose list\n" + expect(toggler.updated_markdown_html).to include('disabled checked> loose list') + end + it 'returns false if line_source does not match the text' do toggler = described_class.new(markdown, markdown_html, toggle_as_checked: false, diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb index 719b4adf212..3c0a4ac8e18 100644 --- a/spec/services/users/activity_service_spec.rb +++ b/spec/services/users/activity_service_spec.rb @@ -26,6 +26,12 @@ describe Users::ActivityService do .from(last_activity_on) .to(Date.today) end + + it 'tries to obtain ExclusiveLease' do + expect(Gitlab::ExclusiveLease).to receive(:new).and_call_original + + subject.execute + end end context 'when a bad object is passed' do @@ -46,6 +52,12 @@ describe Users::ActivityService do it 'does not update last_activity_on' do expect { subject.execute }.not_to change(user, :last_activity_on) end + + it 'does not try to obtain ExclusiveLease' do + expect(Gitlab::ExclusiveLease).not_to receive(:new) + + subject.execute + end end context 'when in GitLab read-only instance' do diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb index 697f999e4c4..5bb1269a19d 100644 --- a/spec/support/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb @@ -58,36 +58,54 @@ end shared_examples 'a GitHub-ish import controller: GET status' do let(:new_import_url) { public_send("new_import_#{provider}_url") } let(:user) { create(:user) } - let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim') } + let(:repo) { OpenStruct.new(login: 'vim', full_name: 'asd/vim', name: 'vim', owner: { login: 'owner' }) } let(:org) { OpenStruct.new(login: 'company') } - let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo') } - let(:extra_assign_expectations) { {} } + let(:org_repo) { OpenStruct.new(login: 'company', full_name: 'company/repo', name: 'repo', owner: { login: 'owner' }) } before do assign_session_token(provider) end - it "assigns variables" do - project = create(:project, import_type: provider, namespace: user.namespace) + it "returns variables for json request" do + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') + group = create(:group) + group.add_owner(user) stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) - get :status + get :status, format: :json - expect(assigns(:already_added_projects)).to eq([project]) - expect(assigns(:repos)).to eq([repo, org_repo]) - extra_assign_expectations.each do |key, value| - expect(assigns(key)).to eq(value) - end + expect(response).to have_gitlab_http_status(200) + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos", 0, "id")).to eq(repo.id) + expect(json_response.dig("provider_repos", 1, "id")).to eq(org_repo.id) + expect(json_response.dig("namespaces", 0, "id")).to eq(group.id) end it "does not show already added project" do - project = create(:project, import_type: provider, namespace: user.namespace, import_source: 'asd/vim') + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'asd/vim') stub_client(repos: [repo], orgs: []) + get :status, format: :json + + expect(json_response.dig("imported_projects", 0, "id")).to eq(project.id) + expect(json_response.dig("provider_repos")).to eq([]) + end + + it "touches the etag cache store" do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| + expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } + end + + get :status, format: :json + end + + it "requests provider repos list" do + expect(stub_client(repos: [], orgs: [])).to receive(:repos) + get :status - expect(assigns(:already_added_projects)).to eq([project]) - expect(assigns(:repos)).to eq([]) + expect(response).to have_gitlab_http_status(200) end it "handles an invalid access token" do @@ -100,13 +118,32 @@ shared_examples 'a GitHub-ish import controller: GET status' do expect(controller).to redirect_to(new_import_url) expect(flash[:alert]).to eq("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.") end + + it "does not produce N+1 database queries" do + stub_client(repos: [repo], orgs: []) + group_a = create(:group) + group_a.add_owner(user) + create(:project, :import_started, import_type: provider, namespace: user.namespace) + + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + get :status, format: :json + end.count + + stub_client(repos: [repo, org_repo], orgs: []) + group_b = create(:group) + group_b.add_owner(user) + create(:project, :import_started, import_type: provider, namespace: user.namespace) + + expect { get :status, format: :json } + .not_to exceed_all_query_limit(control_count) + end end shared_examples 'a GitHub-ish import controller: POST create' do let(:user) { create(:user) } - let(:project) { create(:project) } let(:provider_username) { user.username } let(:provider_user) { OpenStruct.new(login: provider_username) } + let(:project) { create(:project, import_type: provider, import_status: :finished, import_source: "#{provider_username}/vim") } let(:provider_repo) do OpenStruct.new( name: 'vim', @@ -145,6 +182,17 @@ shared_examples 'a GitHub-ish import controller: POST create' do expect(json_response['errors']).to eq('Name is invalid, Path is old') end + it "touches the etag cache store" do + allow(Gitlab::LegacyGithubImport::ProjectCreator) + .to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider) + .and_return(double(execute: project)) + expect_next_instance_of(Gitlab::EtagCaching::Store) do |store| + expect(store).to receive(:touch) { "realtime_changes_import_#{provider}_path" } + end + + post :create, format: :json + end + context "when the repository owner is the provider user" do context "when the provider user and GitLab user's usernames match" do it "takes the current user's namespace" do @@ -351,7 +399,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create a new namespace under the user namespace' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: "#{user.namespace_path}/test_group", new_name: test_name }, format: :js } .not_to change { Namespace.count } @@ -365,7 +413,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not take the selected namespace and name' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, user.namespace, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js end @@ -373,7 +421,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do it 'does not create the namespaces' do allow(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, kind_of(Namespace), user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) expect { post :create, params: { target_namespace: 'foo/foobar/bar', new_name: test_name }, format: :js } .not_to change { Namespace.count } @@ -390,7 +438,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do expect(Gitlab::LegacyGithubImport::ProjectCreator) .to receive(:new).with(provider_repo, test_name, group, user, access_params, type: provider) - .and_return(double(execute: build_stubbed(:project))) + .and_return(double(execute: project)) post :create, params: { target_namespace: 'foo', new_name: test_name }, format: :js end @@ -407,3 +455,20 @@ shared_examples 'a GitHub-ish import controller: POST create' do end end end + +shared_examples 'a GitHub-ish import controller: GET realtime_changes' do + let(:user) { create(:user) } + + before do + assign_session_token(provider) + end + + it 'sets a Poll-Interval header' do + project = create(:project, import_type: provider, namespace: user.namespace, import_status: :finished, import_source: 'example/repo') + + get :realtime_changes + + expect(json_response).to eq([{ "id" => project.id, "import_status" => project.import_status }]) + expect(Integer(response.headers['Poll-Interval'])).to be > -1 + end +end diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index ea3a03879c5..e468ee4676d 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -84,7 +84,7 @@ module GraphqlHelpers QUERY end - def all_graphql_fields_for(class_name) + def all_graphql_fields_for(class_name, parent_types = Set.new) type = GitlabSchema.types[class_name.to_s] return "" unless type @@ -92,8 +92,17 @@ module GraphqlHelpers # We can't guess arguments, so skip fields that require them next if required_arguments?(field) + singular_field_type = field_type(field) + + # If field type is the same as parent type, then we're hitting into + # mutual dependency. Break it from infinite recursion + next if parent_types.include?(singular_field_type) + if nested_fields?(field) - "#{name} { #{all_graphql_fields_for(field_type(field))} }" + fields = + all_graphql_fields_for(singular_field_type, parent_types | [type]) + + "#{name} { #{fields} }" else name end diff --git a/spec/support/helpers/reactive_caching_helpers.rb b/spec/support/helpers/reactive_caching_helpers.rb index a575aa99b79..b76b53db0b9 100644 --- a/spec/support/helpers/reactive_caching_helpers.rb +++ b/spec/support/helpers/reactive_caching_helpers.rb @@ -10,7 +10,7 @@ module ReactiveCachingHelpers def stub_reactive_cache(subject = nil, data = nil, *qualifiers) allow(ReactiveCachingWorker).to receive(:perform_async) allow(ReactiveCachingWorker).to receive(:perform_in) - write_reactive_cache(subject, data, *qualifiers) if data + write_reactive_cache(subject, data, *qualifiers) unless data.nil? end def synchronous_reactive_cache(subject) diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb index 4061a8d1bc9..48258692304 100644 --- a/spec/support/helpers/stub_feature_flags.rb +++ b/spec/support/helpers/stub_feature_flags.rb @@ -1,11 +1,30 @@ module StubFeatureFlags # Stub Feature flags with `flag_name: true/false` # - # @param [Hash] features where key is feature name and value is boolean whether enabled or not + # @param [Hash] features where key is feature name and value is boolean whether enabled or not. + # Alternatively, you can specify Hash to enable the flag on a specific thing. + # + # Examples + # - `stub_feature_flags(ci_live_trace: false)` ... Disable `ci_live_trace` + # feature flag globally. + # - `stub_feature_flags(ci_live_trace: { enabled: false, thing: project })` ... + # Disable `ci_live_trace` feature flag on the specified project. def stub_feature_flags(features) - features.each do |feature_name, enabled| - allow(Feature).to receive(:enabled?).with(feature_name, any_args) { enabled } - allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args) { enabled } + features.each do |feature_name, option| + if option.is_a?(Hash) + enabled, thing = option.values_at(:enabled, :thing) + else + enabled = option + thing = nil + end + + if thing + allow(Feature).to receive(:enabled?).with(feature_name, thing, any_args) { enabled } + allow(Feature).to receive(:enabled?).with(feature_name.to_s, thing, any_args) { enabled } + else + allow(Feature).to receive(:enabled?).with(feature_name, any_args) { enabled } + allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args) { enabled } + end end end end diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb index 23f9b46ae0c..d92e8318fa0 100644 --- a/spec/support/shared_contexts/services_shared_context.rb +++ b/spec/support/shared_contexts/services_shared_context.rb @@ -19,7 +19,7 @@ Service.available_services_names.each do |service| elsif service == 'irker' && k == :server_port hash.merge!(k => 1234) elsif service == 'jira' && k == :jira_issue_transition_id - hash.merge!(k => 1234) + hash.merge!(k => '1,2,3') else hash.merge!(k => "someword") end diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb index c96a65cb56a..b8c19cab0c4 100644 --- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb +++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb @@ -9,6 +9,19 @@ shared_examples 'cluster application status specs' do |application_name| end end + describe '.available' do + subject { described_class.available } + + let!(:installed_cluster) { create(application_name, :installed) } + let!(:updated_cluster) { create(application_name, :updated) } + + before do + create(application_name, :errored) + end + + it { is_expected.to contain_exactly(installed_cluster, updated_cluster) } + end + describe 'status state machine' do describe '#make_installing' do subject { create(application_name, :scheduled) } diff --git a/spec/support/shared_examples/models/with_uploads_shared_examples.rb b/spec/support/shared_examples/models/with_uploads_shared_examples.rb index 1d11b855459..43033a2d256 100644 --- a/spec/support/shared_examples/models/with_uploads_shared_examples.rb +++ b/spec/support/shared_examples/models/with_uploads_shared_examples.rb @@ -44,26 +44,6 @@ shared_examples_for 'model with uploads' do |supports_fileuploads| model_object.destroy end end - - describe 'destroy strategy depending on feature flag' do - let!(:upload) { create(:upload, uploader: FileUploader, model: model_object) } - - it 'does not destroy uploads by default' do - expect(model_object).to receive(:delete_uploads) - expect(model_object).not_to receive(:destroy_uploads) - - model_object.destroy - end - - it 'uses before destroy callback if feature flag is disabled' do - stub_feature_flags(fast_destroy_uploads: false) - - expect(model_object).to receive(:destroy_uploads) - expect(model_object).not_to receive(:delete_uploads) - - model_object.destroy - end - end end end end diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb index 453f42251c8..6713ec47ace 100644 --- a/spec/support/shared_examples/requests/api/merge_requests_list.rb +++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb @@ -257,6 +257,38 @@ shared_examples 'merge requests list' do expect(response_dates).to eq(response_dates.sort.reverse) end + context '2 merge requests with equal created_at' do + let!(:closed_mr2) do + create :merge_request, + state: 'closed', + milestone: milestone1, + author: user, + assignee: user, + source_project: project, + target_project: project, + title: "Test", + created_at: @mr_earlier.created_at + end + + it 'page breaks first page correctly' do + get api("#{endpoint_path}?sort=desc&per_page=4", user) + + response_ids = json_response.map { |merge_request| merge_request['id'] } + + expect(response_ids).to include(closed_mr2.id) + expect(response_ids).not_to include(@mr_earlier.id) + end + + it 'page breaks second page correctly' do + get api("#{endpoint_path}?sort=desc&per_page=4&page=2", user) + + response_ids = json_response.map { |merge_request| merge_request['id'] } + + expect(response_ids).not_to include(closed_mr2.id) + expect(response_ids).to include(@mr_earlier.id) + end + end + it 'returns an array of merge_requests ordered by updated_at' do path = endpoint_path + '?order_by=updated_at' diff --git a/spec/support/shared_examples/requests/api/notes.rb b/spec/support/shared_examples/requests/api/notes.rb index 71499e85654..57eefd5ef01 100644 --- a/spec/support/shared_examples/requests/api/notes.rb +++ b/spec/support/shared_examples/requests/api/notes.rb @@ -8,13 +8,45 @@ shared_examples 'noteable API' do |parent_type, noteable_type, id_name| create_list(:note, 3, params) end - it 'sorts by created_at in descending order by default' do - get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user) + context 'without sort params' do + it 'sorts by created_at in descending order by default' do + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user) - response_dates = json_response.map { |note| note['created_at'] } + response_dates = json_response.map { |note| note['created_at'] } - expect(json_response.length).to eq(4) - expect(response_dates).to eq(response_dates.sort.reverse) + expect(json_response.length).to eq(4) + expect(response_dates).to eq(response_dates.sort.reverse) + end + + context '2 notes with equal created_at' do + before do + @first_note = Note.first + + params = { noteable: noteable, author: user } + params[:project] = parent if parent.is_a?(Project) + params[:created_at] = @first_note.created_at + + @note2 = create(:note, params) + end + + it 'page breaks first page correctly' do + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4", user) + + response_ids = json_response.map { |note| note['id'] } + + expect(response_ids).to include(@note2.id) + expect(response_ids).not_to include(@first_note.id) + end + + it 'page breaks second page correctly' do + get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes?per_page=4&page=2", user) + + response_ids = json_response.map { |note| note['id'] } + + expect(response_ids).not_to include(@note2.id) + expect(response_ids).to include(@first_note.id) + end + end end it 'sorts by ascending order when requested' do diff --git a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb index 1770308f789..96cb71be737 100644 --- a/spec/support/shared_examples/serializers/diff_file_entity_examples.rb +++ b/spec/support/shared_examples/serializers/diff_file_entity_examples.rb @@ -6,9 +6,9 @@ shared_examples 'diff file base entity' do :submodule_tree_url, :old_path_html, :new_path_html, :blob, :can_modify_blob, :file_hash, :file_path, :old_path, :new_path, - :collapsed, :text, :diff_refs, :stored_externally, + :viewer, :diff_refs, :stored_externally, :external_storage, :renamed_file, :deleted_file, - :mode_changed, :a_mode, :b_mode, :new_file) + :a_mode, :b_mode, :new_file) end # Converted diff files from GitHub import does not contain blob file @@ -30,9 +30,9 @@ shared_examples 'diff file entity' do it_behaves_like 'diff file base entity' it 'exposes correct attributes' do - expect(subject).to include(:too_large, :added_lines, :removed_lines, + expect(subject).to include(:added_lines, :removed_lines, :context_lines_path, :highlighted_diff_lines, - :parallel_diff_lines, :empty) + :parallel_diff_lines) end it 'includes viewer' do |