diff options
author | Phil Hughes <me@iamphill.com> | 2018-04-03 10:59:29 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-04-03 10:59:29 +0100 |
commit | c0dddb511c3bedc9b07df97739a27e07354b2242 (patch) | |
tree | 3b1f6042c7fd274579aa82c81a3d5894b6e53d90 /spec | |
parent | 6bec91bfc9ec15556e833f4d8f441328d135638e (diff) | |
parent | 8dca091ff7f04bb92a7835ebeff783b7f0ef76cd (diff) | |
download | gitlab-ce-c0dddb511c3bedc9b07df97739a27e07354b2242.tar.gz |
Merge branch 'master' into ide-pending-tab
Diffstat (limited to 'spec')
115 files changed, 4099 insertions, 1449 deletions
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 5b2614163ff..548c5ef36e7 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -107,7 +107,7 @@ describe Projects::MilestonesController do it 'shows group milestone' do post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid - expect(flash[:notice]).to eq("#{milestone.title} promoted to group milestone") + expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\">group milestone</a>.") expect(response).to redirect_to(project_milestones_path(project)) end end diff --git a/spec/factories/ci/build_metadata.rb b/spec/factories/ci/build_metadata.rb new file mode 100644 index 00000000000..66bbd977b88 --- /dev/null +++ b/spec/factories/ci/build_metadata.rb @@ -0,0 +1,9 @@ +FactoryBot.define do + factory :ci_build_metadata, class: Ci::BuildMetadata do + build factory: :ci_build + + after(:build) do |build_metadata, _| + build_metadata.project ||= build_metadata.build.project + end + end +end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index c89bc54cad4..3005d74c3cf 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -120,6 +120,53 @@ feature 'Admin updates settings' do expect(page).to have_content "Application settings saved successfully" end + scenario 'Change Performance bar settings' do + group = create(:group) + + page.within('.as-performance') do + check 'Enable the Performance Bar' + fill_in 'Allowed group', with: group.path + click_on 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(find_field('Enable the Performance Bar')).to be_checked + expect(find_field('Allowed group').value).to eq group.path + + page.within('.as-performance') do + uncheck 'Enable the Performance Bar' + click_on 'Save changes' + end + + expect(page).to have_content 'Application settings saved successfully' + expect(find_field('Enable the Performance Bar')).not_to be_checked + expect(find_field('Allowed group').value).to be_nil + end + + scenario 'Change Background jobs settings' do + page.within('.as-background') do + fill_in 'Throttling Factor', with: 1 + click_button 'Save changes' + end + + expect(Gitlab::CurrentSettings.sidekiq_throttling_factor).to eq(1) + expect(page).to have_content "Application settings saved successfully" + end + + scenario 'Change Spam settings' do + page.within('.as-spam') do + check 'Enable reCAPTCHA' + fill_in 'reCAPTCHA Site Key', with: 'key' + fill_in 'reCAPTCHA Private Key', with: 'key' + fill_in 'IPs per user', with: 15 + click_button 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.recaptcha_enabled).to be true + expect(Gitlab::CurrentSettings.unique_ips_limit_per_user).to eq(15) + end + scenario 'Change Slack Notifications Service template settings' do first(:link, 'Service Templates').click click_link 'Slack notifications' @@ -172,29 +219,6 @@ feature 'Admin updates settings' do expect(find_field('ED25519 SSH keys').value).to eq(forbidden) end - scenario 'Change Performance Bar settings' do - group = create(:group) - - check 'Enable the Performance Bar' - fill_in 'Allowed group', with: group.path - - click_on 'Save' - - expect(page).to have_content 'Application settings saved successfully' - - expect(find_field('Enable the Performance Bar')).to be_checked - expect(find_field('Allowed group').value).to eq group.path - - uncheck 'Enable the Performance Bar' - - click_on 'Save' - - expect(page).to have_content 'Application settings saved successfully' - - expect(find_field('Enable the Performance Bar')).not_to be_checked - expect(find_field('Allowed group').value).to be_nil - end - def check_all_events page.check('Active') page.check('Push') diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb index d3b25ec3d6c..7bc809b3104 100644 --- a/spec/features/groups/activity_spec.rb +++ b/spec/features/groups/activity_spec.rb @@ -8,11 +8,30 @@ feature 'Group activity page' do context 'when signed in' do before do sign_in(user) - visit path end - it_behaves_like "it has an RSS button with current_user's RSS token" - it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + describe 'RSS' do + before do + visit path + end + + it_behaves_like "it has an RSS button with current_user's RSS token" + it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + end + + context 'when project is in the group', :js do + let(:project) { create(:project, :public, namespace: group) } + + before do + project.add_master(user) + + visit path + end + + it 'renders user joined to project event' do + expect(page).to have_content 'joined project' + end + end end context 'when signed out' do diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb index b83bad3befb..1ce30015e81 100644 --- a/spec/features/groups/group_settings_spec.rb +++ b/spec/features/groups/group_settings_spec.rb @@ -76,6 +76,27 @@ feature 'Edit group settings' do end end end + + describe 'edit group avatar' do + before do + visit edit_group_path(group) + + attach_file(:group_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif')) + + expect { click_button 'Save group' }.to change { group.reload.avatar? }.to(true) + end + + it 'uploads new group avatar' do + expect(group.avatar).to be_instance_of AvatarUploader + expect(group.avatar.url).to eq "/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif" + expect(page).to have_link('Remove avatar') + end + + it 'removes group avatar' do + expect { click_link 'Remove avatar' }.to change { group.reload.avatar? }.to(false) + expect(page).not_to have_link('Remove avatar') + end + end end def update_path(new_group_path) diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 450bc0ff8cf..90bf7ba49f6 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -3,8 +3,11 @@ require 'spec_helper' feature 'Group issues page' do include FilteredSearchHelpers + let(:group) { create(:group) } + let(:project) { create(:project, :public, group: group)} + let(:path) { issues_group_path(group) } + context 'with shared examples' do - let(:path) { issues_group_path(group) } let(:issuable) { create(:issue, project: project, title: "this is my created issuable")} include_examples 'project features apply to issuables', Issue @@ -31,7 +34,6 @@ feature 'Group issues page' do let(:access_level) { ProjectFeature::ENABLED } let(:user) { user_in_group } let(:user2) { user_outside_group } - let(:path) { issues_group_path(group) } it 'filters by only group users' do filtered_search.set('assignee:') @@ -43,9 +45,7 @@ feature 'Group issues page' do end context 'issues list', :nested_groups do - let(:group) { create(:group)} let(:subgroup) { create(:group, parent: group) } - let(:project) { create(:project, :public, group: group)} let(:subgroup_project) { create(:project, :public, group: subgroup)} let!(:issue) { create(:issue, project: project, title: 'root group issue') } let!(:subgroup_issue) { create(:issue, project: subgroup_project, title: 'subgroup issue') } @@ -59,5 +59,17 @@ feature 'Group issues page' do expect(page).to have_content('subgroup issue') end end + + context 'when project is archived' do + before do + project.archive! + end + + it 'does not render issue' do + visit path + + expect(page).not_to have_content issue.title[0..80] + end + end end end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 7ce6a61d50c..672ae785c2d 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -5,14 +5,14 @@ feature 'Group merge requests page' do let(:path) { merge_requests_group_path(group) } let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: 'this is my created issuable') } + let(:access_level) { ProjectFeature::ENABLED } + let(:user) { user_in_group } include_examples 'project features apply to issuables', MergeRequest context 'archived issuable' do let(:project_archived) { create(:project, :archived, :merge_requests_enabled, :repository, group: group) } let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') } - let(:access_level) { ProjectFeature::ENABLED } - let(:user) { user_in_group } before do issuable_archived @@ -36,9 +36,17 @@ feature 'Group merge requests page' do end end + context 'when merge request assignee to user' do + before do + issuable.update!(assignee: user) + + visit path + end + + it { expect(page).to have_content issuable.title[0..80] } + end + context 'group filtered search', :js do - let(:access_level) { ProjectFeature::ENABLED } - let(:user) { user_in_group } let(:user2) { user_outside_group } it 'filters by assignee only group users' do diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index ceccc471405..4ffadbbcd35 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -15,14 +15,44 @@ feature 'Group show page' do end it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token" + + context 'when group does not exist' do + let(:path) { group_path('not-exist') } + + it { expect(status_code).to eq(404) } + end end context 'when signed out' do - before do - visit path + describe 'RSS' do + before do + visit path + end + + it_behaves_like "an autodiscoverable RSS feed without an RSS token" + end + + context 'when group has a public project', :js do + let!(:project) { create(:project, :public, namespace: group) } + + it 'renders public project' do + visit path + + expect(page).to have_link group.name + expect(page).to have_link project.name + end end - it_behaves_like "an autodiscoverable RSS feed without an RSS token" + context 'when group has a private project', :js do + let!(:project) { create(:project, :private, namespace: group) } + + it 'does not render private project' do + visit path + + expect(page).to have_link group.name + expect(page).not_to have_link project.name + end + end end context 'subgroup support' do diff --git a/spec/features/groups/user_browse_projects_group_page_spec.rb b/spec/features/groups/user_browse_projects_group_page_spec.rb new file mode 100644 index 00000000000..e81c3180e78 --- /dev/null +++ b/spec/features/groups/user_browse_projects_group_page_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +describe 'User browse group projects page' do + let(:user) { create :user } + let(:group) { create :group } + + context 'when user is owner' do + before do + group.add_owner(user) + end + + context 'when user signed in' do + before do + sign_in(user) + end + + context 'when group has archived project', :js do + let!(:project) { create :project, :archived, namespace: group } + + it 'renders projects list' do + visit projects_group_path(group) + + expect(page).to have_link project.name + expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') + end + end + end + end +end diff --git a/spec/features/issuables/discussion_lock_spec.rb b/spec/features/issuables/discussion_lock_spec.rb index ecbe51a7bc2..7ea29ff252b 100644 --- a/spec/features/issuables/discussion_lock_spec.rb +++ b/spec/features/issuables/discussion_lock_spec.rb @@ -14,7 +14,7 @@ describe 'Discussion Lock', :js do project.add_developer(user) end - context 'when the discussion is unlocked' do + context 'when the discussion is unlocked' do it 'the user can lock the issue' do visit project_issue_path(project, issue) diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index b835558b142..27551bb70ee 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -161,6 +161,50 @@ feature 'Issue Sidebar' do end end end + + context 'interacting with collapsed sidebar', :js do + collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed' + expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded' + confidentiality_sidebar_block = '.block.confidentiality' + lock_sidebar_block = '.block.lock' + collapsed_sidebar_block_icon = '.sidebar-collapsed-icon' + + before do + resize_screen_sm + end + + it 'confidentiality block expands then collapses sidebar' do + expect(page).to have_css(collapsed_sidebar_selector) + + page.within(confidentiality_sidebar_block) do + find(collapsed_sidebar_block_icon).click + end + + expect(page).to have_css(expanded_sidebar_selector) + + page.within(confidentiality_sidebar_block) do + page.find('button', text: 'Cancel').click + end + + expect(page).to have_css(collapsed_sidebar_selector) + end + + it 'lock block expands then collapses sidebar' do + expect(page).to have_css(collapsed_sidebar_selector) + + page.within(lock_sidebar_block) do + find(collapsed_sidebar_block_icon).click + end + + expect(page).to have_css(expanded_sidebar_selector) + + page.within(lock_sidebar_block) do + page.find('button', text: 'Cancel').click + end + + expect(page).to have_css(collapsed_sidebar_selector) + end + end end context 'as a guest' do diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb index 171e061e60e..e8eb0d17ca4 100644 --- a/spec/features/profiles/account_spec.rb +++ b/spec/features/profiles/account_spec.rb @@ -43,14 +43,14 @@ feature 'Profile > Account' do update_username(new_username) visit new_project_path expect(current_path).to eq(new_project_path) - expect(find('.breadcrumbs-sub-title')).to have_content(project.path) + expect(find('.breadcrumbs-sub-title')).to have_content('Details') end scenario 'the old project path redirects to the new path' do update_username(new_username) visit old_project_path expect(current_path).to eq(new_project_path) - expect(find('.breadcrumbs-sub-title')).to have_content(project.path) + expect(find('.breadcrumbs-sub-title')).to have_content('Details') end end end diff --git a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json index d24a6f93f4b..81c8815caf6 100644 --- a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json +++ b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json @@ -1,7 +1,9 @@ { "type": "object", "allOf": [ - { "$ref": "identity.json" }, + { + "$ref": "identity.json" + }, { "required": [ "export_status" @@ -9,7 +11,12 @@ "properties": { "export_status": { "type": "string", - "enum": ["none", "started", "finished"] + "enum": [ + "none", + "started", + "finished", + "after_export_action" + ] } } } diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index baf927a9acc..b77114a8152 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -50,6 +50,11 @@ describe PageLayoutHelper do allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development')) expect(helper.favicon).to eq 'favicon-blue.ico' end + + it 'has yellow favicon for canary' do + stub_env('CANARY', 'true') + expect(helper.favicon).to eq 'favicon-yellow.ico' + end end describe 'page_image' do diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index 5477581c1b9..3d7ccf432be 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -35,14 +35,14 @@ describe('Api', () => { }); describe('group', () => { - it('fetches a group', (done) => { + it('fetches a group', done => { const groupId = '123456'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`; mock.onGet(expectedUrl).reply(200, { name: 'test', }); - Api.group(groupId, (response) => { + Api.group(groupId, response => { expect(response.name).toBe('test'); done(); }); @@ -50,15 +50,17 @@ describe('Api', () => { }); describe('groups', () => { - it('fetches groups', (done) => { + it('fetches groups', done => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`; - mock.onGet(expectedUrl).reply(200, [{ - name: 'test', - }]); + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); - Api.groups(query, options, (response) => { + Api.groups(query, options, response => { expect(response.length).toBe(1); expect(response[0].name).toBe('test'); done(); @@ -67,14 +69,16 @@ describe('Api', () => { }); describe('namespaces', () => { - it('fetches namespaces', (done) => { + it('fetches namespaces', done => { const query = 'dummy query'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`; - mock.onGet(expectedUrl).reply(200, [{ - name: 'test', - }]); + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); - Api.namespaces(query, (response) => { + Api.namespaces(query, response => { expect(response.length).toBe(1); expect(response[0].name).toBe('test'); done(); @@ -83,31 +87,35 @@ describe('Api', () => { }); describe('projects', () => { - it('fetches projects with membership when logged in', (done) => { + it('fetches projects with membership when logged in', done => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; window.gon.current_user_id = 1; - mock.onGet(expectedUrl).reply(200, [{ - name: 'test', - }]); + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); - Api.projects(query, options, (response) => { + Api.projects(query, options, response => { expect(response.length).toBe(1); expect(response[0].name).toBe('test'); done(); }); }); - it('fetches projects without membership when not logged in', (done) => { + it('fetches projects without membership when not logged in', done => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`; - mock.onGet(expectedUrl).reply(200, [{ - name: 'test', - }]); + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); - Api.projects(query, options, (response) => { + Api.projects(query, options, response => { expect(response.length).toBe(1); expect(response[0].name).toBe('test'); done(); @@ -115,8 +123,65 @@ describe('Api', () => { }); }); + describe('mergerequest', () => { + it('fetches a merge request', done => { + const projectPath = 'abc'; + const mergeRequestId = '123456'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`; + mock.onGet(expectedUrl).reply(200, { + title: 'test', + }); + + Api.mergeRequest(projectPath, mergeRequestId) + .then(({ data }) => { + expect(data.title).toBe('test'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('mergerequest changes', () => { + it('fetches the changes of a merge request', done => { + const projectPath = 'abc'; + const mergeRequestId = '123456'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`; + mock.onGet(expectedUrl).reply(200, { + title: 'test', + }); + + Api.mergeRequestChanges(projectPath, mergeRequestId) + .then(({ data }) => { + expect(data.title).toBe('test'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('mergerequest versions', () => { + it('fetches the versions of a merge request', done => { + const projectPath = 'abc'; + const mergeRequestId = '123456'; + const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`; + mock.onGet(expectedUrl).reply(200, [ + { + id: 123, + }, + ]); + + Api.mergeRequestVersions(projectPath, mergeRequestId) + .then(({ data }) => { + expect(data.length).toBe(1); + expect(data[0].id).toBe(123); + }) + .then(done) + .catch(done.fail); + }); + }); + describe('newLabel', () => { - it('creates a new label', (done) => { + it('creates a new label', done => { const namespace = 'some namespace'; const project = 'some project'; const labelData = { some: 'data' }; @@ -124,36 +189,42 @@ describe('Api', () => { const expectedData = { label: labelData, }; - mock.onPost(expectedUrl).reply((config) => { + mock.onPost(expectedUrl).reply(config => { expect(config.data).toBe(JSON.stringify(expectedData)); - return [200, { - name: 'test', - }]; + return [ + 200, + { + name: 'test', + }, + ]; }); - Api.newLabel(namespace, project, labelData, (response) => { + Api.newLabel(namespace, project, labelData, response => { expect(response.name).toBe('test'); done(); }); }); - it('creates a group label', (done) => { + it('creates a group label', done => { const namespace = 'group/subgroup'; const labelData = { some: 'data' }; const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`; const expectedData = { label: labelData, }; - mock.onPost(expectedUrl).reply((config) => { + mock.onPost(expectedUrl).reply(config => { expect(config.data).toBe(JSON.stringify(expectedData)); - return [200, { - name: 'test', - }]; + return [ + 200, + { + name: 'test', + }, + ]; }); - Api.newLabel(namespace, undefined, labelData, (response) => { + Api.newLabel(namespace, undefined, labelData, response => { expect(response.name).toBe('test'); done(); }); @@ -161,15 +232,17 @@ describe('Api', () => { }); describe('groupProjects', () => { - it('fetches group projects', (done) => { + it('fetches group projects', done => { const groupId = '123456'; const query = 'dummy query'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`; - mock.onGet(expectedUrl).reply(200, [{ - name: 'test', - }]); + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); - Api.groupProjects(groupId, query, (response) => { + Api.groupProjects(groupId, query, response => { expect(response.length).toBe(1); expect(response[0].name).toBe('test'); done(); @@ -178,13 +251,13 @@ describe('Api', () => { }); describe('licenseText', () => { - it('fetches a license text', (done) => { + it('fetches a license text', done => { const licenseKey = "driver's license"; const data = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`; mock.onGet(expectedUrl).reply(200, 'test'); - Api.licenseText(licenseKey, data, (response) => { + Api.licenseText(licenseKey, data, response => { expect(response).toBe('test'); done(); }); @@ -192,12 +265,12 @@ describe('Api', () => { }); describe('gitignoreText', () => { - it('fetches a gitignore text', (done) => { + it('fetches a gitignore text', done => { const gitignoreKey = 'ignore git'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`; mock.onGet(expectedUrl).reply(200, 'test'); - Api.gitignoreText(gitignoreKey, (response) => { + Api.gitignoreText(gitignoreKey, response => { expect(response).toBe('test'); done(); }); @@ -205,12 +278,12 @@ describe('Api', () => { }); describe('gitlabCiYml', () => { - it('fetches a .gitlab-ci.yml', (done) => { + it('fetches a .gitlab-ci.yml', done => { const gitlabCiYmlKey = 'Y CI ML'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`; mock.onGet(expectedUrl).reply(200, 'test'); - Api.gitlabCiYml(gitlabCiYmlKey, (response) => { + Api.gitlabCiYml(gitlabCiYmlKey, response => { expect(response).toBe('test'); done(); }); @@ -218,12 +291,12 @@ describe('Api', () => { }); describe('dockerfileYml', () => { - it('fetches a Dockerfile', (done) => { + it('fetches a Dockerfile', done => { const dockerfileYmlKey = 'a giant whale'; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`; mock.onGet(expectedUrl).reply(200, 'test'); - Api.dockerfileYml(dockerfileYmlKey, (response) => { + Api.dockerfileYml(dockerfileYmlKey, response => { expect(response).toBe('test'); done(); }); @@ -231,12 +304,14 @@ describe('Api', () => { }); describe('issueTemplate', () => { - it('fetches an issue template', (done) => { + it('fetches an issue template', done => { const namespace = 'some namespace'; const project = 'some project'; const templateKey = ' template #%?.key '; const templateType = 'template type'; - const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`; + const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent( + templateKey, + )}`; mock.onGet(expectedUrl).reply(200, 'test'); Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => { @@ -247,13 +322,15 @@ describe('Api', () => { }); describe('users', () => { - it('fetches users', (done) => { + it('fetches users', done => { const query = 'dummy query'; const options = { unused: 'option' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`; - mock.onGet(expectedUrl).reply(200, [{ - name: 'test', - }]); + mock.onGet(expectedUrl).reply(200, [ + { + name: 'test', + }, + ]); Api.users(query, options) .then(({ data }) => { diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js index 0671facb285..81f1a97112f 100644 --- a/spec/javascripts/boards/mock_data.js +++ b/spec/javascripts/boards/mock_data.js @@ -1,7 +1,4 @@ /* global BoardService */ -/* eslint-disable comma-dangle, no-unused-vars, quote-props */ -import _ from 'underscore'; - export const listObj = { id: 300, position: 0, @@ -11,8 +8,8 @@ export const listObj = { id: 5000, title: 'Testing', color: 'red', - description: 'testing;' - } + description: 'testing;', + }, }; export const listObjDuplicate = { @@ -24,35 +21,37 @@ export const listObjDuplicate = { id: listObj.label.id, title: 'Testing', color: 'red', - description: 'testing;' - } + description: 'testing;', + }, }; export const BoardsMockData = { - 'GET': { + GET: { '/test/-/boards/1/lists/300/issues?id=300&page=1&=': { - issues: [{ - title: 'Testing', - id: 1, - iid: 1, - confidential: false, - labels: [], - assignees: [], - }], - } + issues: [ + { + title: 'Testing', + id: 1, + iid: 1, + confidential: false, + labels: [], + assignees: [], + }, + ], + }, + }, + POST: { + '/test/-/boards/1/lists': listObj, }, - 'POST': { - '/test/-/boards/1/lists': listObj + PUT: { + '/test/issue-boards/board/1/lists{/id}': {}, }, - 'PUT': { - '/test/issue-boards/board/1/lists{/id}': {} + DELETE: { + '/test/issue-boards/board/1/lists{/id}': {}, }, - 'DELETE': { - '/test/issue-boards/board/1/lists{/id}': {} - } }; -export const boardsMockInterceptor = (config) => { +export const boardsMockInterceptor = config => { const body = BoardsMockData[config.method.toUpperCase()][config.url]; return [200, body]; }; diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js index b9d28db74cc..23b69defec6 100644 --- a/spec/javascripts/droplab/constants_spec.js +++ b/spec/javascripts/droplab/constants_spec.js @@ -1,39 +1,37 @@ -/* eslint-disable */ - import * as constants from '~/droplab/constants'; -describe('constants', function () { - describe('DATA_TRIGGER', function () { +describe('constants', function() { + describe('DATA_TRIGGER', function() { it('should be `data-dropdown-trigger`', function() { expect(constants.DATA_TRIGGER).toBe('data-dropdown-trigger'); }); }); - describe('DATA_DROPDOWN', function () { + describe('DATA_DROPDOWN', function() { it('should be `data-dropdown`', function() { expect(constants.DATA_DROPDOWN).toBe('data-dropdown'); }); }); - describe('SELECTED_CLASS', function () { + describe('SELECTED_CLASS', function() { it('should be `droplab-item-selected`', function() { expect(constants.SELECTED_CLASS).toBe('droplab-item-selected'); }); }); - describe('ACTIVE_CLASS', function () { + describe('ACTIVE_CLASS', function() { it('should be `droplab-item-active`', function() { expect(constants.ACTIVE_CLASS).toBe('droplab-item-active'); }); }); - describe('TEMPLATE_REGEX', function () { + describe('TEMPLATE_REGEX', function() { it('should be a handlebars templating syntax regex', function() { expect(constants.TEMPLATE_REGEX).toEqual(/\{\{(.+?)\}\}/g); }); }); - describe('IGNORE_CLASS', function () { + describe('IGNORE_CLASS', function() { it('should be `droplab-item-ignore`', function() { expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore'); }); diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index b344b389241..e8865b04874 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -17,8 +17,6 @@ describe 'Projects (JavaScript fixtures)', type: :controller do end before do - # EE-specific start - # EE specific end project.add_master(admin) sign_in(admin) end diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index dc0a5bc275c..1cb20a1e7ff 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -81,13 +81,21 @@ describe('GfmAutoComplete', function () { }); it('should quote if value contains any non-alphanumeric characters', () => { - expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"'); + expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label\\-20"'); expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"'); }); it('should quote integer labels', () => { expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"'); }); + + it('should escape Markdown emphasis characters, except in the first character', () => { + expect(beforeInsert(atwhoInstance, '@_group')).toEqual('@\\_group'); + expect(beforeInsert(atwhoInstance, '~_bug')).toEqual('~\\_bug'); + expect(beforeInsert(atwhoInstance, '~a `bug`')).toEqual('~"a \\`bug\\`"'); + expect(beforeInsert(atwhoInstance, '~a ~bug')).toEqual('~"a \\~bug"'); + expect(beforeInsert(atwhoInstance, '~a **bug')).toEqual('~"a \\*\\*bug"'); + }); }); describe('DefaultOptions.matcher', function () { diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js index 2d386fe1da5..83f29d1b0c2 100644 --- a/spec/javascripts/helpers/vuex_action_helper.js +++ b/spec/javascripts/helpers/vuex_action_helper.js @@ -1,37 +1,71 @@ -/* eslint-disable */ - /** - * helper for testing action with expected mutations + * helper for testing action with expected mutations inspired in * https://vuex.vuejs.org/en/testing.html + * + * @example + * testAction( + * actions.actionName, // action + * { }, // mocked response + * state, // state + * [ + * { type: types.MUTATION} + * { type: types.MUTATION_1, payload: {}} + * ], // mutations + * [ + * { type: 'actionName', payload: {}}, + * { type: 'actionName1', payload: {}} + * ] //actions + * done, + * ); */ -export default (action, payload, state, expectedMutations, done) => { - let count = 0; +export default (action, payload, state, expectedMutations, expectedActions, done) => { + let mutationsCount = 0; + let actionsCount = 0; // mock commit - const commit = (type, payload) => { - const mutation = expectedMutations[count]; - - try { - expect(mutation.type).to.equal(type); - if (payload) { - expect(mutation.payload).to.deep.equal(payload); - } - } catch (error) { - done(error); + const commit = (type, mutationPayload) => { + const mutation = expectedMutations[mutationsCount]; + + expect(mutation.type).toEqual(type); + + if (mutation.payload) { + expect(mutation.payload).toEqual(mutationPayload); } - count++; - if (count >= expectedMutations.length) { + mutationsCount += 1; + if (mutationsCount >= expectedMutations.length) { + done(); + } + }; + + // mock dispatch + const dispatch = (type, actionPayload) => { + const actionExpected = expectedActions[actionsCount]; + + expect(actionExpected.type).toEqual(type); + + if (actionExpected.payload) { + expect(actionExpected.payload).toEqual(actionPayload); + } + + actionsCount += 1; + if (actionsCount >= expectedActions.length) { done(); } }; // call the action with mocked store and arguments - action({ commit, state }, payload); + action({ commit, state, dispatch }, payload); // check if no mutations should have been dispatched if (expectedMutations.length === 0) { - expect(count).to.equal(0); + expect(mutationsCount).toEqual(0); + done(); + } + + // check if no mutations should have been dispatched + if (expectedActions.length === 0) { + expect(actionsCount).toEqual(0); done(); } }; diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js index 987aea7befc..541864e912e 100644 --- a/spec/javascripts/ide/components/changed_file_icon_spec.js +++ b/spec/javascripts/ide/components/changed_file_icon_spec.js @@ -11,6 +11,7 @@ describe('IDE changed file icon', () => { vm = createComponent(component, { file: { tempFile: false, + changed: true, }, }); }); @@ -20,7 +21,7 @@ describe('IDE changed file icon', () => { }); describe('changedIcon', () => { - it('equals file-modified when not a temp file', () => { + it('equals file-modified when not a temp file and has changes', () => { expect(vm.changedIcon).toBe('file-modified'); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index ae657e8c881..9d3fa1280f4 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -89,6 +89,20 @@ describe('RepoEditor', () => { done(); }); }); + + it('calls createDiffInstance when viewer is a merge request diff', done => { + vm.$store.state.viewer = 'mrdiff'; + + spyOn(vm.editor, 'createDiffInstance'); + + vm.createEditorInstance(); + + vm.$nextTick(() => { + expect(vm.editor.createDiffInstance).toHaveBeenCalled(); + + done(); + }); + }); }); describe('setupEditor', () => { @@ -134,4 +148,48 @@ describe('RepoEditor', () => { }); }); }); + + describe('setup editor for merge request viewing', () => { + beforeEach(done => { + // Resetting as the main test setup has already done it + vm.$destroy(); + resetStore(vm.$store); + Editor.editorInstance.modelManager.dispose(); + + const f = { + ...file(), + active: true, + tempFile: true, + html: 'testing', + mrChange: { diff: 'ABC' }, + baseRaw: 'testing', + content: 'test', + }; + const RepoEditor = Vue.extend(repoEditor); + vm = createComponentWithStore(RepoEditor, store, { + file: f, + }); + + vm.$store.state.openFiles.push(f); + vm.$store.state.entries[f.path] = f; + + vm.$store.state.viewer = 'mrdiff'; + + vm.monaco = true; + + vm.$mount(); + + monacoLoader(['vs/editor/editor.main'], () => { + setTimeout(done, 0); + }); + }); + + it('attaches merge request model to editor when merge request diff', () => { + spyOn(vm.editor, 'attachMergeRequestModel').and.callThrough(); + + vm.setupEditor(); + + expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model); + }); + }); }); diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js index 1cd2e362f50..cb785ba2cd3 100644 --- a/spec/javascripts/ide/components/repo_tabs_spec.js +++ b/spec/javascripts/ide/components/repo_tabs_spec.js @@ -18,6 +18,7 @@ describe('RepoTabs', () => { viewer: 'editor', hasChanges: false, activeFile: file('activeFile'), + hasMergeRequest: false, }); openedFiles[0].active = true; @@ -58,6 +59,7 @@ describe('RepoTabs', () => { viewer: 'editor', hasChanges: false, activeFile: file('activeFile'), + hasMergeRequest: false, }, '#test-app', ); diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js index f6979e32cb3..8fc2fccb64c 100644 --- a/spec/javascripts/ide/lib/common/model_spec.js +++ b/spec/javascripts/ide/lib/common/model_spec.js @@ -11,7 +11,10 @@ describe('Multi-file editor library model', () => { spyOn(eventHub, '$on').and.callThrough(); monacoLoader(['vs/editor/editor.main'], () => { - model = new Model(monaco, file('path')); + const f = file('path'); + f.mrChange = { diff: 'ABC' }; + f.baseRaw = 'test'; + model = new Model(monaco, f); done(); }); @@ -21,9 +24,10 @@ describe('Multi-file editor library model', () => { model.dispose(); }); - it('creates original model & new model', () => { + it('creates original model & base model & new model', () => { expect(model.originalModel).not.toBeNull(); expect(model.model).not.toBeNull(); + expect(model.baseModel).not.toBeNull(); }); it('adds eventHub listener', () => { @@ -51,6 +55,12 @@ describe('Multi-file editor library model', () => { }); }); + describe('getBaseModel', () => { + it('returns base model', () => { + expect(model.getBaseModel()).toBe(model.baseModel); + }); + }); + describe('setValue', () => { it('updates models value', () => { model.setValue('testing 123'); diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js index 2ccd87de1a7..ec56ebc0341 100644 --- a/spec/javascripts/ide/lib/editor_spec.js +++ b/spec/javascripts/ide/lib/editor_spec.js @@ -143,6 +143,31 @@ describe('Multi-file editor library', () => { }); }); + describe('attachMergeRequestModel', () => { + let model; + + beforeEach(() => { + instance.createDiffInstance(document.createElement('div')); + + const f = file(); + f.mrChanges = { diff: 'ABC' }; + f.baseRaw = 'testing'; + + model = instance.createModel(f); + }); + + it('sets original & modified', () => { + spyOn(instance.instance, 'setModel'); + + instance.attachMergeRequestModel(model); + + expect(instance.instance.setModel).toHaveBeenCalledWith({ + original: model.getBaseModel(), + modified: model.getModel(), + }); + }); + }); + describe('clearEditor', () => { it('resets the editor model', () => { instance.createInstance(document.createElement('div')); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index eb8933b2b3f..479ed7ce49e 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -5,7 +5,7 @@ import router from '~/ide/ide_router'; import eventHub from '~/ide/eventhub'; import { file, resetStore } from '../../helpers'; -describe('Multi-file store file actions', () => { +describe('IDE store file actions', () => { beforeEach(() => { spyOn(router, 'push'); }); @@ -205,7 +205,7 @@ describe('Multi-file store file actions', () => { it('calls the service', done => { store - .dispatch('getFileData', localFile) + .dispatch('getFileData', { path: localFile.path }) .then(() => { expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL'); @@ -216,7 +216,7 @@ describe('Multi-file store file actions', () => { it('sets the file data', done => { store - .dispatch('getFileData', localFile) + .dispatch('getFileData', { path: localFile.path }) .then(() => { expect(localFile.blamePath).toBe('blame_path'); @@ -227,7 +227,7 @@ describe('Multi-file store file actions', () => { it('sets document title', done => { store - .dispatch('getFileData', localFile) + .dispatch('getFileData', { path: localFile.path }) .then(() => { expect(document.title).toBe('testing getFileData'); @@ -238,7 +238,7 @@ describe('Multi-file store file actions', () => { it('sets the file as active', done => { store - .dispatch('getFileData', localFile) + .dispatch('getFileData', { path: localFile.path }) .then(() => { expect(localFile.active).toBeTruthy(); @@ -247,9 +247,20 @@ describe('Multi-file store file actions', () => { .catch(done.fail); }); + it('sets the file not as active if we pass makeFileActive false', done => { + store + .dispatch('getFileData', { path: localFile.path, makeFileActive: false }) + .then(() => { + expect(localFile.active).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + it('adds the file to open files', done => { store - .dispatch('getFileData', localFile) + .dispatch('getFileData', { path: localFile.path }) .then(() => { expect(store.state.openFiles.length).toBe(1); expect(store.state.openFiles[0].name).toBe(localFile.name); @@ -272,7 +283,7 @@ describe('Multi-file store file actions', () => { it('calls getRawFileData service method', done => { store - .dispatch('getRawFileData', tmpFile) + .dispatch('getRawFileData', { path: tmpFile.path }) .then(() => { expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); @@ -283,7 +294,7 @@ describe('Multi-file store file actions', () => { it('updates file raw data', done => { store - .dispatch('getRawFileData', tmpFile) + .dispatch('getRawFileData', { path: tmpFile.path }) .then(() => { expect(tmpFile.raw).toBe('raw'); @@ -291,6 +302,22 @@ describe('Multi-file store file actions', () => { }) .catch(done.fail); }); + + it('calls also getBaseRawFileData service method', done => { + spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw')); + + tmpFile.mrChange = { new_file: false }; + + store + .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' }) + .then(() => { + expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA'); + expect(tmpFile.baseRaw).toBe('baseraw'); + + done(); + }) + .catch(done.fail); + }); }); describe('changeFileContent', () => { diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js new file mode 100644 index 00000000000..b4ec4a0b173 --- /dev/null +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -0,0 +1,110 @@ +import store from '~/ide/stores'; +import service from '~/ide/services'; +import { resetStore } from '../../helpers'; + +describe('IDE store merge request actions', () => { + beforeEach(() => { + store.state.projects.abcproject = { + mergeRequests: {}, + }; + }); + + afterEach(() => { + resetStore(store); + }); + + describe('getMergeRequestData', () => { + beforeEach(() => { + spyOn(service, 'getProjectMergeRequestData').and.returnValue( + Promise.resolve({ data: { title: 'mergerequest' } }), + ); + }); + + it('calls getProjectMergeRequestData service method', done => { + store + .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 }) + .then(() => { + expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1); + + done(); + }) + .catch(done.fail); + }); + + it('sets the Merge Request Object', done => { + store + .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 }) + .then(() => { + expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest'); + expect(store.state.currentMergeRequestId).toBe(1); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('getMergeRequestChanges', () => { + beforeEach(() => { + spyOn(service, 'getProjectMergeRequestChanges').and.returnValue( + Promise.resolve({ data: { title: 'mergerequest' } }), + ); + + store.state.projects.abcproject.mergeRequests['1'] = { changes: [] }; + }); + + it('calls getProjectMergeRequestChanges service method', done => { + store + .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 }) + .then(() => { + expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1); + + done(); + }) + .catch(done.fail); + }); + + it('sets the Merge Request Changes Object', done => { + store + .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 }) + .then(() => { + expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe( + 'mergerequest', + ); + done(); + }) + .catch(done.fail); + }); + }); + + describe('getMergeRequestVersions', () => { + beforeEach(() => { + spyOn(service, 'getProjectMergeRequestVersions').and.returnValue( + Promise.resolve({ data: [{ id: 789 }] }), + ); + + store.state.projects.abcproject.mergeRequests['1'] = { versions: [] }; + }); + + it('calls getProjectMergeRequestVersions service method', done => { + store + .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 }) + .then(() => { + expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1); + + done(); + }) + .catch(done.fail); + }); + + it('sets the Merge Request Versions Object', done => { + store + .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 }) + .then(() => { + expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1); + done(); + }) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js index 381f038067b..e0ef57a3966 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -68,9 +68,7 @@ describe('Multi-file store tree actions', () => { expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js'); expect(projectTree.tree[1].type).toBe('blob'); expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob'); - expect(projectTree.tree[0].tree[0].tree[0].name).toBe( - 'fileinsubfolder.js', - ); + expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js'); done(); }) @@ -132,9 +130,7 @@ describe('Multi-file store tree actions', () => { store .dispatch('getLastCommitData', projectTree) .then(() => { - expect(service.getTreeLastCommit).toHaveBeenCalledWith( - 'lastcommitpath', - ); + expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath'); done(); }) @@ -160,9 +156,7 @@ describe('Multi-file store tree actions', () => { .dispatch('getLastCommitData', projectTree) .then(Vue.nextTick) .then(() => { - expect(projectTree.tree[0].lastCommit.message).not.toBe( - 'commit message', - ); + expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message'); done(); }) diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js index a613f3a21cc..33733b97dff 100644 --- a/spec/javascripts/ide/stores/getters_spec.js +++ b/spec/javascripts/ide/stores/getters_spec.js @@ -2,7 +2,7 @@ import * as getters from '~/ide/stores/getters'; import state from '~/ide/stores/state'; import { file } from '../helpers'; -describe('Multi-file store getters', () => { +describe('IDE store getters', () => { let localState; beforeEach(() => { @@ -52,4 +52,24 @@ describe('Multi-file store getters', () => { expect(modifiedFiles[0].name).toBe('added'); }); }); + + describe('currentMergeRequest', () => { + it('returns Current Merge Request', () => { + localState.currentProjectId = 'abcproject'; + localState.currentMergeRequestId = 1; + localState.projects.abcproject = { + mergeRequests: { + 1: { mergeId: 1 }, + }, + }; + + expect(getters.currentMergeRequest(localState).mergeId).toBe(1); + }); + + it('returns null if no active Merge Request was found', () => { + localState.currentProjectId = 'otherproject'; + + expect(getters.currentMergeRequest(localState)).toBeNull(); + }); + }); }); diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index 4f9e00b8543..88285ee409f 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -2,7 +2,7 @@ import mutations from '~/ide/stores/mutations/file'; import state from '~/ide/stores/state'; import { file } from '../../helpers'; -describe('Multi-file store file mutations', () => { +describe('IDE store file mutations', () => { let localState; let localFile; @@ -77,6 +77,8 @@ describe('Multi-file store file mutations', () => { expect(localFile.rawPath).toBe('raw'); expect(localFile.binary).toBeTruthy(); expect(localFile.renderError).toBe('render_error'); + expect(localFile.raw).toBeNull(); + expect(localFile.baseRaw).toBeNull(); }); }); @@ -91,6 +93,17 @@ describe('Multi-file store file mutations', () => { }); }); + describe('SET_FILE_BASE_RAW_DATA', () => { + it('sets raw data from base branch', () => { + mutations.SET_FILE_BASE_RAW_DATA(localState, { + file: localFile, + baseRaw: 'testing', + }); + + expect(localFile.baseRaw).toBe('testing'); + }); + }); + describe('UPDATE_FILE_CONTENT', () => { beforeEach(() => { localFile.raw = 'test'; @@ -127,6 +140,17 @@ describe('Multi-file store file mutations', () => { }); }); + describe('SET_FILE_MERGE_REQUEST_CHANGE', () => { + it('sets file mr change', () => { + mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { + file: localFile, + mrChange: { diff: 'ABC' }, + }); + + expect(localFile.mrChange.diff).toBe('ABC'); + }); + }); + describe('DISCARD_FILE_CHANGES', () => { beforeEach(() => { localFile.content = 'test'; diff --git a/spec/javascripts/ide/stores/mutations/merge_request_spec.js b/spec/javascripts/ide/stores/mutations/merge_request_spec.js new file mode 100644 index 00000000000..f724bf464f5 --- /dev/null +++ b/spec/javascripts/ide/stores/mutations/merge_request_spec.js @@ -0,0 +1,65 @@ +import mutations from '~/ide/stores/mutations/merge_request'; +import state from '~/ide/stores/state'; + +describe('IDE store merge request mutations', () => { + let localState; + + beforeEach(() => { + localState = state(); + localState.projects = { abcproject: { mergeRequests: {} } }; + + mutations.SET_MERGE_REQUEST(localState, { + projectPath: 'abcproject', + mergeRequestId: 1, + mergeRequest: { + title: 'mr', + }, + }); + }); + + describe('SET_CURRENT_MERGE_REQUEST', () => { + it('sets current merge request', () => { + mutations.SET_CURRENT_MERGE_REQUEST(localState, 2); + + expect(localState.currentMergeRequestId).toBe(2); + }); + }); + + describe('SET_MERGE_REQUEST', () => { + it('setsmerge request data', () => { + const newMr = localState.projects.abcproject.mergeRequests[1]; + + expect(newMr.title).toBe('mr'); + expect(newMr.active).toBeTruthy(); + }); + }); + + describe('SET_MERGE_REQUEST_CHANGES', () => { + it('sets merge request changes', () => { + mutations.SET_MERGE_REQUEST_CHANGES(localState, { + projectPath: 'abcproject', + mergeRequestId: 1, + changes: { + diff: 'abc', + }, + }); + + const newMr = localState.projects.abcproject.mergeRequests[1]; + expect(newMr.changes.diff).toBe('abc'); + }); + }); + + describe('SET_MERGE_REQUEST_VERSIONS', () => { + it('sets merge request versions', () => { + mutations.SET_MERGE_REQUEST_VERSIONS(localState, { + projectPath: 'abcproject', + mergeRequestId: 1, + versions: [{ id: 123 }], + }); + + const newMr = localState.projects.abcproject.mergeRequests[1]; + expect(newMr.versions.length).toBe(1); + expect(newMr.versions[0].id).toBe(123); + }); + }); +}); diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js index 43589d54be4..25ca8eb6c0b 100644 --- a/spec/javascripts/jobs/mock_data.js +++ b/spec/javascripts/jobs/mock_data.js @@ -115,6 +115,10 @@ export default { commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6', }, }, + metadata: { + timeout_human_readable: '1m 40s', + timeout_source: 'runner', + }, merge_request: { iid: 2, path: '/root/ci-mock/merge_requests/2', diff --git a/spec/javascripts/jobs/sidebar_detail_row_spec.js b/spec/javascripts/jobs/sidebar_detail_row_spec.js index 3ac65709c4a..e6bfb0c4adc 100644 --- a/spec/javascripts/jobs/sidebar_detail_row_spec.js +++ b/spec/javascripts/jobs/sidebar_detail_row_spec.js @@ -37,4 +37,25 @@ describe('Sidebar detail row', () => { vm.$el.textContent.replace(/\s+/g, ' ').trim(), ).toEqual('this is the title: this is the value'); }); + + describe('when helpUrl not provided', () => { + it('should not render help', () => { + expect(vm.$el.querySelector('.help-button')).toBeNull(); + }); + }); + + describe('when helpUrl provided', () => { + beforeEach(() => { + vm = new SidebarDetailRow({ + propsData: { + helpUrl: 'help url', + value: 'foo', + }, + }).$mount(); + }); + + it('should render help', () => { + expect(vm.$el.querySelector('.help-button a').getAttribute('href')).toEqual('help url'); + }); + }); }); diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js index 95532ef5382..602dae514b1 100644 --- a/spec/javascripts/jobs/sidebar_details_block_spec.js +++ b/spec/javascripts/jobs/sidebar_details_block_spec.js @@ -96,6 +96,12 @@ describe('Sidebar details block', () => { ).toEqual('Runner: #1'); }); + it('should render timeout information', () => { + expect( + trimWhitespace(vm.$el.querySelector('.js-job-timeout')), + ).toEqual('Timeout: 1m 40s (from runner)'); + }); + it('should render coverage', () => { expect( trimWhitespace(vm.$el.querySelector('.js-job-coverage')), diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js index 19504e4f7c8..cda550760fe 100644 --- a/spec/javascripts/notes/components/noteable_discussion_spec.js +++ b/spec/javascripts/notes/components/noteable_discussion_spec.js @@ -25,26 +25,34 @@ describe('issue_discussion component', () => { }); it('should render user avatar', () => { - expect(vm.$el.querySelector('.user-avatar-link')).toBeDefined(); + expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull(); }); it('should render discussion header', () => { - expect(vm.$el.querySelector('.discussion-header')).toBeDefined(); + expect(vm.$el.querySelector('.discussion-header')).not.toBeNull(); expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length); }); describe('actions', () => { it('should render reply button', () => { - expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual('Reply...'); + expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual( + 'Reply...', + ); }); - it('should toggle reply form', (done) => { + it('should toggle reply form', done => { vm.$el.querySelector('.js-vue-discussion-reply').click(); Vue.nextTick(() => { - expect(vm.$refs.noteForm).toBeDefined(); + expect(vm.$refs.noteForm).not.toBeNull(); expect(vm.isReplying).toEqual(true); done(); }); }); + + it('does not render jump to discussion button', () => { + expect( + vm.$el.querySelector('*[data-original-title="Jump to next unresolved discussion"]'), + ).toBeNull(); + }); }); }); diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js index 5be13ed0dfe..2d88cee61f1 100644 --- a/spec/javascripts/notes/mock_data.js +++ b/spec/javascripts/notes/mock_data.js @@ -1,4 +1,3 @@ -/* eslint-disable */ export const notesDataMock = { discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json', lastFetchedAt: 1501862675, @@ -43,7 +42,8 @@ export const noteableDataMock = { milestone: null, milestone_id: null, moved_to_id: null, - preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', + preview_note_path: + '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', project_id: 2, state: 'opened', time_estimate: 0, @@ -60,465 +60,504 @@ export const individualNote = { expanded: true, id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', individual_note: true, - notes: [{ - id: 1390, - attachment: { - url: null, - filename: null, - image: false, - }, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: 'test', - path: '/root', + notes: [ + { + id: 1390, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: 'test', + path: '/root', + }, + created_at: '2017-08-01T17: 09: 33.762Z', + updated_at: '2017-08-01T17: 09: 33.762Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: null, + human_access: 'Owner', + note: 'sdfdsaf', + note_html: "<p dir='auto'>sdfdsaf</p>", + current_user: { can_edit: true }, + discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + emoji_awardable: true, + award_emoji: [ + { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } }, + { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, + ], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1390', }, - created_at: '2017-08-01T17: 09: 33.762Z', - updated_at: '2017-08-01T17: 09: 33.762Z', - system: false, - noteable_id: 98, - noteable_type: 'Issue', - type: null, - human_access: 'Owner', - note: 'sdfdsaf', - note_html: '<p dir=\'auto\'>sdfdsaf</p>', - current_user: { can_edit: true }, - discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', - emoji_awardable: true, - award_emoji: [ - { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } }, - { name: 'art', user: { id: 1, name: 'Root', username: 'root' } }, - ], - toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', - report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1', - path: '/gitlab-org/gitlab-ce/notes/1390', - }], + ], reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', }; export const note = { - "id": 546, - "attachment": { - "url": null, - "filename": null, - "image": false + id: 546, + attachment: { + url: null, + filename: null, + image: false, }, - "author": { - "id": 1, - "name": "Administrator", - "username": "root", - "state": "active", - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "path": "/root" + author: { + id: 1, + name: 'Administrator', + username: 'root', + state: 'active', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + path: '/root', }, - "created_at": "2017-08-10T15:24:03.087Z", - "updated_at": "2017-08-10T15:24:03.087Z", - "system": false, - "noteable_id": 67, - "noteable_type": "Issue", - "noteable_iid": 7, - "type": null, - "human_access": "Owner", - "note": "Vel id placeat reprehenderit sit numquam.", - "note_html": "<p dir=\"auto\">Vel id placeat reprehenderit sit numquam.</p>", - "current_user": { - "can_edit": true + created_at: '2017-08-10T15:24:03.087Z', + updated_at: '2017-08-10T15:24:03.087Z', + system: false, + noteable_id: 67, + noteable_type: 'Issue', + noteable_iid: 7, + type: null, + human_access: 'Owner', + note: 'Vel id placeat reprehenderit sit numquam.', + note_html: '<p dir="auto">Vel id placeat reprehenderit sit numquam.</p>', + current_user: { + can_edit: true, }, - "discussion_id": "d3842a451b7f3d9a5dfce329515127b2d29a4cd0", - "emoji_awardable": true, - "award_emoji": [{ - "name": "baseball", - "user": { - "id": 1, - "name": "Administrator", - "username": "root" - } - }, { - "name": "bath_tone3", - "user": { - "id": 1, - "name": "Administrator", - "username": "root" - } - }], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/546" - } + discussion_id: 'd3842a451b7f3d9a5dfce329515127b2d29a4cd0', + emoji_awardable: true, + award_emoji: [ + { + name: 'baseball', + user: { + id: 1, + name: 'Administrator', + username: 'root', + }, + }, + { + name: 'bath_tone3', + user: { + id: 1, + name: 'Administrator', + username: 'root', + }, + }, + ], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/546', +}; export const discussionMock = { id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', expanded: true, - notes: [{ - id: 1395, - attachment: { - url: null, - filename: null, - image: false, - }, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', - }, - created_at: '2017-08-02T10:51:58.559Z', - updated_at: '2017-08-02T10:51:58.559Z', - system: false, - noteable_id: 98, - noteable_type: 'Issue', - type: 'DiscussionNote', - human_access: 'Owner', - note: 'THIS IS A DICUSSSION!', - note_html: '<p dir=\'auto\'>THIS IS A DICUSSSION!</p>', - current_user: { - can_edit: true, - }, - discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', - emoji_awardable: true, - award_emoji: [], - toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji', - report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1', - path: '/gitlab-org/gitlab-ce/notes/1395', - }, { - id: 1396, - attachment: { - url: null, - filename: null, - image: false, - }, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', - }, - created_at: '2017-08-02T10:56:50.980Z', - updated_at: '2017-08-03T14:19:35.691Z', - system: false, - noteable_id: 98, - noteable_type: 'Issue', - type: 'DiscussionNote', - human_access: 'Owner', - note: 'sadfasdsdgdsf', - note_html: '<p dir=\'auto\'>sadfasdsdgdsf</p>', - last_edited_at: '2017-08-03T14:19:35.691Z', - last_edited_by: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', - }, - current_user: { - can_edit: true, + notes: [ + { + id: 1395, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:51:58.559Z', + updated_at: '2017-08-02T10:51:58.559Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'THIS IS A DICUSSSION!', + note_html: "<p dir='auto'>THIS IS A DICUSSSION!</p>", + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1395', }, - discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', - emoji_awardable: true, - award_emoji: [], - toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji', - report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1', - path: '/gitlab-org/gitlab-ce/notes/1396', - }, { - id: 1437, - attachment: { - url: null, - filename: null, - image: false, + { + id: 1396, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:56:50.980Z', + updated_at: '2017-08-03T14:19:35.691Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'sadfasdsdgdsf', + note_html: "<p dir='auto'>sadfasdsdgdsf</p>", + last_edited_at: '2017-08-03T14:19:35.691Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1396', }, - author: { - id: 1, - name: 'Root', - username: 'root', - state: 'active', - avatar_url: null, - path: '/root', + { + id: 1437, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-03T18:11:18.780Z', + updated_at: '2017-08-04T09:52:31.062Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: 'DiscussionNote', + human_access: 'Owner', + note: 'adsfasf Should disappear', + note_html: "<p dir='auto'>adsfasf Should disappear</p>", + last_edited_at: '2017-08-04T09:52:31.062Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + current_user: { + can_edit: true, + }, + discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1437', }, - created_at: '2017-08-03T18:11:18.780Z', - updated_at: '2017-08-04T09:52:31.062Z', - system: false, - noteable_id: 98, - noteable_type: 'Issue', - type: 'DiscussionNote', - human_access: 'Owner', - note: 'adsfasf Should disappear', - note_html: '<p dir=\'auto\'>adsfasf Should disappear</p>', - last_edited_at: '2017-08-04T09:52:31.062Z', - last_edited_by: { + ], + individual_note: false, +}; + +export const loggedOutnoteableData = { + id: 98, + iid: 26, + author_id: 1, + description: '', + lock_version: 1, + milestone_id: null, + state: 'opened', + title: 'asdsa', + updated_by_id: 1, + created_at: '2017-02-07T10:11:18.395Z', + updated_at: '2017-08-08T10:22:51.564Z', + time_estimate: 0, + total_time_spent: 0, + human_time_estimate: null, + human_total_time_spent: null, + milestone: null, + labels: [], + branch_name: null, + confidential: false, + assignees: [ + { id: 1, name: 'Root', username: 'root', state: 'active', avatar_url: null, - path: '/root', + web_url: 'http://localhost:3000/root', }, - current_user: { - can_edit: true, - }, - discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1', - emoji_awardable: true, - award_emoji: [], - toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji', - report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1', - path: '/gitlab-org/gitlab-ce/notes/1437', - }], - individual_note: false, -}; - -export const loggedOutnoteableData = { - "id": 98, - "iid": 26, - "author_id": 1, - "description": "", - "lock_version": 1, - "milestone_id": null, - "state": "opened", - "title": "asdsa", - "updated_by_id": 1, - "created_at": "2017-02-07T10:11:18.395Z", - "updated_at": "2017-08-08T10:22:51.564Z", - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": null, - "human_total_time_spent": null, - "milestone": null, - "labels": [], - "branch_name": null, - "confidential": false, - "assignees": [{ - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "web_url": "http://localhost:3000/root" - }], - "due_date": null, - "moved_to_id": null, - "project_id": 2, - "web_url": "/gitlab-org/gitlab-ce/issues/26", - "current_user": { - "can_create_note": false, - "can_update": false + ], + due_date: null, + moved_to_id: null, + project_id: 2, + web_url: '/gitlab-org/gitlab-ce/issues/26', + current_user: { + can_create_note: false, + can_update: false, }, - "create_note_path": "/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue", - "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue" -} + create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue', + preview_note_path: + '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue', +}; export const INDIVIDUAL_NOTE_RESPONSE_MAP = { - 'GET': { - '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{ - "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "expanded": true, - "notes": [{ - "id": 1390, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-01T17:09:33.762Z", - "updated_at": "2017-08-01T17:09:33.762Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "sdfdsaf", - "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd", - "emoji_awardable": true, - "award_emoji": [{ - "name": "baseball", - "user": { - "id": 1, - "name": "Root", - "username": "root" - } - }, { - "name": "art", - "user": { - "id": 1, - "name": "Root", - "username": "root" - } - }], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1390" - }], - "individual_note": true - }, { - "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "expanded": true, - "notes": [{ - "id": 1391, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-02T10:51:38.685Z", - "updated_at": "2017-08-02T10:51:38.685Z", - "system": false, - "noteable_id": 98, - "noteable_type": "Issue", - "type": null, - "human_access": "Owner", - "note": "New note!", - "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1391" - }], - "individual_note": true - }], + GET: { + '/gitlab-org/gitlab-ce/issues/26/discussions.json': [ + { + id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + expanded: true, + notes: [ + { + id: 1390, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-01T17:09:33.762Z', + updated_at: '2017-08-01T17:09:33.762Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: null, + human_access: 'Owner', + note: 'sdfdsaf', + note_html: '\u003cp dir="auto"\u003esdfdsaf\u003c/p\u003e', + current_user: { + can_edit: true, + }, + discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd', + emoji_awardable: true, + award_emoji: [ + { + name: 'baseball', + user: { + id: 1, + name: 'Root', + username: 'root', + }, + }, + { + name: 'art', + user: { + id: 1, + name: 'Root', + username: 'root', + }, + }, + ], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1390', + }, + ], + individual_note: true, + }, + { + id: '70d5c92a4039a36c70100c6691c18c27e4b0a790', + reply_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790', + expanded: true, + notes: [ + { + id: 1391, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-02T10:51:38.685Z', + updated_at: '2017-08-02T10:51:38.685Z', + system: false, + noteable_id: 98, + noteable_type: 'Issue', + type: null, + human_access: 'Owner', + note: 'New note!', + note_html: '\u003cp dir="auto"\u003eNew note!\u003c/p\u003e', + current_user: { + can_edit: true, + }, + discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1391', + }, + ], + individual_note: true, + }, + ], '/gitlab-org/gitlab-ce/noteable/issue/98/notes': { last_fetched_at: 1512900838, notes: [], }, }, - 'PUT': { + PUT: { '/gitlab-org/gitlab-ce/notes/1471': { - "commands_changes": null, - "valid": true, - "id": 1471, - "attachment": null, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" + commands_changes: null, + valid: true, + id: 1471, + attachment: null, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', }, - "created_at": "2017-08-08T16:53:00.666Z", - "updated_at": "2017-12-10T11:03:21.876Z", - "system": false, - "noteable_id": 124, - "noteable_type": "Issue", - "noteable_iid": 29, - "type": "DiscussionNote", - "human_access": "Owner", - "note": "Adding a comment", - "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", - "last_edited_at": "2017-12-10T11:03:21.876Z", - "last_edited_by": { - "id": 1, - "name": 'Root', - "username": 'root', - "state": 'active', - "avatar_url": null, - "path": '/root', + created_at: '2017-08-08T16:53:00.666Z', + updated_at: '2017-12-10T11:03:21.876Z', + system: false, + noteable_id: 124, + noteable_type: 'Issue', + noteable_iid: 29, + type: 'DiscussionNote', + human_access: 'Owner', + note: 'Adding a comment', + note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e', + last_edited_at: '2017-12-10T11:03:21.876Z', + last_edited_by: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', }, - "current_user": { - "can_edit": true + current_user: { + can_edit: true, }, - "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1471" + discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1471', }, - } + }, }; export const DISCUSSION_NOTE_RESPONSE_MAP = { ...INDIVIDUAL_NOTE_RESPONSE_MAP, - 'GET': { + GET: { ...INDIVIDUAL_NOTE_RESPONSE_MAP.GET, - '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{ - "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "expanded": true, - "notes": [{ - "id": 1471, - "attachment": { - "url": null, - "filename": null, - "image": false - }, - "author": { - "id": 1, - "name": "Root", - "username": "root", - "state": "active", - "avatar_url": null, - "path": "/root" - }, - "created_at": "2017-08-08T16:53:00.666Z", - "updated_at": "2017-08-08T16:53:00.666Z", - "system": false, - "noteable_id": 124, - "noteable_type": "Issue", - "noteable_iid": 29, - "type": "DiscussionNote", - "human_access": "Owner", - "note": "Adding a comment", - "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e", - "current_user": { - "can_edit": true - }, - "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052", - "emoji_awardable": true, - "award_emoji": [], - "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji", - "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1", - "path": "/gitlab-org/gitlab-ce/notes/1471" - }], - "individual_note": false - }], + '/gitlab-org/gitlab-ce/issues/26/discussions.json': [ + { + id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', + reply_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', + expanded: true, + notes: [ + { + id: 1471, + attachment: { + url: null, + filename: null, + image: false, + }, + author: { + id: 1, + name: 'Root', + username: 'root', + state: 'active', + avatar_url: null, + path: '/root', + }, + created_at: '2017-08-08T16:53:00.666Z', + updated_at: '2017-08-08T16:53:00.666Z', + system: false, + noteable_id: 124, + noteable_type: 'Issue', + noteable_iid: 29, + type: 'DiscussionNote', + human_access: 'Owner', + note: 'Adding a comment', + note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e', + current_user: { + can_edit: true, + }, + discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052', + emoji_awardable: true, + award_emoji: [], + toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji', + report_abuse_path: + '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1', + path: '/gitlab-org/gitlab-ce/notes/1471', + }, + ], + individual_note: false, + }, + ], }, }; export function individualNoteInterceptor(request, next) { const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url]; - next(request.respondWith(JSON.stringify(body), { - status: 200, - })); + next( + request.respondWith(JSON.stringify(body), { + status: 200, + }), + ); } export function discussionNoteInterceptor(request, next) { const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url]; - next(request.respondWith(JSON.stringify(body), { - status: 200, - })); + next( + request.respondWith(JSON.stringify(body), { + status: 200, + }), + ); } diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 91249b2c79e..520a25cc5c6 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -5,7 +5,13 @@ import * as actions from '~/notes/stores/actions'; import store from '~/notes/stores'; import testAction from '../../helpers/vuex_action_helper'; import { resetStore } from '../helpers'; -import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data'; +import { + discussionMock, + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, +} from '../mock_data'; describe('Actions Notes Store', () => { afterEach(() => { @@ -13,66 +19,103 @@ describe('Actions Notes Store', () => { }); describe('setNotesData', () => { - it('should set received notes data', (done) => { - testAction(actions.setNotesData, null, { notesData: {} }, [ - { type: 'SET_NOTES_DATA', payload: notesDataMock }, - ], done); + it('should set received notes data', done => { + testAction( + actions.setNotesData, + notesDataMock, + { notesData: {} }, + [{ type: 'SET_NOTES_DATA', payload: notesDataMock }], + [], + done, + ); }); }); describe('setNoteableData', () => { - it('should set received issue data', (done) => { - testAction(actions.setNoteableData, null, { noteableData: {} }, [ - { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock }, - ], done); + it('should set received issue data', done => { + testAction( + actions.setNoteableData, + noteableDataMock, + { noteableData: {} }, + [{ type: 'SET_NOTEABLE_DATA', payload: noteableDataMock }], + [], + done, + ); }); }); describe('setUserData', () => { - it('should set received user data', (done) => { - testAction(actions.setUserData, null, { userData: {} }, [ - { type: 'SET_USER_DATA', payload: userDataMock }, - ], done); + it('should set received user data', done => { + testAction( + actions.setUserData, + userDataMock, + { userData: {} }, + [{ type: 'SET_USER_DATA', payload: userDataMock }], + [], + done, + ); }); }); describe('setLastFetchedAt', () => { - it('should set received timestamp', (done) => { - testAction(actions.setLastFetchedAt, null, { lastFetchedAt: {} }, [ - { type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }, - ], done); + it('should set received timestamp', done => { + testAction( + actions.setLastFetchedAt, + 'timestamp', + { lastFetchedAt: {} }, + [{ type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }], + [], + done, + ); }); }); describe('setInitialNotes', () => { - it('should set initial notes', (done) => { - testAction(actions.setInitialNotes, null, { notes: [] }, [ - { type: 'SET_INITIAL_NOTES', payload: [individualNote] }, - ], done); + it('should set initial notes', done => { + testAction( + actions.setInitialNotes, + [individualNote], + { notes: [] }, + [{ type: 'SET_INITIAL_NOTES', payload: [individualNote] }], + [], + done, + ); }); }); describe('setTargetNoteHash', () => { - it('should set target note hash', (done) => { - testAction(actions.setTargetNoteHash, null, { notes: [] }, [ - { type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }, - ], done); + it('should set target note hash', done => { + testAction( + actions.setTargetNoteHash, + 'hash', + { notes: [] }, + [{ type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }], + [], + done, + ); }); }); describe('toggleDiscussion', () => { - it('should toggle discussion', (done) => { - testAction(actions.toggleDiscussion, null, { notes: [discussionMock] }, [ - { type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }, - ], done); + it('should toggle discussion', done => { + testAction( + actions.toggleDiscussion, + { discussionId: discussionMock.id }, + { notes: [discussionMock] }, + [{ type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }], + [], + done, + ); }); }); describe('async methods', () => { const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify({}), { - status: 200, - })); + next( + request.respondWith(JSON.stringify({}), { + status: 200, + }), + ); }; beforeEach(() => { @@ -84,8 +127,9 @@ describe('Actions Notes Store', () => { }); describe('closeIssue', () => { - it('sets state as closed', (done) => { - store.dispatch('closeIssue', { notesData: { closeIssuePath: '' } }) + it('sets state as closed', done => { + store + .dispatch('closeIssue', { notesData: { closeIssuePath: '' } }) .then(() => { expect(store.state.noteableData.state).toEqual('closed'); expect(store.state.isToggleStateButtonLoading).toEqual(false); @@ -96,8 +140,9 @@ describe('Actions Notes Store', () => { }); describe('reopenIssue', () => { - it('sets state as reopened', (done) => { - store.dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } }) + it('sets state as reopened', done => { + store + .dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } }) .then(() => { expect(store.state.noteableData.state).toEqual('reopened'); expect(store.state.isToggleStateButtonLoading).toEqual(false); @@ -110,7 +155,7 @@ describe('Actions Notes Store', () => { describe('emitStateChangedEvent', () => { it('emits an event on the document', () => { - document.addEventListener('issuable_vue_app:change', (event) => { + document.addEventListener('issuable_vue_app:change', event => { expect(event.detail.data).toEqual({ id: '1', state: 'closed' }); expect(event.detail.isClosed).toEqual(false); }); @@ -120,40 +165,47 @@ describe('Actions Notes Store', () => { }); describe('toggleStateButtonLoading', () => { - it('should set loading as true', (done) => { - testAction(actions.toggleStateButtonLoading, true, {}, [ - { type: 'TOGGLE_STATE_BUTTON_LOADING', payload: true }, - ], done); + it('should set loading as true', done => { + testAction( + actions.toggleStateButtonLoading, + true, + {}, + [{ type: 'TOGGLE_STATE_BUTTON_LOADING', payload: true }], + [], + done, + ); }); - it('should set loading as false', (done) => { - testAction(actions.toggleStateButtonLoading, false, {}, [ - { type: 'TOGGLE_STATE_BUTTON_LOADING', payload: false }, - ], done); + it('should set loading as false', done => { + testAction( + actions.toggleStateButtonLoading, + false, + {}, + [{ type: 'TOGGLE_STATE_BUTTON_LOADING', payload: false }], + [], + done, + ); }); }); describe('toggleIssueLocalState', () => { - it('sets issue state as closed', (done) => { - testAction(actions.toggleIssueLocalState, 'closed', {}, [ - { type: 'CLOSE_ISSUE', payload: 'closed' }, - ], done); + it('sets issue state as closed', done => { + testAction(actions.toggleIssueLocalState, 'closed', {}, [{ type: 'CLOSE_ISSUE' }], [], done); }); - it('sets issue state as reopened', (done) => { - testAction(actions.toggleIssueLocalState, 'reopened', {}, [ - { type: 'REOPEN_ISSUE', payload: 'reopened' }, - ], done); + it('sets issue state as reopened', done => { + testAction(actions.toggleIssueLocalState, 'reopened', {}, [{ type: 'REOPEN_ISSUE' }], [], done); }); }); describe('poll', () => { - beforeEach((done) => { + beforeEach(done => { jasmine.clock().install(); spyOn(Vue.http, 'get').and.callThrough(); - store.dispatch('setNotesData', notesDataMock) + store + .dispatch('setNotesData', notesDataMock) .then(done) .catch(done.fail); }); @@ -162,23 +214,29 @@ describe('Actions Notes Store', () => { jasmine.clock().uninstall(); }); - it('calls service with last fetched state', (done) => { + it('calls service with last fetched state', done => { const interceptor = (request, next) => { - next(request.respondWith(JSON.stringify({ - notes: [], - last_fetched_at: '123456', - }), { - status: 200, - headers: { - 'poll-interval': '1000', - }, - })); + next( + request.respondWith( + JSON.stringify({ + notes: [], + last_fetched_at: '123456', + }), + { + status: 200, + headers: { + 'poll-interval': '1000', + }, + }, + ), + ); }; Vue.http.interceptors.push(interceptor); Vue.http.interceptors.push(headersInterceptor); - store.dispatch('poll') + store + .dispatch('poll') .then(() => new Promise(resolve => requestAnimationFrame(resolve))) .then(() => { expect(Vue.http.get).toHaveBeenCalledWith(jasmine.anything(), { @@ -192,9 +250,12 @@ describe('Actions Notes Store', () => { jasmine.clock().tick(1500); }) - .then(() => new Promise((resolve) => { - requestAnimationFrame(resolve); - })) + .then( + () => + new Promise(resolve => { + requestAnimationFrame(resolve); + }), + ) .then(() => { expect(Vue.http.get.calls.count()).toBe(2); expect(Vue.http.get.calls.mostRecent().args[1].headers).toEqual({ diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js index 080158a8ee0..a24f8204fe1 100644 --- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js +++ b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js @@ -12,6 +12,7 @@ describe('Promote label modal', () => { labelColor: '#5cb85c', labelTextColor: '#ffffff', url: `${gl.TEST_HOST}/dummy/promote/labels`, + groupName: 'group', }; describe('Modal title and description', () => { @@ -24,7 +25,7 @@ describe('Promote label modal', () => { }); it('contains the proper description', () => { - expect(vm.text).toContain('Promoting this label will make it available for all projects inside the group'); + expect(vm.text).toContain(`Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${labelMockData.groupName}`); }); it('contains a label span with the color', () => { diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js index 22956929e7b..8b220423637 100644 --- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js +++ b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js @@ -10,6 +10,7 @@ describe('Promote milestone modal', () => { const milestoneMockData = { milestoneTitle: 'v1.0', url: `${gl.TEST_HOST}/dummy/promote/milestones`, + groupName: 'group', }; describe('Modal title and description', () => { @@ -22,7 +23,7 @@ describe('Promote milestone modal', () => { }); it('contains the proper description', () => { - expect(vm.text).toContain('Promoting this milestone will make it available for all projects inside the group.'); + expect(vm.text).toContain(`Promoting ${milestoneMockData.milestoneTitle} will make it available for all projects inside ${milestoneMockData.groupName}.`); }); it('contains the correct title', () => { diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js index b9494f86d74..70eba98e939 100644 --- a/spec/javascripts/pipelines/graph/mock_data.js +++ b/spec/javascripts/pipelines/graph/mock_data.js @@ -1,232 +1,261 @@ -/* eslint-disable quote-props, quotes, comma-dangle */ export default { - "id": 123, - "user": { - "name": "Root", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": null, - "web_url": "http://localhost:3000/root" + id: 123, + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', }, - "active": false, - "coverage": null, - "path": "/root/ci-mock/pipelines/123", - "details": { - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/pipelines/123", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico" + active: false, + coverage: null, + path: '/root/ci-mock/pipelines/123', + details: { + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', }, - "duration": 9, - "finished_at": "2017-04-19T14:30:27.542Z", - "stages": [{ - "name": "test", - "title": "test: passed", - "groups": [{ - "name": "test", - "size": 1, - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/builds/4153", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/root/ci-mock/builds/4153/retry", - "method": "post" - } + duration: 9, + finished_at: '2017-04-19T14:30:27.542Z', + stages: [ + { + name: 'test', + title: 'test: passed', + groups: [ + { + name: 'test', + size: 1, + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4153', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4153/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4153, + name: 'test', + build_path: '/root/ci-mock/builds/4153', + retry_path: '/root/ci-mock/builds/4153/retry', + playable: false, + created_at: '2017-04-13T09:25:18.959Z', + updated_at: '2017-04-13T09:25:23.118Z', + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4153', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4153/retry', + method: 'post', + }, + }, + }, + ], + }, + ], + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123#test', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', }, - "jobs": [{ - "id": 4153, - "name": "test", - "build_path": "/root/ci-mock/builds/4153", - "retry_path": "/root/ci-mock/builds/4153/retry", - "playable": false, - "created_at": "2017-04-13T09:25:18.959Z", - "updated_at": "2017-04-13T09:25:23.118Z", - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/builds/4153", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/root/ci-mock/builds/4153/retry", - "method": "post" - } - } - }] - }], - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/pipelines/123#test", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico" + path: '/root/ci-mock/pipelines/123#test', + dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test', }, - "path": "/root/ci-mock/pipelines/123#test", - "dropdown_path": "/root/ci-mock/pipelines/123/stage.json?stage=test" - }, { - "name": "deploy", - "title": "deploy: passed", - "groups": [{ - "name": "deploy to production", - "size": 1, - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/builds/4166", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/root/ci-mock/builds/4166/retry", - "method": "post" - } + { + name: 'deploy', + title: 'deploy: passed', + groups: [ + { + name: 'deploy to production', + size: 1, + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4166', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4166/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4166, + name: 'deploy to production', + build_path: '/root/ci-mock/builds/4166', + retry_path: '/root/ci-mock/builds/4166/retry', + playable: false, + created_at: '2017-04-19T14:29:46.463Z', + updated_at: '2017-04-19T14:30:27.498Z', + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4166', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4166/retry', + method: 'post', + }, + }, + }, + ], + }, + { + name: 'deploy to staging', + size: 1, + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4159', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4159/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4159, + name: 'deploy to staging', + build_path: '/root/ci-mock/builds/4159', + retry_path: '/root/ci-mock/builds/4159/retry', + playable: false, + created_at: '2017-04-18T16:32:08.420Z', + updated_at: '2017-04-18T16:32:12.631Z', + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4159', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4159/retry', + method: 'post', + }, + }, + }, + ], + }, + ], + status: { + icon: 'icon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123#deploy', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', }, - "jobs": [{ - "id": 4166, - "name": "deploy to production", - "build_path": "/root/ci-mock/builds/4166", - "retry_path": "/root/ci-mock/builds/4166/retry", - "playable": false, - "created_at": "2017-04-19T14:29:46.463Z", - "updated_at": "2017-04-19T14:30:27.498Z", - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/builds/4166", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/root/ci-mock/builds/4166/retry", - "method": "post" - } - } - }] - }, { - "name": "deploy to staging", - "size": 1, - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/builds/4159", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/root/ci-mock/builds/4159/retry", - "method": "post" - } - }, - "jobs": [{ - "id": 4159, - "name": "deploy to staging", - "build_path": "/root/ci-mock/builds/4159", - "retry_path": "/root/ci-mock/builds/4159/retry", - "playable": false, - "created_at": "2017-04-18T16:32:08.420Z", - "updated_at": "2017-04-18T16:32:12.631Z", - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/builds/4159", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico", - "action": { - "icon": "retry", - "title": "Retry", - "path": "/root/ci-mock/builds/4159/retry", - "method": "post" - } - } - }] - }], - "status": { - "icon": "icon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/ci-mock/pipelines/123#deploy", - "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico" + path: '/root/ci-mock/pipelines/123#deploy', + dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy', + }, + ], + artifacts: [], + manual_actions: [ + { + name: 'deploy to production', + path: '/root/ci-mock/builds/4166/play', + playable: false, }, - "path": "/root/ci-mock/pipelines/123#deploy", - "dropdown_path": "/root/ci-mock/pipelines/123/stage.json?stage=deploy" - }], - "artifacts": [], - "manual_actions": [{ - "name": "deploy to production", - "path": "/root/ci-mock/builds/4166/play", - "playable": false - }] + ], }, - "flags": { - "latest": true, - "triggered": false, - "stuck": false, - "yaml_errors": false, - "retryable": false, - "cancelable": false + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: false, + cancelable: false, }, - "ref": { - "name": "master", - "path": "/root/ci-mock/tree/master", - "tag": false, - "branch": true + ref: { + name: 'master', + path: '/root/ci-mock/tree/master', + tag: false, + branch: true, }, - "commit": { - "id": "798e5f902592192afaba73f4668ae30e56eae492", - "short_id": "798e5f90", - "title": "Merge branch 'new-branch' into 'master'\r", - "created_at": "2017-04-13T10:25:17.000+01:00", - "parent_ids": ["54d483b1ed156fbbf618886ddf7ab023e24f8738", "c8e2d38a6c538822e81c57022a6e3a0cfedebbcc"], - "message": "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1", - "author_name": "Root", - "author_email": "admin@example.com", - "authored_date": "2017-04-13T10:25:17.000+01:00", - "committer_name": "Root", - "committer_email": "admin@example.com", - "committed_date": "2017-04-13T10:25:17.000+01:00", - "author": { - "name": "Root", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": null, - "web_url": "http://localhost:3000/root" + commit: { + id: '798e5f902592192afaba73f4668ae30e56eae492', + short_id: '798e5f90', + title: "Merge branch 'new-branch' into 'master'\r", + created_at: '2017-04-13T10:25:17.000+01:00', + parent_ids: [ + '54d483b1ed156fbbf618886ddf7ab023e24f8738', + 'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc', + ], + message: + "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1", + author_name: 'Root', + author_email: 'admin@example.com', + authored_date: '2017-04-13T10:25:17.000+01:00', + committer_name: 'Root', + committer_email: 'admin@example.com', + committed_date: '2017-04-13T10:25:17.000+01:00', + author: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', }, - "author_gravatar_url": null, - "commit_url": "http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492", - "commit_path": "/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492" + author_gravatar_url: null, + commit_url: + 'http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492', + commit_path: '/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492', }, - "created_at": "2017-04-13T09:25:18.881Z", - "updated_at": "2017-04-19T14:30:27.561Z" + created_at: '2017-04-13T09:25:18.881Z', + updated_at: '2017-04-19T14:30:27.561Z', }; diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js index 3c9da4f107b..bc4c444655a 100644 --- a/spec/javascripts/registry/stores/actions_spec.js +++ b/spec/javascripts/registry/stores/actions_spec.js @@ -29,57 +29,96 @@ describe('Actions Registry Store', () => { describe('fetchRepos', () => { beforeEach(() => { interceptor = (request, next) => { - next(request.respondWith(JSON.stringify(reposServerResponse), { - status: 200, - })); + next( + request.respondWith(JSON.stringify(reposServerResponse), { + status: 200, + }), + ); }; Vue.http.interceptors.push(interceptor); }); - it('should set receveived repos', (done) => { - testAction(actions.fetchRepos, null, mockedState, [ - { type: types.TOGGLE_MAIN_LOADING }, - { type: types.SET_REPOS_LIST, payload: reposServerResponse }, - ], done); + it('should set receveived repos', done => { + testAction( + actions.fetchRepos, + null, + mockedState, + [ + { type: types.TOGGLE_MAIN_LOADING }, + { type: types.TOGGLE_MAIN_LOADING }, + { type: types.SET_REPOS_LIST, payload: reposServerResponse }, + ], + [], + done, + ); }); }); describe('fetchList', () => { beforeEach(() => { interceptor = (request, next) => { - next(request.respondWith(JSON.stringify(registryServerResponse), { - status: 200, - })); + next( + request.respondWith(JSON.stringify(registryServerResponse), { + status: 200, + }), + ); }; Vue.http.interceptors.push(interceptor); }); - it('should set received list', (done) => { + it('should set received list', done => { mockedState.repos = parsedReposServerResponse; - testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [ - { type: types.TOGGLE_REGISTRY_LIST_LOADING }, - { type: types.SET_REGISTRY_LIST, payload: registryServerResponse }, - ], done); + const repo = mockedState.repos[1]; + + testAction( + actions.fetchList, + { repo }, + mockedState, + [ + { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo }, + { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo }, + { + type: types.SET_REGISTRY_LIST, + payload: { + repo, + resp: registryServerResponse, + headers: jasmine.anything(), + }, + }, + ], + [], + done, + ); }); }); }); describe('setMainEndpoint', () => { - it('should commit set main endpoint', (done) => { - testAction(actions.setMainEndpoint, 'endpoint', mockedState, [ - { type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }, - ], done); + it('should commit set main endpoint', done => { + testAction( + actions.setMainEndpoint, + 'endpoint', + mockedState, + [{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }], + [], + done, + ); }); }); describe('toggleLoading', () => { - it('should commit toggle main loading', (done) => { - testAction(actions.toggleLoading, null, mockedState, [ - { type: types.TOGGLE_MAIN_LOADING }, - ], done); + it('should commit toggle main loading', done => { + testAction( + actions.toggleLoading, + null, + mockedState, + [{ type: types.TOGGLE_MAIN_LOADING }], + [], + done, + ); }); }); }); diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js index 88a33caf2e3..0c173062835 100644 --- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js @@ -62,4 +62,22 @@ describe('Confidential Issue Sidebar Block', () => { done(); }); }); + + it('displays the edit form when opened from collapsed state', (done) => { + expect(vm1.edit).toBe(false); + + vm1.$el.querySelector('.sidebar-collapsed-icon').click(); + + expect(vm1.edit).toBe(true); + + setTimeout(() => { + expect( + vm1.$el + .innerHTML + .includes('You are going to turn off the confidentiality.'), + ).toBe(true); + + done(); + }); + }); }); diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js index 696fca516bc..9abc3daf221 100644 --- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js +++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js @@ -68,4 +68,22 @@ describe('LockIssueSidebar', () => { done(); }); }); + + it('displays the edit form when opened from collapsed state', (done) => { + expect(vm1.isLockDialogOpen).toBe(false); + + vm1.$el.querySelector('.sidebar-collapsed-icon').click(); + + expect(vm1.isLockDialogOpen).toBe(true); + + setTimeout(() => { + expect( + vm1.$el + .innerHTML + .includes('Unlock this issue?'), + ).toBe(true); + + done(); + }); + }); }); diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js index d9e84e35f69..8b6e8b24f00 100644 --- a/spec/javascripts/sidebar/mock_data.js +++ b/spec/javascripts/sidebar/mock_data.js @@ -1,7 +1,5 @@ -/* eslint-disable quote-props*/ - const RESPONSE_MAP = { - 'GET': { + GET: { '/gitlab-org/gitlab-shell/issues/5.json': { id: 45, iid: 5, @@ -27,7 +25,8 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/user0', }, { @@ -35,7 +34,8 @@ const RESPONSE_MAP = { username: 'tajuana', id: 18, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/tajuana', }, { @@ -43,7 +43,8 @@ const RESPONSE_MAP = { username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', web_url: 'http: //localhost:3001/michaele.will', }, ], @@ -72,7 +73,8 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', web_url: 'http://localhost:3001/user0', }, { @@ -80,7 +82,8 @@ const RESPONSE_MAP = { username: 'tajuana', id: 18, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', web_url: 'http://localhost:3001/tajuana', }, { @@ -88,7 +91,8 @@ const RESPONSE_MAP = { username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', web_url: 'http://localhost:3001/michaele.will', }, ], @@ -100,7 +104,8 @@ const RESPONSE_MAP = { username: 'user0', id: 22, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon', web_url: 'http://localhost:3001/user0', }, { @@ -108,7 +113,8 @@ const RESPONSE_MAP = { username: 'tajuana', id: 18, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon', web_url: 'http://localhost:3001/tajuana', }, { @@ -116,7 +122,8 @@ const RESPONSE_MAP = { username: 'michaele.will', id: 16, state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon', web_url: 'http://localhost:3001/michaele.will', }, ], @@ -126,20 +133,21 @@ const RESPONSE_MAP = { }, '/autocomplete/projects?project_id=15': [ { - 'id': 0, - 'name_with_namespace': 'No project', - }, { - 'id': 20, - 'name_with_namespace': 'foo / bar', + id: 0, + name_with_namespace: 'No project', + }, + { + id: 20, + name_with_namespace: 'foo / bar', }, ], }, - 'PUT': { + PUT: { '/gitlab-org/gitlab-shell/issues/5.json': { data: {}, }, }, - 'POST': { + POST: { '/gitlab-org/gitlab-shell/issues/5/move': { id: 123, iid: 5, @@ -182,7 +190,8 @@ const mockData = { id: 1, name: 'Administrator', username: 'root', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', }, rootPath: '/', fullPath: '/gitlab-org/gitlab-shell', @@ -201,12 +210,14 @@ const mockData = { }, }; -mockData.sidebarMockInterceptor = function (request, next) { +mockData.sidebarMockInterceptor = function(request, next) { const body = this.responseMap[request.method.toUpperCase()][request.url]; - next(request.respondWith(JSON.stringify(body), { - status: 200, - })); + next( + request.respondWith(JSON.stringify(body), { + status: 200, + }), + ); }.bind(mockData); export default mockData; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js index 235c33fac0d..9b9c9656979 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js @@ -17,46 +17,58 @@ describe('MRWidgetHeader', () => { describe('computed', () => { describe('shouldShowCommitsBehindText', () => { it('return true when there are divergedCommitsCount', () => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', - targetBranch: 'master', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + statusPath: 'abc', + }, + }); expect(vm.shouldShowCommitsBehindText).toEqual(true); }); it('returns false where there are no divergedComits count', () => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 0, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', - targetBranch: 'master', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 0, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + statusPath: 'abc', + }, + }); expect(vm.shouldShowCommitsBehindText).toEqual(false); }); }); describe('commitsText', () => { it('returns singular when there is one commit', () => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 1, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', - targetBranch: 'master', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 1, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + statusPath: 'abc', + }, + }); expect(vm.commitsText).toEqual('1 commit behind'); }); it('returns plural when there is more than one commit', () => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 2, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', - targetBranch: 'master', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 2, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>', + targetBranch: 'master', + statusPath: 'abc', + }, + }); expect(vm.commitsText).toEqual('2 commits behind'); }); @@ -66,24 +78,27 @@ describe('MRWidgetHeader', () => { describe('template', () => { describe('common elements', () => { beforeEach(() => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', - sourceBranchRemoved: false, - targetBranchPath: 'foo/bar/commits-path', - targetBranchTreePath: 'foo/bar/tree/path', - targetBranch: 'master', - isOpen: true, - emailPatchesPath: '/mr/email-patches', - plainDiffPath: '/mr/plainDiffPath', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + statusPath: 'abc', + }, + }); }); it('renders source branch link', () => { - expect( - vm.$el.querySelector('.js-source-branch').innerHTML, - ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>'); + expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual( + '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + ); }); it('renders clipboard button', () => { @@ -101,18 +116,21 @@ describe('MRWidgetHeader', () => { }); beforeEach(() => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', - sourceBranchRemoved: false, - targetBranchPath: 'foo/bar/commits-path', - targetBranchTreePath: 'foo/bar/tree/path', - targetBranch: 'master', - isOpen: true, - emailPatchesPath: '/mr/email-patches', - plainDiffPath: '/mr/plainDiffPath', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + statusPath: 'abc', + }, + }); }); it('renders checkout branch button with modal trigger', () => { @@ -123,39 +141,49 @@ describe('MRWidgetHeader', () => { expect(button.getAttribute('data-toggle')).toEqual('modal'); }); + it('renders web ide button', () => { + const button = vm.$el.querySelector('.js-web-ide'); + + expect(button.textContent.trim()).toEqual('Web IDE'); + expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc'); + }); + it('renders download dropdown with links', () => { - expect( - vm.$el.querySelector('.js-download-email-patches').textContent.trim(), - ).toEqual('Email patches'); + expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual( + 'Email patches', + ); - expect( - vm.$el.querySelector('.js-download-email-patches').getAttribute('href'), - ).toEqual('/mr/email-patches'); + expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual( + '/mr/email-patches', + ); - expect( - vm.$el.querySelector('.js-download-plain-diff').textContent.trim(), - ).toEqual('Plain diff'); + expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual( + 'Plain diff', + ); - expect( - vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'), - ).toEqual('/mr/plainDiffPath'); + expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual( + '/mr/plainDiffPath', + ); }); }); describe('with a closed merge request', () => { beforeEach(() => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', - sourceBranchRemoved: false, - targetBranchPath: 'foo/bar/commits-path', - targetBranchTreePath: 'foo/bar/tree/path', - targetBranch: 'master', - isOpen: false, - emailPatchesPath: '/mr/email-patches', - plainDiffPath: '/mr/plainDiffPath', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: false, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + statusPath: 'abc', + }, + }); }); it('does not render checkout branch button with modal trigger', () => { @@ -165,30 +193,29 @@ describe('MRWidgetHeader', () => { }); it('does not render download dropdown with links', () => { - expect( - vm.$el.querySelector('.js-download-email-patches'), - ).toEqual(null); + expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null); - expect( - vm.$el.querySelector('.js-download-plain-diff'), - ).toEqual(null); + expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null); }); }); describe('without diverged commits', () => { beforeEach(() => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 0, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', - sourceBranchRemoved: false, - targetBranchPath: 'foo/bar/commits-path', - targetBranchTreePath: 'foo/bar/tree/path', - targetBranch: 'master', - isOpen: true, - emailPatchesPath: '/mr/email-patches', - plainDiffPath: '/mr/plainDiffPath', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 0, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + statusPath: 'abc', + }, + }); }); it('does not render diverged commits info', () => { @@ -198,22 +225,27 @@ describe('MRWidgetHeader', () => { describe('with diverged commits', () => { beforeEach(() => { - vm = mountComponent(Component, { mr: { - divergedCommitsCount: 12, - sourceBranch: 'mr-widget-refactor', - sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', - sourceBranchRemoved: false, - targetBranchPath: 'foo/bar/commits-path', - targetBranchTreePath: 'foo/bar/tree/path', - targetBranch: 'master', - isOpen: true, - emailPatchesPath: '/mr/email-patches', - plainDiffPath: '/mr/plainDiffPath', - } }); + vm = mountComponent(Component, { + mr: { + divergedCommitsCount: 12, + sourceBranch: 'mr-widget-refactor', + sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>', + sourceBranchRemoved: false, + targetBranchPath: 'foo/bar/commits-path', + targetBranchTreePath: 'foo/bar/tree/path', + targetBranch: 'master', + isOpen: true, + emailPatchesPath: '/mr/email-patches', + plainDiffPath: '/mr/plainDiffPath', + statusPath: 'abc', + }, + }); }); it('renders diverged commits info', () => { - expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)'); + expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual( + '(12 commits behind)', + ); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 3dd75307484..3fc7663b9c2 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -1,213 +1,218 @@ -/* eslint-disable */ - export default { - "id": 132, - "iid": 22, - "assignee_id": null, - "author_id": 1, - "description": "", - "lock_version": null, - "milestone_id": null, - "position": 0, - "state": "merged", - "title": "Update README.md", - "updated_by_id": null, - "created_at": "2017-04-07T12:27:26.718Z", - "updated_at": "2017-04-07T15:39:25.852Z", - "time_estimate": 0, - "total_time_spent": 0, - "human_time_estimate": null, - "human_total_time_spent": null, - "in_progress_merge_commit_sha": null, - "merge_commit_sha": "53027d060246c8f47e4a9310fb332aa52f221775", - "merge_error": null, - "merge_params": { - "force_remove_source_branch": null + id: 132, + iid: 22, + assignee_id: null, + author_id: 1, + description: '', + lock_version: null, + milestone_id: null, + position: 0, + state: 'merged', + title: 'Update README.md', + updated_by_id: null, + created_at: '2017-04-07T12:27:26.718Z', + updated_at: '2017-04-07T15:39:25.852Z', + time_estimate: 0, + total_time_spent: 0, + human_time_estimate: null, + human_total_time_spent: null, + in_progress_merge_commit_sha: null, + merge_commit_sha: '53027d060246c8f47e4a9310fb332aa52f221775', + merge_error: null, + merge_params: { + force_remove_source_branch: null, }, - "merge_status": "can_be_merged", - "merge_user_id": null, - "merge_when_pipeline_succeeds": false, - "source_branch": "daaaa", - "source_branch_link": "daaaa", - "source_project_id": 19, - "target_branch": "master", - "target_project_id": 19, - "metrics": { - "merged_by": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/root" + merge_status: 'can_be_merged', + merge_user_id: null, + merge_when_pipeline_succeeds: false, + source_branch: 'daaaa', + source_branch_link: 'daaaa', + source_project_id: 19, + target_branch: 'master', + target_project_id: 19, + metrics: { + merged_by: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', }, - "merged_at": "2017-04-07T15:39:25.696Z", - "closed_by": null, - "closed_at": null + merged_at: '2017-04-07T15:39:25.696Z', + closed_by: null, + closed_at: null, }, - "author": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/root" + author: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', }, - "merge_user": null, - "diff_head_sha": "104096c51715e12e7ae41f9333e9fa35b73f385d", - "diff_head_commit_short_id": "104096c5", - "merge_commit_message": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", - "pipeline": { - "id": 172, - "user": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/root" + merge_user: null, + diff_head_sha: '104096c51715e12e7ae41f9333e9fa35b73f385d', + diff_head_commit_short_id: '104096c5', + merge_commit_message: + "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", + pipeline: { + id: 172, + user: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', }, - "active": false, - "coverage": "92.16", - "path": "/root/acets-app/pipelines/172", - "details": { - "status": { - "icon": "icon_status_success", - "favicon": "favicon_status_success", - "text": "passed", - "label": "passed", - "group": "success", - "has_details": true, - "details_path": "/root/acets-app/pipelines/172" + active: false, + coverage: '92.16', + path: '/root/acets-app/pipelines/172', + details: { + status: { + icon: 'icon_status_success', + favicon: 'favicon_status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/acets-app/pipelines/172', }, - "duration": null, - "finished_at": "2017-04-07T14:00:14.256Z", - "stages": [ + duration: null, + finished_at: '2017-04-07T14:00:14.256Z', + stages: [ { - "name": "build", - "title": "build: failed", - "status": { - "icon": "icon_status_failed", - "favicon": "favicon_status_failed", - "text": "failed", - "label": "failed", - "group": "failed", - "has_details": true, - "details_path": "/root/acets-app/pipelines/172#build" + name: 'build', + title: 'build: failed', + status: { + icon: 'icon_status_failed', + favicon: 'favicon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/acets-app/pipelines/172#build', }, - "path": "/root/acets-app/pipelines/172#build", - "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=build" + path: '/root/acets-app/pipelines/172#build', + dropdown_path: '/root/acets-app/pipelines/172/stage.json?stage=build', }, { - "name": "review", - "title": "review: skipped", - "status": { - "icon": "icon_status_skipped", - "favicon": "favicon_status_skipped", - "text": "skipped", - "label": "skipped", - "group": "skipped", - "has_details": true, - "details_path": "/root/acets-app/pipelines/172#review" + name: 'review', + title: 'review: skipped', + status: { + icon: 'icon_status_skipped', + favicon: 'favicon_status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + has_details: true, + details_path: '/root/acets-app/pipelines/172#review', }, - "path": "/root/acets-app/pipelines/172#review", - "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=review" - } - ], - "artifacts": [ - + path: '/root/acets-app/pipelines/172#review', + dropdown_path: '/root/acets-app/pipelines/172/stage.json?stage=review', + }, ], - "manual_actions": [ + artifacts: [], + manual_actions: [ { - "name": "stop_review", - "path": "/root/acets-app/builds/1427/play", - "playable": false - } - ] + name: 'stop_review', + path: '/root/acets-app/builds/1427/play', + playable: false, + }, + ], }, - "flags": { - "latest": false, - "triggered": false, - "stuck": false, - "yaml_errors": false, - "retryable": true, - "cancelable": false + flags: { + latest: false, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: false, }, - "ref": { - "name": "daaaa", - "path": "/root/acets-app/tree/daaaa", - "tag": false, - "branch": true + ref: { + name: 'daaaa', + path: '/root/acets-app/tree/daaaa', + tag: false, + branch: true, }, - "commit": { - "id": "104096c51715e12e7ae41f9333e9fa35b73f385d", - "short_id": "104096c5", - "title": "Update README.md", - "created_at": "2017-04-07T15:27:18.000+03:00", - "parent_ids": [ - "2396536178668d8930c29d904e53bd4d06228b32" - ], - "message": "Update README.md", - "author_name": "Administrator", - "author_email": "admin@example.com", - "authored_date": "2017-04-07T15:27:18.000+03:00", - "committer_name": "Administrator", - "committer_email": "admin@example.com", - "committed_date": "2017-04-07T15:27:18.000+03:00", - "author": { - "name": "Administrator", - "username": "root", - "id": 1, - "state": "active", - "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "web_url": "http://localhost:3000/root" + commit: { + id: '104096c51715e12e7ae41f9333e9fa35b73f385d', + short_id: '104096c5', + title: 'Update README.md', + created_at: '2017-04-07T15:27:18.000+03:00', + parent_ids: ['2396536178668d8930c29d904e53bd4d06228b32'], + message: 'Update README.md', + author_name: 'Administrator', + author_email: 'admin@example.com', + authored_date: '2017-04-07T15:27:18.000+03:00', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + committed_date: '2017-04-07T15:27:18.000+03:00', + author: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', }, - "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", - "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d", - "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d" + author_gravatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commit_url: + 'http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d', + commit_path: '/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d', }, - "retry_path": "/root/acets-app/pipelines/172/retry", - "created_at": "2017-04-07T12:27:19.520Z", - "updated_at": "2017-04-07T15:28:44.800Z" + retry_path: '/root/acets-app/pipelines/172/retry', + created_at: '2017-04-07T12:27:19.520Z', + updated_at: '2017-04-07T15:28:44.800Z', }, - "work_in_progress": false, - "source_branch_exists": false, - "mergeable_discussions_state": true, - "conflicts_can_be_resolved_in_ui": false, - "branch_missing": true, - "commits_count": 1, - "has_conflicts": false, - "can_be_merged": true, - "has_ci": true, - "ci_status": "success", - "pipeline_status_path": "/root/acets-app/merge_requests/22/pipeline_status", - "issues_links": { - "closing": "", - "mentioned_but_not_closing": "" + work_in_progress: false, + source_branch_exists: false, + mergeable_discussions_state: true, + conflicts_can_be_resolved_in_ui: false, + branch_missing: true, + commits_count: 1, + has_conflicts: false, + can_be_merged: true, + has_ci: true, + ci_status: 'success', + pipeline_status_path: '/root/acets-app/merge_requests/22/pipeline_status', + issues_links: { + closing: '', + mentioned_but_not_closing: '', }, - "current_user": { - "can_resolve_conflicts": true, - "can_remove_source_branch": false, - "can_revert_on_current_merge_request": true, - "can_cherry_pick_on_current_merge_request": true + current_user: { + can_resolve_conflicts: true, + can_remove_source_branch: false, + can_revert_on_current_merge_request: true, + can_cherry_pick_on_current_merge_request: true, }, - "target_branch_path": "/root/acets-app/branches/master", - "source_branch_path": "/root/acets-app/branches/daaaa", - "conflict_resolution_ui_path": "/root/acets-app/merge_requests/22/conflicts", - "remove_wip_path": "/root/acets-app/merge_requests/22/remove_wip", - "cancel_merge_when_pipeline_succeeds_path": "/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds", - "create_issue_to_resolve_discussions_path": "/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22", - "merge_path": "/root/acets-app/merge_requests/22/merge", - "cherry_pick_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1", - "revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1", - "email_patches_path": "/root/acets-app/merge_requests/22.patch", - "plain_diff_path": "/root/acets-app/merge_requests/22.diff", - "status_path": "/root/acets-app/merge_requests/22.json", - "merge_check_path": "/root/acets-app/merge_requests/22/merge_check", - "ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status", - "project_archived": false, - "merge_commit_message_with_description": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", - "diverged_commits_count": 0, - "only_allow_merge_if_pipeline_succeeds": false, - "commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content" -} + target_branch_path: '/root/acets-app/branches/master', + source_branch_path: '/root/acets-app/branches/daaaa', + conflict_resolution_ui_path: '/root/acets-app/merge_requests/22/conflicts', + remove_wip_path: '/root/acets-app/merge_requests/22/remove_wip', + cancel_merge_when_pipeline_succeeds_path: + '/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds', + create_issue_to_resolve_discussions_path: + '/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22', + merge_path: '/root/acets-app/merge_requests/22/merge', + cherry_pick_in_fork_path: + '/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1', + revert_in_fork_path: + '/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1', + email_patches_path: '/root/acets-app/merge_requests/22.patch', + plain_diff_path: '/root/acets-app/merge_requests/22.diff', + status_path: '/root/acets-app/merge_requests/22.json', + merge_check_path: '/root/acets-app/merge_requests/22/merge_check', + ci_environments_status_url: '/root/acets-app/merge_requests/22/ci_environments_status', + project_archived: false, + merge_commit_message_with_description: + "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22", + diverged_commits_count: 0, + only_allow_merge_if_pipeline_succeeds: false, + commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content', +}; diff --git a/spec/javascripts/vue_shared/components/mock_data.js b/spec/javascripts/vue_shared/components/mock_data.js index 0d781bdca74..15b56c58c33 100644 --- a/spec/javascripts/vue_shared/components/mock_data.js +++ b/spec/javascripts/vue_shared/components/mock_data.js @@ -1,5 +1,3 @@ -/* eslint-disable */ - export const mockMetrics = [ [1493716685, '4.30859375'], [1493716745, '4.30859375'], diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb new file mode 100644 index 00000000000..14d055cbcc1 --- /dev/null +++ b/spec/lib/backup/files_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Backup::Files do + let(:progress) { StringIO.new } + let!(:project) { create(:project) } + + before do + allow(progress).to receive(:puts) + allow(progress).to receive(:print) + allow(FileUtils).to receive(:mkdir_p).and_return(true) + allow(FileUtils).to receive(:mv).and_return(true) + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:realpath).with("/var/gitlab-registry").and_return("/var/gitlab-registry") + allow(File).to receive(:realpath).with("/var/gitlab-registry/..").and_return("/var") + + allow_any_instance_of(String).to receive(:color) do |string, _color| + string + end + + allow_any_instance_of(described_class).to receive(:progress).and_return(progress) + end + + describe '#restore' do + subject { described_class.new('registry', '/var/gitlab-registry') } + let(:timestamp) { Time.utc(2017, 3, 22) } + + around do |example| + Timecop.freeze(timestamp) { example.run } + end + + describe 'folders with permission' do + before do + allow(subject).to receive(:run_pipeline!).and_return(true) + allow(subject).to receive(:backup_existing_files).and_return(true) + allow(Dir).to receive(:glob).with("/var/gitlab-registry/*", File::FNM_DOTMATCH).and_return(["/var/gitlab-registry/.", "/var/gitlab-registry/..", "/var/gitlab-registry/sample1"]) + end + + it 'moves all necessary files' do + allow(subject).to receive(:backup_existing_files).and_call_original + expect(FileUtils).to receive(:mv).with(["/var/gitlab-registry/sample1"], File.join(Gitlab.config.backup.path, "tmp", "registry.#{Time.now.to_i}")) + subject.restore + end + + it 'raises no errors' do + expect { subject.restore }.not_to raise_error + end + + it 'calls tar command with unlink' do + expect(subject).to receive(:run_pipeline!).with([%w(gzip -cd), %w(tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -)], any_args) + subject.restore + end + end + + describe 'folders without permissions' do + before do + allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES) + allow(subject).to receive(:run_pipeline!).and_return(true) + end + + it 'shows error message' do + expect(subject).to receive(:access_denied_error).with("/var/gitlab-registry") + subject.restore + end + end + end +end diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb index 03573c304aa..e4c1c9bafc0 100644 --- a/spec/lib/backup/repository_spec.rb +++ b/spec/lib/backup/repository_spec.rb @@ -7,6 +7,8 @@ describe Backup::Repository do before do allow(progress).to receive(:puts) allow(progress).to receive(:print) + allow(FileUtils).to receive(:mkdir_p).and_return(true) + allow(FileUtils).to receive(:mv).and_return(true) allow_any_instance_of(String).to receive(:color) do |string, _color| string @@ -68,6 +70,17 @@ describe Backup::Repository do end end end + + describe 'folders without permissions' do + before do + allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES) + end + + it 'shows error message' do + expect(subject).to receive(:access_denied_error) + subject.restore + end + end end describe '#empty_repo?' do diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb index cbb0089bde7..a50329473ad 100644 --- a/spec/lib/banzai/filter/autolink_filter_spec.rb +++ b/spec/lib/banzai/filter/autolink_filter_spec.rb @@ -167,6 +167,15 @@ describe Banzai::Filter::AutolinkFilter do expect(actual).to eq(expected_complicated_link) end + it 'does not double-encode HTML entities' do + encoded_link = "#{link}?foo=bar&baz=quux" + expected_encoded_link = %Q{<a href="#{encoded_link}">#{encoded_link}</a>} + actual = unescape(filter(encoded_link).to_html) + + expect(actual).to eq(Rinku.auto_link(encoded_link)) + expect(actual).to eq(expected_encoded_link) + end + it 'does not include trailing HTML entities' do doc = filter("See <<<#{link}>>>") diff --git a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb index e112e9e9e3d..5ce84c61042 100644 --- a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb @@ -51,4 +51,20 @@ describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 201 expect { described_class.new.perform(1, 6) } .to raise_error ActiveRecord::RecordNotUnique end + + context 'when invalid class can be loaded due to single table inheritance' do + let(:commit_status) do + jobs.create!(id: 7, commit_id: 1, project_id: 123, stage_idx: 4, + stage: 'post-deploy', status: :failed) + end + + before do + commit_status.update_column(:type, 'SomeClass') + end + + it 'does ignore single table inheritance type' do + expect { described_class.new.perform(1, 7) }.not_to raise_error + expect(jobs.find(7)).to have_attributes(stage_id: (a_value > 0)) + end + end end diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb new file mode 100644 index 00000000000..2ce858836e3 --- /dev/null +++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Policy::Variables do + set(:project) { create(:project) } + + let(:pipeline) do + build(:ci_empty_pipeline, project: project, ref: 'master', source: :push) + end + + let(:ci_build) do + build(:ci_build, pipeline: pipeline, project: project, ref: 'master') + end + + let(:seed) { double('build seed', to_resource: ci_build) } + + before do + pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '') + end + + describe '#satisfied_by?' do + it 'is satisfied by at least one matching statement' do + policy = described_class.new(['$CI_PROJECT_ID', '$UNDEFINED']) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is not satisfied by an overriden empty variable' do + policy = described_class.new(['$CI_PROJECT_NAME']) + + expect(policy).not_to be_satisfied_by(pipeline, seed) + end + + it 'is satisfied by a truthy pipeline expression' do + policy = described_class.new([%($CI_PIPELINE_SOURCE == "push")]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is not satisfied by a falsy pipeline expression' do + policy = described_class.new([%($CI_PIPELINE_SOURCE == "invalid source")]) + + expect(policy).not_to be_satisfied_by(pipeline, seed) + end + + it 'is satisfied by a truthy expression using undefined variable' do + policy = described_class.new(['$UNDEFINED == null']) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'is not satisfied by a falsy expression using undefined variable' do + policy = described_class.new(['$UNDEFINED']) + + expect(policy).not_to be_satisfied_by(pipeline, seed) + end + + it 'allows to evaluate regular secret variables' do + create(:ci_variable, project: project, key: 'SECRET', value: 'my secret') + + policy = described_class.new(["$SECRET == 'my secret'"]) + + expect(policy).to be_satisfied_by(pipeline, seed) + end + + it 'does not persist neither pipeline nor build' do + described_class.new('$VAR').satisfied_by?(pipeline, seed) + + expect(pipeline).not_to be_persisted + expect(seed.to_resource).not_to be_persisted + end + end +end diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb index 5a21282712a..cce4efaa069 100644 --- a/spec/lib/gitlab/ci/build/step_spec.rb +++ b/spec/lib/gitlab/ci/build/step_spec.rb @@ -5,10 +5,14 @@ describe Gitlab::Ci::Build::Step do shared_examples 'has correct script' do subject { described_class.from_commands(job) } + before do + job.run! + end + it 'fabricates an object' do expect(subject.name).to eq(:script) expect(subject.script).to eq(script) - expect(subject.timeout).to eq(job.timeout) + expect(subject.timeout).to eq(job.metadata_timeout) expect(subject.when).to eq('on_success') expect(subject.allow_failure).to be_falsey end @@ -47,6 +51,10 @@ describe Gitlab::Ci::Build::Step do subject { described_class.from_after_script(job) } + before do + job.run! + end + context 'when after_script is empty' do it 'doesn not fabricate an object' do is_expected.to be_nil @@ -59,7 +67,7 @@ describe Gitlab::Ci::Build::Step do it 'fabricates an object' do expect(subject.name).to eq(:after_script) expect(subject.script).to eq(['ls -la', 'date']) - expect(subject.timeout).to eq(job.timeout) + expect(subject.timeout).to eq(job.metadata_timeout) expect(subject.when).to eq('always') expect(subject.allow_failure).to be_truthy end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 5e83abf645b..08718c382b9 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -83,6 +83,39 @@ describe Gitlab::Ci::Config::Entry::Policy do end end + context 'when specifying valid variables expressions policy' do + let(:config) { { variables: ['$VAR == null'] } } + + it 'is a correct configuraton' do + expect(entry).to be_valid + expect(entry.value).to eq(config) + end + end + + context 'when specifying variables expressions in invalid format' do + let(:config) { { variables: '$MY_VAR' } } + + it 'reports an error about invalid format' do + expect(entry.errors).to include /should be an array of strings/ + end + end + + context 'when specifying invalid variables expressions statement' do + let(:config) { { variables: ['$MY_VAR =='] } } + + it 'reports an error about invalid statement' do + expect(entry.errors).to include /invalid expression syntax/ + end + end + + context 'when specifying invalid variables expressions token' do + let(:config) { { variables: ['$MY_VAR == 123'] } } + + it 'reports an error about invalid statement' do + expect(entry.errors).to include /invalid expression syntax/ + end + end + context 'when specifying unknown policy' do let(:config) { { refs: ['master'], invalid: :something } } diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 2258ae83f38..8312fa47cfa 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -6,7 +6,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do let(:pipeline) do build(:ci_pipeline_with_one_job, project: project, - ref: 'master') + ref: 'master', + user: user) end let(:command) do @@ -42,6 +43,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do expect(pipeline.stages.first.builds).to be_one expect(pipeline.stages.first.builds.first).not_to be_persisted end + + it 'correctly assigns user' do + expect(pipeline.builds).to all(have_attributes(user: user)) + end end context 'when pipeline is empty' do diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb index 86234dfb9e5..1ccb792d1da 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb @@ -73,6 +73,22 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do expect(token).not_to be_nil expect(token.build.evaluate).to eq 'some " string' end + + it 'allows to use an empty string inside single quotes' do + scanner = StringScanner.new(%('')) + + token = described_class.scan(scanner) + + expect(token.build.evaluate).to eq '' + end + + it 'allow to use an empty string inside double quotes' do + scanner = StringScanner.new(%("")) + + token = described_class.scan(scanner) + + expect(token.build.evaluate).to eq '' + end end end diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index 472a58599d8..6685bf5385b 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -1,14 +1,23 @@ require 'spec_helper' describe Gitlab::Ci::Pipeline::Expression::Statement do - let(:pipeline) { build(:ci_pipeline) } - subject do - described_class.new(text, pipeline) + described_class.new(text, variables) + end + + let(:variables) do + { 'PRESENT_VARIABLE' => 'my variable', + EMPTY_VARIABLE: '' } end - before do - pipeline.variables.build([key: 'VARIABLE', value: 'my variable']) + describe '.new' do + context 'when variables are not provided' do + it 'allows to properly initializes the statement' do + statement = described_class.new('$PRESENT_VARIABLE') + + expect(statement.evaluate).to be_nil + end + end end describe '#parse_tree' do @@ -23,18 +32,26 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do context 'when expression grammar is incorrect' do table = [ - '$VAR "text"', # missing operator - '== "123"', # invalid right side - "'single quotes'", # single quotes string - '$VAR ==', # invalid right side - '12345', # unknown syntax - '' # empty statement + '$VAR "text"', # missing operator + '== "123"', # invalid left side + '"some string"', # only string provided + '$VAR ==', # invalid right side + '12345', # unknown syntax + '' # empty statement ] table.each do |syntax| - it "raises an error when syntax is `#{syntax}`" do - expect { described_class.new(syntax, pipeline).parse_tree } - .to raise_error described_class::StatementError + context "when expression grammar is #{syntax.inspect}" do + let(:text) { syntax } + + it 'aises a statement error exception' do + expect { subject.parse_tree } + .to raise_error described_class::StatementError + end + + it 'is an invalid statement' do + expect(subject).not_to be_valid + end end end end @@ -47,10 +64,14 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do expect(subject.parse_tree) .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals end + + it 'is a valid statement' do + expect(subject).to be_valid + end end context 'when using a single token' do - let(:text) { '$VARIABLE' } + let(:text) { '$PRESENT_VARIABLE' } it 'returns a single token instance' do expect(subject.parse_tree) @@ -62,14 +83,17 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do describe '#evaluate' do statements = [ - ['$VARIABLE == "my variable"', true], - ["$VARIABLE == 'my variable'", true], - ['"my variable" == $VARIABLE', true], - ['$VARIABLE == null', false], - ['$VAR == null', true], - ['null == $VAR', true], - ['$VARIABLE', 'my variable'], - ['$VAR', nil] + ['$PRESENT_VARIABLE == "my variable"', true], + ["$PRESENT_VARIABLE == 'my variable'", true], + ['"my variable" == $PRESENT_VARIABLE', true], + ['$PRESENT_VARIABLE == null', false], + ['$EMPTY_VARIABLE == null', false], + ['"" == $EMPTY_VARIABLE', true], + ['$EMPTY_VARIABLE', ''], + ['$UNDEFINED_VARIABLE == null', true], + ['null == $UNDEFINED_VARIABLE', true], + ['$PRESENT_VARIABLE', 'my variable'], + ['$UNDEFINED_VARIABLE', nil] ] statements.each do |expression, value| @@ -82,4 +106,25 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do end end end + + describe '#truthful?' do + statements = [ + ['$PRESENT_VARIABLE == "my variable"', true], + ["$PRESENT_VARIABLE == 'no match'", false], + ['$UNDEFINED_VARIABLE == null', true], + ['$PRESENT_VARIABLE', true], + ['$UNDEFINED_VARIABLE', false], + ['$EMPTY_VARIABLE', false] + ] + + statements.each do |expression, value| + context "when using expression `#{expression}`" do + let(:text) { expression } + + it "returns `#{value.inspect}`" do + expect(subject.truthful?).to eq value + end + end + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 116573379e0..fffa727c2ed 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -21,16 +21,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build do end end - describe '#user=' do - let(:user) { build(:user) } - - it 'assignes user to a build' do - subject.user = user - - expect(subject.attributes).to include(user: user) - end - end - describe '#to_resource' do it 'returns a valid build resource' do expect(subject.to_resource).to be_a(::Ci::Build) diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb index 8f0bf40d624..eb1b285c7bd 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb @@ -95,16 +95,6 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do end end - describe '#user=' do - let(:user) { build(:user) } - - it 'assignes relevant pipeline attributes' do - subject.user = user - - expect(subject.seeds.map(&:attributes)).to all(include(user: user)) - end - end - describe '#to_resource' do it 'builds a valid stage object with all builds' do subject.to_resource.save! diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb index cc1257484d2..bf9208f1ff4 100644 --- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb @@ -46,9 +46,13 @@ describe Gitlab::Ci::Variables::Collection::Item do end end - describe '#to_hash' do - it 'returns a hash representation of a collection item' do - expect(described_class.new(**variable).to_hash).to eq variable + describe '#to_runner_variable' do + it 'returns a runner-compatible hash representation' do + runner_variable = described_class + .new(**variable) + .to_runner_variable + + expect(runner_variable).to eq variable end end end diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index 90b6e178242..cb2f7718c9c 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::Ci::Variables::Collection do collection = described_class.new([variable]) - expect(collection.first.to_hash).to eq variable + expect(collection.first.to_runner_variable).to eq variable end it 'can be initialized without an argument' do @@ -96,4 +96,19 @@ describe Gitlab::Ci::Variables::Collection do .to eq [{ key: 'TEST', value: 1, public: true }] end end + + describe '#to_hash' do + it 'returns regular hash in valid order without duplicates' do + collection = described_class.new + .append(key: 'TEST1', value: 'test-1') + .append(key: 'TEST2', value: 'test-2') + .append(key: 'TEST1', value: 'test-3') + + expect(collection.to_hash).to eq('TEST1' => 'test-3', + 'TEST2' => 'test-2') + + expect(collection.to_hash).to include(TEST1: 'test-3') + expect(collection.to_hash).not_to include(TEST1: 'test-1') + end + end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index fbc2af29b98..ecb16daec96 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1311,6 +1311,14 @@ module Gitlab Gitlab::Ci::YamlProcessor.new(config) end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") end + + it 'returns errors if pipeline variables expression is invalid' do + config = YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } }) + + expect { Gitlab::Ci::YamlProcessor.new(config) } + .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only variables invalid expression syntax') + end end describe "Validate configuration templates" do diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb index 143aa2218c9..6fd2b33486b 100644 --- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb +++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::Git::GitmodulesParser do it 'should parse a .gitmodules file correctly' do - parser = described_class.new(<<-'GITMODULES'.strip_heredoc) + data = <<~GITMODULES [submodule "vendor/libgit2"] path = vendor/libgit2 [submodule "vendor/libgit2"] @@ -16,6 +16,7 @@ describe Gitlab::Git::GitmodulesParser do url = https://example.com/another/project GITMODULES + parser = described_class.new(data.gsub("\n", "\r\n")) modules = parser.parse expect(modules).to eq({ diff --git a/spec/lib/gitlab/git/env_spec.rb b/spec/lib/gitlab/git/hook_env_spec.rb index 03836d49518..e6aa5ad8c90 100644 --- a/spec/lib/gitlab/git/env_spec.rb +++ b/spec/lib/gitlab/git/hook_env_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' -describe Gitlab::Git::Env do +describe Gitlab::Git::HookEnv do + let(:gl_repository) { 'project-123' } + describe ".set" do context 'with RequestStore.store disabled' do before do @@ -8,9 +10,9 @@ describe Gitlab::Git::Env do end it 'does not store anything' do - described_class.set(GIT_OBJECT_DIRECTORY: 'foo') + described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo') - expect(described_class.all).to be_empty + expect(described_class.all(gl_repository)).to be_empty end end @@ -21,15 +23,19 @@ describe Gitlab::Git::Env do it 'whitelist some `GIT_*` variables and stores them using RequestStore' do described_class.set( - GIT_OBJECT_DIRECTORY: 'foo', - GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar', + gl_repository, + GIT_OBJECT_DIRECTORY_RELATIVE: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: 'bar', GIT_EXEC_PATH: 'baz', PATH: '~/.bin:/bin') - expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo') - expect(described_class[:GIT_ALTERNATE_OBJECT_DIRECTORIES]).to eq('bar') - expect(described_class[:GIT_EXEC_PATH]).to be_nil - expect(described_class[:bar]).to be_nil + git_env = described_class.all(gl_repository) + + expect(git_env[:GIT_OBJECT_DIRECTORY_RELATIVE]).to eq('foo') + expect(git_env[:GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE]).to eq('bar') + expect(git_env[:GIT_EXEC_PATH]).to be_nil + expect(git_env[:PATH]).to be_nil + expect(git_env[:bar]).to be_nil end end end @@ -39,14 +45,15 @@ describe Gitlab::Git::Env do before do allow(RequestStore).to receive(:active?).and_return(true) described_class.set( - GIT_OBJECT_DIRECTORY: 'foo', - GIT_ALTERNATE_OBJECT_DIRECTORIES: ['bar']) + gl_repository, + GIT_OBJECT_DIRECTORY_RELATIVE: 'foo', + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: ['bar']) end it 'returns an env hash' do - expect(described_class.all).to eq({ - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => ['bar'] + expect(described_class.all(gl_repository)).to eq({ + 'GIT_OBJECT_DIRECTORY_RELATIVE' => 'foo', + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['bar'] }) end end @@ -56,8 +63,8 @@ describe Gitlab::Git::Env do context 'with RequestStore.store enabled' do using RSpec::Parameterized::TableSyntax - let(:key) { 'GIT_OBJECT_DIRECTORY' } - subject { described_class.to_env_hash } + let(:key) { 'GIT_OBJECT_DIRECTORY_RELATIVE' } + subject { described_class.to_env_hash(gl_repository) } where(:input, :output) do nil | nil @@ -70,7 +77,7 @@ describe Gitlab::Git::Env do with_them do before do allow(RequestStore).to receive(:active?).and_return(true) - described_class.set(key.to_sym => input) + described_class.set(gl_repository, key.to_sym => input) end it 'puts the right value in the hash' do @@ -84,47 +91,25 @@ describe Gitlab::Git::Env do end end - describe ".[]" do - context 'with RequestStore.store enabled' do - before do - allow(RequestStore).to receive(:active?).and_return(true) - end - - before do - described_class.set( - GIT_OBJECT_DIRECTORY: 'foo', - GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar') - end - - it 'returns a stored value for an existing key' do - expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo') - end - - it 'returns nil for an non-existing key' do - expect(described_class[:foo]).to be_nil - end - end - end - describe 'thread-safety' do context 'with RequestStore.store enabled' do before do allow(RequestStore).to receive(:active?).and_return(true) - described_class.set(GIT_OBJECT_DIRECTORY: 'foo') + described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo') end it 'is thread-safe' do another_thread = Thread.new do - described_class.set(GIT_OBJECT_DIRECTORY: 'bar') + described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'bar') Thread.stop - described_class[:GIT_OBJECT_DIRECTORY] + described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE] end # Ensure another_thread runs first sleep 0.1 until another_thread.stop? - expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo') + expect(described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE]).to eq('foo') another_thread.run expect(another_thread.value).to eq('bar') diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 0e315b3f49e..5cbe2808d0b 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -120,7 +120,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe 'alternates keyword argument' do context 'with no Git env stored' do before do - allow(Gitlab::Git::Env).to receive(:all).and_return({}) + allow(Gitlab::Git::HookEnv).to receive(:all).and_return({}) end it "is passed an empty array" do @@ -132,7 +132,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with absolute and relative Git object dir envvars stored' do before do - allow(Gitlab::Git::Env).to receive(:all).and_return({ + allow(Gitlab::Git::HookEnv).to receive(:all).and_return({ 'GIT_OBJECT_DIRECTORY_RELATIVE' => './objects/foo', 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['./objects/bar', './objects/baz'], 'GIT_OBJECT_DIRECTORY' => 'ignored', @@ -148,22 +148,6 @@ describe Gitlab::Git::Repository, seed_helper: true do repository.rugged end end - - context 'with only absolute Git object dir envvars stored' do - before do - allow(Gitlab::Git::Env).to receive(:all).and_return({ - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[bar baz], - 'GIT_OTHER' => 'another_env' - }) - end - - it "is passed the absolute object dir envvars as is" do - expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar baz]) - - repository.rugged - end - end end end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index 4e0ee206219..32ec1e029c8 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -3,17 +3,6 @@ require 'spec_helper' describe Gitlab::Git::RevList do let(:repository) { create(:project, :repository).repository.raw } let(:rev_list) { described_class.new(repository, newrev: 'newrev') } - let(:env_hash) do - { - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' - } - end - let(:command_env) { { 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'foo:bar' } } - - before do - allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash) - end def args_for_popen(args_list) [Gitlab.config.git.bin_path, 'rev-list', *args_list] @@ -23,7 +12,7 @@ describe Gitlab::Git::RevList do params = [ args_for_popen(additional_args), repository.path, - command_env, + {}, hash_including(lazy_block: with_lazy_block ? anything : nil) ] diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb index 872377c93d8..f03c7e3f04b 100644 --- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb @@ -58,4 +58,14 @@ describe Gitlab::GitalyClient::RemoteService do client.update_remote_mirror(ref_name, only_branches_matching) end end + + describe '.exists?' do + context "when the remote doesn't exist" do + let(:url) { 'https://gitlab.com/gitlab-org/ik-besta-niet-of-ik-word-geplaagd.git' } + + it 'returns false' do + expect(described_class.exists?(url)).to be(false) + end + end + end end diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb index d1e0136f8c1..550db6db6d9 100644 --- a/spec/lib/gitlab/gitaly_client/util_spec.rb +++ b/spec/lib/gitlab/gitaly_client/util_spec.rb @@ -7,16 +7,19 @@ describe Gitlab::GitalyClient::Util do let(:gl_repository) { 'project-1' } let(:git_object_directory) { '.git/objects' } let(:git_alternate_object_directory) { ['/dir/one', '/dir/two'] } + let(:git_env) do + { + 'GIT_OBJECT_DIRECTORY_RELATIVE' => git_object_directory, + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => git_alternate_object_directory + } + end subject do described_class.repository(repository_storage, relative_path, gl_repository) end it 'creates a Gitaly::Repository with the given data' do - allow(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY_RELATIVE') - .and_return(git_object_directory) - allow(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE') - .and_return(git_alternate_object_directory) + allow(Gitlab::Git::HookEnv).to receive(:all).with(gl_repository).and_return(git_env) expect(subject).to be_a(Gitaly::Repository) expect(subject.storage_name).to eq(repository_storage) diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb index 5bedfc79dd3..1f0f1fdd7da 100644 --- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb @@ -38,8 +38,12 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do expect(project) .to receive(:wiki_repository_exists?) .and_return(false) + expect(Gitlab::GitalyClient::RemoteService) + .to receive(:exists?) + .with("foo.wiki.git") + .and_return(true) - expect(importer.import_wiki?).to eq(true) + expect(importer.import_wiki?).to be(true) end it 'returns false if the GitHub wiki is disabled' do diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb index b0bc081a3c8..d0dadfa78da 100644 --- a/spec/lib/gitlab/http_spec.rb +++ b/spec/lib/gitlab/http_spec.rb @@ -12,11 +12,11 @@ describe Gitlab::HTTP do end it 'deny requests to localhost' do - expect { described_class.get('http://localhost:3003') }.to raise_error(URI::InvalidURIError) + expect { described_class.get('http://localhost:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError) end it 'deny requests to private network' do - expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(URI::InvalidURIError) + expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError) end context 'if allow_local_requests set to true' do @@ -41,7 +41,7 @@ describe Gitlab::HTTP do context 'if allow_local_requests set to false' do it 'override the global value and ban requests to localhost or private network' do - expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(URI::InvalidURIError) + expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(Gitlab::HTTP::BlockedUrlError) end end end diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb new file mode 100644 index 00000000000..ed54d87de4a --- /dev/null +++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do + let!(:service) { described_class.new } + let!(:project) { create(:project, :with_export) } + let(:shared) { project.import_export_shared } + let!(:user) { create(:user) } + + describe '#execute' do + before do + allow(service).to receive(:strategy_execute) + end + + it 'returns if project exported file is not found' do + allow(project).to receive(:export_project_path).and_return(nil) + + expect(service).not_to receive(:strategy_execute) + + service.execute(user, project) + end + + it 'creates a lock file in the export dir' do + allow(service).to receive(:delete_after_export_lock) + + service.execute(user, project) + + expect(lock_path_exist?).to be_truthy + end + + context 'when the method succeeds' do + it 'removes the lock file' do + service.execute(user, project) + + expect(lock_path_exist?).to be_falsey + end + end + + context 'when the method fails' do + before do + allow(service).to receive(:strategy_execute).and_call_original + end + + context 'when validation fails' do + before do + allow(service).to receive(:invalid?).and_return(true) + end + + it 'does not create the lock file' do + expect(service).not_to receive(:create_or_update_after_export_lock) + + service.execute(user, project) + end + + it 'does not execute main logic' do + expect(service).not_to receive(:strategy_execute) + + service.execute(user, project) + end + + it 'logs validation errors in shared context' do + expect(service).to receive(:log_validation_errors) + + service.execute(user, project) + end + end + + context 'when an exception is raised' do + it 'removes the lock' do + expect { service.execute(user, project) }.to raise_error(NotImplementedError) + + expect(lock_path_exist?).to be_falsey + end + end + end + end + + describe '#log_validation_errors' do + it 'add the message to the shared context' do + errors = %w(test_message test_message2) + + allow(service).to receive(:invalid?).and_return(true) + allow(service.errors).to receive(:full_messages).and_return(errors) + + expect(shared).to receive(:add_error_message).twice.and_call_original + + service.execute(user, project) + + expect(shared.errors).to eq errors + end + end + + describe '#to_json' do + it 'adds the current strategy class to the serialized attributes' do + params = { param1: 1 } + result = params.merge(klass: described_class.to_s).to_json + + expect(described_class.new(params).to_json).to eq result + end + end + + def lock_path_exist? + File.exist?(described_class.lock_file_path(project)) + end +end diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb new file mode 100644 index 00000000000..5fe57d9987b --- /dev/null +++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do + let(:example_url) { 'http://www.example.com' } + let(:strategy) { subject.new(url: example_url, http_method: 'post') } + let!(:project) { create(:project, :with_export) } + let!(:user) { build(:user) } + + subject { described_class } + + describe 'validations' do + it 'only POST and PUT method allowed' do + %w(POST post PUT put).each do |method| + expect(subject.new(url: example_url, http_method: method)).to be_valid + end + + expect(subject.new(url: example_url, http_method: 'whatever')).not_to be_valid + end + + it 'onyl allow urls as upload urls' do + expect(subject.new(url: example_url)).to be_valid + expect(subject.new(url: 'whatever')).not_to be_valid + end + end + + describe '#execute' do + it 'removes the exported project file after the upload' do + allow(strategy).to receive(:send_file) + allow(strategy).to receive(:handle_response_error) + + expect(project).to receive(:remove_exported_project_file) + + strategy.execute(user, project) + end + end +end diff --git a/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb b/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb new file mode 100644 index 00000000000..bf727285a9f --- /dev/null +++ b/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::AfterExportStrategyBuilder do + let!(:strategies_namespace) { 'Gitlab::ImportExport::AfterExportStrategies' } + + describe '.build!' do + context 'when klass param is' do + it 'null it returns the default strategy' do + expect(described_class.build!(nil).class).to eq described_class.default_strategy + end + + it 'not a valid class it raises StrategyNotFoundError exception' do + expect { described_class.build!('Whatever') }.to raise_error(described_class::StrategyNotFoundError) + end + + it 'not a descendant of AfterExportStrategy' do + expect { described_class.build!('User') }.to raise_error(described_class::StrategyNotFoundError) + end + end + + it 'initializes strategy with attributes param' do + params = { param1: 1, param2: 2, param3: 3 } + + strategy = described_class.build!("#{strategies_namespace}::DownloadNotificationStrategy", params) + + params.each { |k, v| expect(strategy.public_send(k)).to eq v } + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a204a8f1ffe..b675d5dc031 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -18,6 +18,7 @@ issues: - metrics - timelogs - issue_assignees +- closed_by events: - author - project diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 0716852f57f..f949a23ffbb 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -15,6 +15,7 @@ Issue: - updated_by_id - confidential - closed_at +- closed_by_id - due_date - moved_to_id - lock_version diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb index 2d35b026485..a3b3dc3be6d 100644 --- a/spec/lib/gitlab/url_blocker_spec.rb +++ b/spec/lib/gitlab/url_blocker_spec.rb @@ -74,13 +74,13 @@ describe Gitlab::UrlBlocker do expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false end - context 'when allow_private_networks is' do - let(:private_networks) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] } + context 'when allow_local_network is' do + let(:local_ips) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] } let(:fake_domain) { 'www.fakedomain.fake' } context 'true (default)' do it 'does not block urls from private networks' do - private_networks.each do |ip| + local_ips.each do |ip| stub_domain_resolv(fake_domain, ip) expect(described_class).not_to be_blocked_url("http://#{fake_domain}") @@ -94,14 +94,14 @@ describe Gitlab::UrlBlocker do context 'false' do it 'blocks urls from private networks' do - private_networks.each do |ip| + local_ips.each do |ip| stub_domain_resolv(fake_domain, ip) - expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_private_networks: false) + expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false) unstub_domain_resolv - expect(described_class).to be_blocked_url("http://#{ip}", allow_private_networks: false) + expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false) end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 138d21ede97..9e6aa109a4b 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -12,6 +12,14 @@ describe Gitlab::UsageData do create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true) create(:service, project: projects[1], type: 'SlackService', active: true) create(:service, project: projects[2], type: 'SlackService', active: true) + + gcp_cluster = create(:cluster, :provided_by_gcp) + create(:cluster, :provided_by_user) + create(:cluster, :provided_by_user, :disabled) + create(:clusters_applications_helm, :installed, cluster: gcp_cluster) + create(:clusters_applications_ingress, :installed, cluster: gcp_cluster) + create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster) + create(:clusters_applications_runner, :installed, cluster: gcp_cluster) end subject { described_class.data } @@ -64,6 +72,12 @@ describe Gitlab::UsageData do clusters clusters_enabled clusters_disabled + clusters_platforms_gke + clusters_platforms_user + clusters_applications_helm + clusters_applications_ingress + clusters_applications_prometheus + clusters_applications_runner in_review_folder groups issues @@ -97,6 +111,15 @@ describe Gitlab::UsageData do expect(count_data[:projects_jira_active]).to eq(2) expect(count_data[:projects_slack_notifications_active]).to eq(2) expect(count_data[:projects_slack_slash_active]).to eq(1) + + expect(count_data[:clusters_enabled]).to eq(6) + expect(count_data[:clusters_disabled]).to eq(1) + expect(count_data[:clusters_platforms_gke]).to eq(1) + expect(count_data[:clusters_platforms_user]).to eq(1) + expect(count_data[:clusters_applications_helm]).to eq(1) + expect(count_data[:clusters_applications_ingress]).to eq(1) + expect(count_data[:clusters_applications_prometheus]).to eq(1) + expect(count_data[:clusters_applications_runner]).to eq(1) end end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 1d0faf56f7c..2b3ffb2d7c0 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -55,7 +55,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do + context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -100,7 +100,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do + context 'when Gitaly workhorse_send_git_patch feature is disabled', :disable_gitaly do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -173,7 +173,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do + context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -275,12 +275,14 @@ describe Gitlab::Workhorse do describe '.git_http_ok' do let(:user) { create(:user) } + let(:repo_path) { 'ignored but not allowed to be empty in gitlab-workhorse' } let(:action) { 'info_refs' } let(:params) do { GL_ID: "user-#{user.id}", GL_USERNAME: user.username, GL_REPOSITORY: "project-#{project.id}", + RepoPath: repo_path, ShowAllRefs: false } end @@ -295,6 +297,7 @@ describe Gitlab::Workhorse do GL_ID: "user-#{user.id}", GL_USERNAME: user.username, GL_REPOSITORY: "wiki-#{project.id}", + RepoPath: repo_path, ShowAllRefs: false } end @@ -452,7 +455,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do + context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) diff --git a/spec/mailers/previews/notify_preview.rb b/spec/mailers/previews/notify_preview.rb index 580f0d56a92..43c3c89f140 100644 --- a/spec/mailers/previews/notify_preview.rb +++ b/spec/mailers/previews/notify_preview.rb @@ -65,7 +65,7 @@ class NotifyPreview < ActionMailer::Preview end def merge_request - @merge_request ||= project.merge_requests.find_by(source_branch: 'master', target_branch: 'feature') + @merge_request ||= project.merge_requests.first end def user diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb index 4e72d9d748e..0014bbcf9f5 100644 --- a/spec/models/ci/artifact_blob_spec.rb +++ b/spec/models/ci/artifact_blob_spec.rb @@ -65,6 +65,19 @@ describe Ci::ArtifactBlob do expect(url).not_to be_nil expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}") end + + context 'when port is configured' do + let(:port) { 1234 } + + it 'returns an URL with port number' do + allow(Gitlab.config.pages).to receive(:url).and_return("#{Gitlab.config.pages.url}:#{port}") + + url = subject.external_url(build.project, build) + + expect(url).not_to be_nil + expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}:#{port}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}") + end + end end end diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb new file mode 100644 index 00000000000..268561ee941 --- /dev/null +++ b/spec/models/ci/build_metadata_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Ci::BuildMetadata do + set(:user) { create(:user) } + set(:group) { create(:group, :access_requestable) } + set(:project) { create(:project, :repository, group: group, build_timeout: 2000) } + + set(:pipeline) do + create(:ci_pipeline, project: project, + sha: project.commit.id, + ref: project.default_branch, + status: 'success') + end + + let(:build) { create(:ci_build, pipeline: pipeline) } + let(:build_metadata) { create(:ci_build_metadata, build: build) } + + describe '#update_timeout_state' do + subject { build_metadata } + + context 'when runner is not assigned to the job' do + it "doesn't change timeout value" do + expect { subject.update_timeout_state }.not_to change { subject.reload.timeout } + end + + it "doesn't change timeout_source value" do + expect { subject.update_timeout_state }.not_to change { subject.reload.timeout_source } + end + end + + context 'when runner is assigned to the job' do + before do + build.update_attributes(runner: runner) + end + + context 'when runner timeout is lower than project timeout' do + let(:runner) { create(:ci_runner, maximum_timeout: 1900) } + + it 'sets runner timeout' do + expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(1900) + end + + it 'sets runner_timeout_source' do + expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to('runner_timeout_source') + end + end + + context 'when runner timeout is higher than project timeout' do + let(:runner) { create(:ci_runner, maximum_timeout: 2100) } + + it 'sets project timeout' do + expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(2000) + end + + it 'sets project_timeout_source' do + expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to('project_timeout_source') + end + end + end + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7d935cf8d76..a12717835b0 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1271,12 +1271,6 @@ describe Ci::Build do end describe 'project settings' do - describe '#timeout' do - it 'returns project timeout configuration' do - expect(build.timeout).to eq(project.build_timeout) - end - end - describe '#allow_git_fetch' do it 'return project allow_git_fetch configuration' do expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch) @@ -1469,24 +1463,24 @@ describe Ci::Build do let(:container_registry_enabled) { false } let(:predefined_variables) do [ + { key: 'CI_JOB_ID', value: build.id.to_s, public: true }, + { key: 'CI_JOB_TOKEN', value: build.token, public: false }, + { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, + { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, + { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true }, + { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false }, + { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }, { key: 'CI', value: 'true', public: true }, { key: 'GITLAB_CI', value: 'true', public: true }, { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true }, { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, - { key: 'CI_JOB_ID', value: build.id.to_s, public: true }, { key: 'CI_JOB_NAME', value: 'test', public: true }, { key: 'CI_JOB_STAGE', value: 'test', public: true }, - { key: 'CI_JOB_TOKEN', value: build.token, public: false }, { key: 'CI_COMMIT_SHA', value: build.sha, public: true }, { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true }, { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true }, - { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true }, - { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false }, - { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false }, - { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, - { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, { key: 'CI_BUILD_REF', value: build.sha, public: true }, { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, { key: 'CI_BUILD_REF_NAME', value: build.ref, public: true }, @@ -1951,6 +1945,7 @@ describe Ci::Build do before do allow(build).to receive(:predefined_variables) { [build_pre_var] } allow(build).to receive(:yaml_variables) { [build_yaml_var] } + allow(build).to receive(:persisted_variables) { [] } allow_any_instance_of(Project) .to receive(:predefined_variables) { [project_pre_var] } @@ -1999,6 +1994,106 @@ describe Ci::Build do end end end + + context 'when build has not been persisted yet' do + let(:build) do + described_class.new( + name: 'rspec', + stage: 'test', + ref: 'feature', + project: project, + pipeline: pipeline + ) + end + + it 'returns static predefined variables' do + expect(build.variables.size).to be >= 28 + expect(build.variables) + .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true) + expect(build).not_to be_persisted + end + end + end + + describe '#scoped_variables' do + context 'when build has not been persisted yet' do + let(:build) do + described_class.new( + name: 'rspec', + stage: 'test', + ref: 'feature', + project: project, + pipeline: pipeline + ) + end + + it 'does not persist the build' do + expect(build).to be_valid + expect(build).not_to be_persisted + + build.scoped_variables + + expect(build).not_to be_persisted + end + + it 'returns static predefined variables' do + keys = %w[CI_JOB_NAME + CI_COMMIT_SHA + CI_COMMIT_REF_NAME + CI_COMMIT_REF_SLUG + CI_JOB_STAGE] + + variables = build.scoped_variables + + variables.map { |env| env[:key] }.tap do |names| + expect(names).to include(*keys) + end + + expect(variables) + .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true) + end + + it 'does not return prohibited variables' do + keys = %w[CI_JOB_ID + CI_JOB_TOKEN + CI_BUILD_ID + CI_BUILD_TOKEN + CI_REGISTRY_USER + CI_REGISTRY_PASSWORD + CI_REPOSITORY_URL + CI_ENVIRONMENT_URL] + + build.scoped_variables.map { |env| env[:key] }.tap do |names| + expect(names).not_to include(*keys) + end + end + end + end + + describe '#scoped_variables_hash' do + context 'when overriding secret variables' do + before do + project.variables.create!(key: 'MY_VAR', value: 'my value 1') + pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2') + end + + it 'returns a regular hash created using valid ordering' do + expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2') + expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1') + end + end + + context 'when overriding user-provided variables' do + before do + pipeline.variables.build(key: 'MY_VAR', value: 'pipeline value') + build.yaml_variables = [{ key: 'MY_VAR', value: 'myvar', public: true }] + end + + it 'returns a hash including variable with higher precedence' do + expect(build.scoped_variables_hash).to include('MY_VAR': 'pipeline value') + expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar') + end + end end describe 'state transition: any => [:pending]' do @@ -2011,6 +2106,70 @@ describe Ci::Build do end end + describe 'state transition: pending: :running' do + let(:runner) { create(:ci_runner) } + let(:job) { create(:ci_build, :pending, runner: runner) } + + before do + job.project.update_attribute(:build_timeout, 1800) + end + + def run_job_without_exception + job.run! + rescue StateMachines::InvalidTransition + end + + shared_examples 'saves data on transition' do + it 'saves timeout' do + expect { job.run! }.to change { job.reload.ensure_metadata.timeout }.from(nil).to(expected_timeout) + end + + it 'saves timeout_source' do + expect { job.run! }.to change { job.reload.ensure_metadata.timeout_source }.from('unknown_timeout_source').to(expected_timeout_source) + end + + context 'when Ci::BuildMetadata#update_timeout_state fails update' do + before do + allow_any_instance_of(Ci::BuildMetadata).to receive(:update_timeout_state).and_return(false) + end + + it "doesn't save timeout" do + expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source } + end + + it "doesn't save timeout_source" do + expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source } + end + + it 'raises an exception' do + expect { job.run! }.to raise_error(StateMachines::InvalidTransition) + end + end + end + + context 'when runner timeout overrides project timeout' do + let(:expected_timeout) { 900 } + let(:expected_timeout_source) { 'runner_timeout_source' } + + before do + runner.update_attribute(:maximum_timeout, 900) + end + + it_behaves_like 'saves data on transition' + end + + context "when runner timeout doesn't override project timeout" do + let(:expected_timeout) { 1800 } + let(:expected_timeout_source) { 'project_timeout_source' } + + before do + runner.update_attribute(:maximum_timeout, 3600) + end + + it_behaves_like 'saves data on transition' + end + end + describe 'state transition: any => [:running]' do shared_examples 'validation is active' do context 'when depended job has not been completed yet' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 92f00cfbc19..dd94515b0a4 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -346,6 +346,20 @@ describe Ci::Pipeline, :mailer do end end end + + context 'when variables policy is specified' do + let(:config) do + { unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } }, + feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } } } + end + + it 'returns stage seeds only when variables expression is truthy' do + seeds = pipeline.stage_seeds + + expect(seeds.size).to eq 1 + expect(seeds.dig(0, 0, :name)).to eq 'unit' + end + end end describe '#seeds_size' do diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index ba7bad617b4..0eb1e3876e2 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -3,6 +3,18 @@ require 'rails_helper' describe Clusters::Applications::Helm do include_examples 'cluster application core specs', :clusters_applications_helm + describe '.installed' do + subject { described_class.installed } + + let!(:cluster) { create(:clusters_applications_helm, :installed) } + + before do + create(:clusters_applications_helm, :errored) + end + + it { is_expected.to contain_exactly(cluster) } + end + describe '#install_command' do let(:helm) { create(:clusters_applications_helm) } diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 03f5b88a525..a47a07d908d 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -11,6 +11,18 @@ 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/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 2905b58066b..aeca6ee903a 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -4,6 +4,18 @@ describe Clusters::Applications::Prometheus do include_examples 'cluster application core specs', :clusters_applications_prometheus include_examples 'cluster application status specs', :cluster_application_prometheus + 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, projects: [project]) } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index a574779e39d..64d995a73c1 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -8,6 +8,18 @@ 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 8f12a0e3085..b942554d67b 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -39,6 +39,42 @@ describe Clusters::Cluster do it { is_expected.to contain_exactly(cluster) } end + describe '.user_provided' do + subject { described_class.user_provided } + + let!(:cluster) { create(:cluster, :provided_by_user) } + + before do + create(:cluster, :provided_by_gcp) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe '.gcp_provided' do + subject { described_class.gcp_provided } + + let!(:cluster) { create(:cluster, :provided_by_gcp) } + + before do + create(:cluster, :provided_by_user) + end + + it { is_expected.to contain_exactly(cluster) } + end + + describe '.gcp_installed' do + subject { described_class.gcp_installed } + + let!(:cluster) { create(:cluster, :provided_by_gcp) } + + before do + create(:cluster, :providing_by_gcp) + end + + it { is_expected.to contain_exactly(cluster) } + end + describe 'validation' do subject { cluster.valid? } diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb new file mode 100644 index 00000000000..27c86e60e60 --- /dev/null +++ b/spec/models/concerns/chronic_duration_attribute_spec.rb @@ -0,0 +1,115 @@ +require 'spec_helper' + +shared_examples 'ChronicDurationAttribute reader' do + it 'contains dynamically created reader method' do + expect(subject.class).to be_public_method_defined(virtual_field) + end + + it 'outputs chronic duration formatted value' do + subject.send("#{source_field}=", 120) + + expect(subject.send(virtual_field)).to eq('2m') + end + + context 'when value is set to nil' do + it 'outputs nil' do + subject.send("#{source_field}=", nil) + + expect(subject.send(virtual_field)).to be_nil + end + end +end + +shared_examples 'ChronicDurationAttribute writer' do + it 'contains dynamically created writer method' do + expect(subject.class).to be_public_method_defined("#{virtual_field}=") + end + + before do + subject.send("#{virtual_field}=", '10m') + end + + it 'parses chronic duration input' do + expect(subject.send(source_field)).to eq(600) + end + + it 'passes validation' do + expect(subject.valid?).to be_truthy + end + + context 'when negative input is used' do + before do + subject.send("#{source_field}=", 3600) + end + + it "doesn't raise exception" do + expect { subject.send("#{virtual_field}=", '-10m') }.not_to raise_error(ChronicDuration::DurationParseError) + end + + it "doesn't change value" do + expect { subject.send("#{virtual_field}=", '-10m') }.not_to change { subject.send(source_field) } + end + + it "doesn't pass validation" do + subject.send("#{virtual_field}=", '-10m') + + expect(subject.valid?).to be_falsey + expect(subject.errors&.messages).to include(virtual_field => ['is not a correct duration']) + end + end + + context 'when empty input is used' do + before do + subject.send("#{virtual_field}=", '') + end + + it 'writes nil' do + expect(subject.send(source_field)).to be_nil + end + + it 'passes validation' do + expect(subject.valid?).to be_truthy + end + end + + context 'when nil input is used' do + before do + subject.send("#{virtual_field}=", nil) + end + + it 'writes nil' do + expect(subject.send(source_field)).to be_nil + end + + it 'passes validation' do + expect(subject.valid?).to be_truthy + end + + it "doesn't raise exception" do + expect { subject.send("#{virtual_field}=", nil) }.not_to raise_error(NoMethodError) + end + end +end + +describe 'ChronicDurationAttribute' do + let(:source_field) {:maximum_timeout} + let(:virtual_field) {:maximum_timeout_human_readable} + + subject { Ci::Runner.new } + + it_behaves_like 'ChronicDurationAttribute reader' + it_behaves_like 'ChronicDurationAttribute writer' +end + +describe 'ChronicDurationAttribute - reader' do + let(:source_field) {:timeout} + let(:virtual_field) {:timeout_human_readable} + + subject {Ci::BuildMetadata.new} + + it "doesn't contain dynamically created writer method" do + expect(subject.class).not_to be_public_method_defined("#{virtual_field}=") + end + + it_behaves_like 'ChronicDurationAttribute reader' +end diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb index 3d7283e2164..41440c6d288 100644 --- a/spec/models/deploy_key_spec.rb +++ b/spec/models/deploy_key_spec.rb @@ -17,4 +17,25 @@ describe DeployKey, :mailer do should_not_email(user) end end + + describe '#user' do + let(:deploy_key) { create(:deploy_key) } + let(:user) { create(:user) } + + context 'when user is set' do + before do + deploy_key.user = user + end + + it 'returns the user' do + expect(deploy_key.user).to be(user) + end + end + + context 'when user is not set' do + it 'returns the ghost user' do + expect(deploy_key.user).to eq(User.ghost) + end + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1a7a6e035ea..fef868ac0f2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -224,14 +224,14 @@ describe Project do project2 = build(:project, import_url: 'http://localhost:9000/t.git') expect(project2).to be_invalid - expect(project2.errors[:import_url]).to include('imports are not allowed from that URL') + expect(project2.errors[:import_url].first).to include('Requests to localhost are not allowed') end it "does not allow blocked import_url port" do project2 = build(:project, import_url: 'http://github.com:25/t.git') expect(project2).to be_invalid - expect(project2.errors[:import_url]).to include('imports are not allowed from that URL') + expect(project2.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443') end describe 'project pending deletion' do @@ -1265,6 +1265,34 @@ describe Project do end end + describe '#pages_group_url' do + let(:group) { create :group, name: group_name } + let(:project) { create :project, namespace: group, name: project_name } + let(:domain) { 'Example.com' } + let(:port) { 1234 } + + subject { project.pages_group_url } + + before do + allow(Settings.pages).to receive(:host).and_return(domain) + allow(Gitlab.config.pages).to receive(:url).and_return("http://example.com:#{port}") + end + + context 'group page' do + let(:group_name) { 'Group' } + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq("http://group.example.com:#{port}") } + end + + context 'project page' do + let(:group_name) { 'Group' } + let(:project_name) { 'Project' } + + it { is_expected.to eq("http://group.example.com:#{port}") } + end + end + describe '.search' do let(:project) { create(:project, description: 'kitten mittens') } @@ -2532,7 +2560,7 @@ describe Project do end end - describe '#remove_exports' do + describe '#remove_export' do let(:legacy_project) { create(:project, :legacy_storage, :with_export) } let(:project) { create(:project, :with_export) } @@ -2580,6 +2608,23 @@ describe Project do end end + describe '#remove_exported_project_file' do + let(:project) { create(:project, :with_export) } + + it 'removes the exported project file' do + exported_file = project.export_project_path + + expect(File.exist?(exported_file)).to be_truthy + + allow(FileUtils).to receive(:rm_f).and_call_original + expect(FileUtils).to receive(:rm_f).with(exported_file).and_call_original + + project.remove_exported_project_file + + expect(File.exist?(exported_file)).to be_falsy + end + end + describe '#forks_count' do it 'returns the number of forks' do project = build(:project) diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 79f25dc4360..83ed3b203e6 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -58,6 +58,21 @@ describe Service do end describe "Template" do + describe '.build_from_template' do + context 'when template is invalid' do + it 'sets service template to inactive when template is invalid' do + project = create(:project) + template = JiraService.new(template: true, active: true) + template.save(validate: false) + + service = described_class.build_from_template(project.id, template) + + expect(service).to be_valid + expect(service.active).to be false + end + end + end + describe "for pushover service" do let!(:service_template) do PushoverService.create( diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bbfdda23a31..100418da804 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -25,7 +25,7 @@ describe User do it { is_expected.to have_many(:group_members) } it { is_expected.to have_many(:groups) } it { is_expected.to have_many(:keys).dependent(:destroy) } - it { is_expected.to have_many(:deploy_keys).dependent(:destroy) } + it { is_expected.to have_many(:deploy_keys).dependent(:nullify) } it { is_expected.to have_many(:events).dependent(:destroy) } it { is_expected.to have_many(:issues).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) } diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 852f67db958..8ad19e3f0f5 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -1141,4 +1141,33 @@ describe API::Commits do end end end + + describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do + let!(:project) { create(:project, :repository, :private) } + let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') } + let(:commit) { merged_mr.merge_request_diff.commits.last } + + it 'returns the correct merge request' do + get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + expect(json_response.length).to eq(1) + expect(json_response[0]['id']).to eq(merged_mr.id) + end + + it 'returns 403 for an unauthorized user' do + project.add_guest(user) + + get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user) + + expect(response).to have_gitlab_http_status(403) + end + + it 'responds 404 when the commit does not exist' do + get api("/projects/#{project.id}/repository/commits/a7d26f00c35b/merge_requests", user) + + expect(response).to have_gitlab_http_status(404) + end + end end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 0772b3f2e64..ae9c0e9c304 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -91,6 +91,10 @@ describe API::DeployKeys do expect do post api("/projects/#{project.id}/deploy_keys", admin), key_attrs end.to change { project.deploy_keys.count }.by(1) + + new_key = project.deploy_keys.last + expect(new_key.key).to eq(key_attrs[:key]) + expect(new_key.user).to eq(admin) end it 'returns an existing ssh key when attempting to add a duplicate' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 3cb90a1b8ef..db8c5f963d6 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -251,44 +251,23 @@ describe API::Internal do end context 'with env passed as a JSON' do - context 'when relative path envs are not set' do - it 'sets env in RequestStore' do - expect(Gitlab::Git::Env).to receive(:set).with({ - 'GIT_OBJECT_DIRECTORY' => 'foo', - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar' - }) - - push(key, project.wiki, env: { - GIT_OBJECT_DIRECTORY: 'foo', - GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar' - }.to_json) + let(:gl_repository) { project.gl_repository(is_wiki: true) } - expect(response).to have_gitlab_http_status(200) - end - end + it 'sets env in RequestStore' do + obj_dir_relative = './objects' + alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2'] - context 'when relative path envs are set' do - it 'sets env in RequestStore' do - obj_dir_relative = './objects' - alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2'] - repo_path = project.wiki.repository.path_to_repo - - expect(Gitlab::Git::Env).to receive(:set).with({ - 'GIT_OBJECT_DIRECTORY' => File.join(repo_path, obj_dir_relative), - 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => alt_obj_dirs_relative.map { |d| File.join(repo_path, d) }, - 'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative, - 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative - }) - - push(key, project.wiki, env: { - GIT_OBJECT_DIRECTORY: 'foo', - GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar', - GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative, - GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative - }.to_json) + expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, { + 'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative, + 'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative + }) - expect(response).to have_gitlab_http_status(200) - end + push(key, project.wiki, env: { + GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative, + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative + }.to_json) + + expect(response).to have_gitlab_http_status(200) end end diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb index 12583109b59..3834d27d0a9 100644 --- a/spec/requests/api/project_export_spec.rb +++ b/spec/requests/api/project_export_spec.rb @@ -5,6 +5,7 @@ describe API::ProjectExport do set(:project_none) { create(:project) } set(:project_started) { create(:project) } set(:project_finished) { create(:project) } + set(:project_after_export) { create(:project) } set(:user) { create(:user) } set(:admin) { create(:admin) } @@ -12,11 +13,13 @@ describe API::ProjectExport do let(:path_none) { "/projects/#{project_none.id}/export" } let(:path_started) { "/projects/#{project_started.id}/export" } let(:path_finished) { "/projects/#{project_finished.id}/export" } + let(:path_after_export) { "/projects/#{project_after_export.id}/export" } let(:download_path) { "/projects/#{project.id}/export/download" } let(:download_path_none) { "/projects/#{project_none.id}/export/download" } let(:download_path_started) { "/projects/#{project_started.id}/export/download" } let(:download_path_finished) { "/projects/#{project_finished.id}/export/download" } + let(:download_path_export_action) { "/projects/#{project_after_export.id}/export/download" } let(:export_path) { "#{Dir.tmpdir}/project_export_spec" } @@ -29,6 +32,11 @@ describe API::ProjectExport do # simulate exported FileUtils.mkdir_p project_finished.export_path FileUtils.touch File.join(project_finished.export_path, '_export.tar.gz') + + # simulate in after export action + FileUtils.mkdir_p project_after_export.export_path + FileUtils.touch File.join(project_after_export.export_path, '_export.tar.gz') + FileUtils.touch Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy.lock_file_path(project_after_export) end after do @@ -73,6 +81,14 @@ describe API::ProjectExport do expect(json_response['export_status']).to eq('started') end + it 'is after_export' do + get api(path_after_export, user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to match_response_schema('public_api/v4/project/export_status') + expect(json_response['export_status']).to eq('after_export_action') + end + it 'is finished' do get api(path_finished, user) @@ -99,6 +115,7 @@ describe API::ProjectExport do project_none.add_master(user) project_started.add_master(user) project_finished.add_master(user) + project_after_export.add_master(user) end it_behaves_like 'get project export status ok' @@ -163,6 +180,36 @@ describe API::ProjectExport do end end + shared_examples_for 'get project export upload after action' do + context 'and is uploading' do + it 'downloads' do + get api(download_path_export_action, user) + + expect(response).to have_gitlab_http_status(200) + end + end + + context 'when upload complete' do + before do + FileUtils.rm_rf(project_after_export.export_path) + end + + it_behaves_like '404 response' do + let(:request) { get api(download_path_export_action, user) } + end + end + end + + shared_examples_for 'get project download by strategy' do + context 'when upload strategy set' do + it_behaves_like 'get project export upload after action' + end + + context 'when download strategy set' do + it_behaves_like 'get project export download' + end + end + it_behaves_like 'when project export is disabled' do let(:request) { get api(download_path, admin) } end @@ -171,7 +218,7 @@ describe API::ProjectExport do context 'when user is an admin' do let(:user) { admin } - it_behaves_like 'get project export download' + it_behaves_like 'get project download by strategy' end context 'when user is a master' do @@ -180,9 +227,10 @@ describe API::ProjectExport do project_none.add_master(user) project_started.add_master(user) project_finished.add_master(user) + project_after_export.add_master(user) end - it_behaves_like 'get project export download' + it_behaves_like 'get project download by strategy' end context 'when user is a developer' do @@ -229,10 +277,30 @@ describe API::ProjectExport do end shared_examples_for 'post project export start' do - it 'starts' do - post api(path, user) + context 'with upload strategy' do + context 'when params invalid' do + it_behaves_like '400 response' do + let(:request) { post(api(path, user), 'upload[url]' => 'whatever') } + end + end + + it 'starts' do + allow_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).to receive(:send_file) + + post(api(path, user), 'upload[url]' => 'http://gitlab.com') - expect(response).to have_gitlab_http_status(202) + expect(response).to have_gitlab_http_status(202) + end + end + + context 'with download strategy' do + it 'starts' do + expect_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).not_to receive(:send_file) + + post api(path, user) + + expect(response).to have_gitlab_http_status(202) + end end end @@ -253,6 +321,7 @@ describe API::ProjectExport do project_none.add_master(user) project_started.add_master(user) project_finished.add_master(user) + project_after_export.add_master(user) end it_behaves_like 'post project export start' diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index f3dd121faa9..5084b36c761 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -109,6 +109,26 @@ describe API::Runner do end end + context 'when maximum job timeout is specified' do + it 'creates runner' do + post api('/runners'), token: registration_token, + maximum_timeout: 9000 + + expect(response).to have_gitlab_http_status 201 + expect(Ci::Runner.first.maximum_timeout).to eq(9000) + end + + context 'when maximum job timeout is empty' do + it 'creates runner' do + post api('/runners'), token: registration_token, + maximum_timeout: '' + + expect(response).to have_gitlab_http_status 201 + expect(Ci::Runner.first.maximum_timeout).to be_nil + end + end + end + %w(name version revision platform architecture).each do |param| context "when info parameter '#{param}' info is present" do let(:value) { "#{param}_value" } @@ -340,12 +360,12 @@ describe API::Runner do let(:expected_steps) do [{ 'name' => 'script', 'script' => %w(ls date), - 'timeout' => job.timeout, + 'timeout' => job.metadata_timeout, 'when' => 'on_success', 'allow_failure' => false }, { 'name' => 'after_script', 'script' => %w(ls date), - 'timeout' => job.timeout, + 'timeout' => job.metadata_timeout, 'when' => 'always', 'allow_failure' => true }] end @@ -648,6 +668,41 @@ describe API::Runner do end end end + + describe 'timeout support' do + context 'when project specifies job timeout' do + let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) } + + it 'contains info about timeout taken from project' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['runner_info']).to include({ 'timeout' => 1234 }) + end + + context 'when runner specifies lower timeout' do + let(:runner) { create(:ci_runner, maximum_timeout: 1000) } + + it 'contains info about timeout overridden by runner' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['runner_info']).to include({ 'timeout' => 1000 }) + end + end + + context 'when runner specifies bigger timeout' do + let(:runner) { create(:ci_runner, maximum_timeout: 2000) } + + it 'contains info about timeout not overridden by runner' do + request_job + + expect(response).to have_gitlab_http_status(201) + expect(json_response['runner_info']).to include({ 'timeout' => 1234 }) + end + end + end + end end def request_job(token = runner.token, **params) diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index ec5cad4f4fd..d30f0cf36e2 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -123,6 +123,7 @@ describe API::Runners do expect(response).to have_gitlab_http_status(200) expect(json_response['description']).to eq(shared_runner.description) + expect(json_response['maximum_timeout']).to be_nil end end @@ -192,7 +193,8 @@ describe API::Runners do tag_list: ['ruby2.1', 'pgsql', 'mysql'], run_untagged: 'false', locked: 'true', - access_level: 'ref_protected') + access_level: 'ref_protected', + maximum_timeout: 1234) shared_runner.reload expect(response).to have_gitlab_http_status(200) @@ -204,6 +206,7 @@ describe API::Runners do expect(shared_runner.ref_protected?).to be_truthy expect(shared_runner.ensure_runner_queue_value) .not_to eq(runner_queue_value) + expect(shared_runner.maximum_timeout).to eq(1234) end end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index 16431ed4188..70402bac2e2 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -25,5 +25,10 @@ describe StatusEntity do allow(Rails.env).to receive(:development?) { true } expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico') end + + it 'contains a canary namespaced favicon if canary env' do + stub_env('CANARY', 'true') + expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.ico') + end end end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index b86a3d72bb4..8de0bdf92e2 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -29,7 +29,8 @@ describe Ci::RetryBuildService do commit_id deployments erased_by_id last_deployment project_id runner_id tag_taggings taggings tags trigger_request_id user_id auto_canceled_by_id retried failure_reason - artifacts_file_store artifacts_metadata_store].freeze + artifacts_file_store artifacts_metadata_store + metadata].freeze shared_examples 'build duplication' do let(:another_pipeline) { create(:ci_empty_pipeline, project: project) } diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 47c1ebbeb81..7ae49c06896 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -67,6 +67,10 @@ describe Issues::CloseService do expect(issue).to be_closed end + it 'records closed user' do + expect(issue.closed_by_id).to be(user.id) + end + it 'sends email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last expect(email.to.first).to eq(user2.email) diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 4413c6ef83e..2cacb97a293 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -70,6 +70,16 @@ describe Projects::CreateService, '#execute' do opts[:default_branch] = 'master' expect(create_project(user, opts)).to eq(nil) end + + it 'sets invalid service as inactive' do + create(:service, type: 'JiraService', project: nil, template: true, active: true) + + project = create_project(user, opts) + service = project.services.first + + expect(project).to be_persisted + expect(service.active).to be false + end end context 'wiki_enabled creates repository directory' do @@ -232,14 +242,15 @@ describe Projects::CreateService, '#execute' do end context 'when a bad service template is created' do - it 'reports an error in the imported project' do + it 'sets service to be inactive' do opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce' create(:service, type: 'DroneCiService', project: nil, template: true, active: true) project = create_project(user, opts) + service = project.services.first - expect(project.errors.full_messages_for(:base).first).to match(/Unable to save project. Error: Unable to save DroneCiService/) - expect(project.services.count).to eq 0 + expect(project).to be_persisted + expect(service.active).to be false end end diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb new file mode 100644 index 00000000000..51491c7d529 --- /dev/null +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Projects::ImportExport::ExportService do + describe '#execute' do + let!(:user) { create(:user) } + let(:project) { create(:project) } + let(:shared) { project.import_export_shared } + let(:service) { described_class.new(project, user) } + let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new } + + context 'when all saver services succeed' do + before do + allow(service).to receive(:save_services).and_return(true) + end + + it 'saves the project in the file system' do + expect(Gitlab::ImportExport::Saver).to receive(:save).with(project: project, shared: shared) + + service.execute + end + + it 'calls the after export strategy' do + expect(after_export_strategy).to receive(:execute) + + service.execute(after_export_strategy) + end + + context 'when after export strategy fails' do + before do + allow(after_export_strategy).to receive(:execute).and_return(false) + end + + after do + service.execute(after_export_strategy) + end + + it 'removes the remaining exported data' do + allow(shared).to receive(:export_path).and_return('whatever') + allow(FileUtils).to receive(:rm_rf) + + expect(FileUtils).to receive(:rm_rf).with(shared.export_path) + end + + it 'notifies the user' do + expect_any_instance_of(NotificationService).to receive(:project_not_exported) + end + + it 'notifies logger' do + allow(Rails.logger).to receive(:error) + + expect(Rails.logger).to receive(:error) + end + end + end + + context 'when saver services fail' do + before do + allow(service).to receive(:save_services).and_return(false) + end + + after do + expect { service.execute }.to raise_error(Gitlab::ImportExport::Error) + end + + it 'removes the remaining exported data' do + allow(shared).to receive(:export_path).and_return('whatever') + allow(FileUtils).to receive(:rm_rf) + + expect(FileUtils).to receive(:rm_rf).with(shared.export_path) + end + + it 'notifies the user' do + expect_any_instance_of(NotificationService).to receive(:project_not_exported) + end + + it 'notifies logger' do + expect(Rails.logger).to receive(:error) + end + + it 'the after export strategy is not called' do + expect(service).not_to receive(:execute_after_export_action) + end + end + end +end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index bf7facaec99..30c89ebd821 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -156,7 +156,7 @@ describe Projects::ImportService do result = described_class.new(project, user).execute expect(result[:status]).to eq :error - expect(result[:message]).to end_with 'Blocked import URL.' + expect(result[:message]).to include('Requests to localhost are not allowed') end it 'fails with port 25' do @@ -165,7 +165,7 @@ describe Projects::ImportService do result = described_class.new(project, user).execute expect(result[:status]).to eq :error - expect(result[:message]).to end_with 'Blocked import URL.' + expect(result[:message]).to include('Only allowed ports are 22, 80, 443') end end diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb index d72925e1838..5ff7b0b68c9 100644 --- a/spec/support/cookie_helper.rb +++ b/spec/support/cookie_helper.rb @@ -2,12 +2,25 @@ # module CookieHelper def set_cookie(name, value, options = {}) + case page.driver + when Capybara::RackTest::Driver + rack_set_cookie(name, value) + else + selenium_set_cookie(name, value, options) + end + end + + def selenium_set_cookie(name, value, options = {}) # Selenium driver will not set cookies for a given domain when the browser is at `about:blank`. # It also doesn't appear to allow overriding the cookie path. loading `/` is the most inclusive. visit options.fetch(:path, '/') unless on_a_page? page.driver.browser.manage.add_cookie(name: name, value: value, **options) end + def rack_set_cookie(name, value) + page.driver.browser.set_cookie("#{name}=#{value}") + end + def get_cookie(name) page.driver.browser.manage.cookie_named(name) end diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb index c7e8a39a617..9cf541372b5 100644 --- a/spec/support/gitaly.rb +++ b/spec/support/gitaly.rb @@ -1,11 +1,13 @@ RSpec.configure do |config| config.before(:each) do |example| if example.metadata[:disable_gitaly] - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) + # Use 'and_wrap_original' to make sure the arguments are valid + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) && false } else next if example.metadata[:skip_gitaly_mock] - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) + # Use 'and_wrap_original' to make sure the arguments are valid + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) || true } end end end diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index d08183846a0..db34090e971 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -140,6 +140,10 @@ module LoginHelpers end allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config) stub_omniauth_setting(messages) + stub_saml_authorize_path_helpers + end + + def stub_saml_authorize_path_helpers allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml') allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') end diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb index 6bf976a2cf9..5d6f662e8fe 100644 --- a/spec/support/migrations_helpers.rb +++ b/spec/support/migrations_helpers.rb @@ -1,6 +1,9 @@ module MigrationsHelpers def table(name) - Class.new(ActiveRecord::Base) { self.table_name = name } + Class.new(ActiveRecord::Base) do + self.table_name = name + self.inheritance_column = :_type_disabled + end end def migrations_paths diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb index cd9974cd6e2..6352f1527cd 100644 --- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb +++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb @@ -61,12 +61,18 @@ shared_examples "migrates" do |to_store:, from_store: nil| expect { migrate(to) }.not_to change { file.exists? } end - context 'when migrate! is not oqqupied by another process' do + context 'when migrate! is not occupied by another process' do it 'executes migrate!' do expect(subject).to receive(:object_store=).at_least(1) migrate(to) end + + it 'executes use_file' do + expect(subject).to receive(:unsafe_use_file).once + + subject.use_file + end end context 'when migrate! is occupied by another process' do @@ -79,7 +85,13 @@ shared_examples "migrates" do |to_store:, from_store: nil| it 'does not execute migrate!' do expect(subject).not_to receive(:unsafe_migrate!) - expect { migrate(to) }.to raise_error('Already running') + expect { migrate(to) }.to raise_error('exclusive lease already taken') + end + + it 'does not execute use_file' do + expect(subject).not_to receive(:unsafe_use_file) + + expect { subject.use_file }.to raise_error('exclusive lease already taken') end after do diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb index b778d26060d..6fcfae358ec 100644 --- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb +++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb @@ -1,10 +1,9 @@ require 'rake_helper' describe 'gitlab:uploads:migrate rake tasks' do - let!(:projects) { create_list(:project, 10, :with_avatar) } - let(:model_class) { Project } - let(:uploader_class) { AvatarUploader } - let(:mounted_as) { :avatar } + let(:model_class) { nil } + let(:uploader_class) { nil } + let(:mounted_as) { nil } let(:batch_size) { 3 } before do @@ -20,9 +19,125 @@ describe 'gitlab:uploads:migrate rake tasks' do run_rake_task("gitlab:uploads:migrate", *args) end - it 'enqueue jobs in batch' do - expect(ObjectStorage::MigrateUploadsWorker).to receive(:enqueue!).exactly(4).times + shared_examples 'enqueue jobs in batch' do |batch:| + it do + expect(ObjectStorage::MigrateUploadsWorker) + .to receive(:perform_async).exactly(batch).times + .and_return("A fake job.") - run + run + end + end + + context "for AvatarUploader" do + let(:uploader_class) { AvatarUploader } + let(:mounted_as) { :avatar } + + context "for Project" do + let(:model_class) { Project } + let!(:projects) { create_list(:project, 10, :with_avatar) } + + it_behaves_like 'enqueue jobs in batch', batch: 4 + + context 'Upload has store = nil' do + before do + Upload.where(model: projects).update_all(store: nil) + end + + it_behaves_like 'enqueue jobs in batch', batch: 4 + end + end + + context "for Group" do + let(:model_class) { Group } + + before do + create_list(:group, 10, :with_avatar) + end + + it_behaves_like 'enqueue jobs in batch', batch: 4 + end + + context "for User" do + let(:model_class) { User } + + before do + create_list(:user, 10, :with_avatar) + end + + it_behaves_like 'enqueue jobs in batch', batch: 4 + end + end + + context "for AttachmentUploader" do + let(:uploader_class) { AttachmentUploader } + + context "for Note" do + let(:model_class) { Note } + let(:mounted_as) { :attachment } + + before do + create_list(:note, 10, :with_attachment) + end + + it_behaves_like 'enqueue jobs in batch', batch: 4 + end + + context "for Appearance" do + let(:model_class) { Appearance } + let(:mounted_as) { :logo } + + before do + create(:appearance, :with_logos) + end + + %i(logo header_logo).each do |mount| + it_behaves_like 'enqueue jobs in batch', batch: 1 do + let(:mounted_as) { mount } + end + end + end + end + + context "for FileUploader" do + let(:uploader_class) { FileUploader } + let(:model_class) { Project } + + before do + create_list(:project, 10) do |model| + uploader_class.new(model) + .store!(fixture_file_upload('spec/fixtures/doc_sample.txt')) + end + end + + it_behaves_like 'enqueue jobs in batch', batch: 4 + end + + context "for PersonalFileUploader" do + let(:uploader_class) { PersonalFileUploader } + let(:model_class) { PersonalSnippet } + + before do + create_list(:personal_snippet, 10) do |model| + uploader_class.new(model) + .store!(fixture_file_upload('spec/fixtures/doc_sample.txt')) + end + end + + it_behaves_like 'enqueue jobs in batch', batch: 4 + end + + context "for NamespaceFileUploader" do + let(:uploader_class) { NamespaceFileUploader } + let(:model_class) { Snippet } + + before do + create_list(:snippet, 10) do |model| + uploader_class.new(model) + .store!(fixture_file_upload('spec/fixtures/doc_sample.txt')) + end + end + + it_behaves_like 'enqueue jobs in batch', batch: 4 end end diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 1d406c71955..59e02fecbce 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -308,6 +308,30 @@ describe ObjectStorage do it { is_expected.to eq(remote_directory) } end + context 'when file is in use' do + def when_file_is_in_use + uploader.use_file do + yield + end + end + + it 'cannot migrate' do + when_file_is_in_use do + expect(uploader).not_to receive(:unsafe_migrate!) + + expect { uploader.migrate!(described_class::Store::REMOTE) }.to raise_error('exclusive lease already taken') + end + end + + it 'cannot use_file' do + when_file_is_in_use do + expect(uploader).not_to receive(:unsafe_use_file) + + expect { uploader.use_file }.to raise_error('exclusive lease already taken') + end + end + end + describe '#fog_credentials' do let(:connection) { Settingslogic.new("provider" => "AWS") } diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb new file mode 100644 index 00000000000..b34f427fd8a --- /dev/null +++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb @@ -0,0 +1,146 @@ +require 'spec_helper' + +describe ObjectStorage::BackgroundMoveWorker do + let(:local) { ObjectStorage::Store::LOCAL } + let(:remote) { ObjectStorage::Store::REMOTE } + + def perform + described_class.perform_async(uploader_class.name, subject_class, file_field, subject_id) + end + + context 'for LFS' do + let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) } + let(:uploader_class) { LfsObjectUploader } + let(:subject_class) { LfsObject } + let(:file_field) { :file } + let(:subject_id) { lfs_object.id } + + context 'when object storage is enabled' do + before do + stub_lfs_object_storage(background_upload: true) + end + + it 'uploads object to storage' do + expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote) + end + + context 'when background upload is disabled' do + before do + allow(Gitlab.config.lfs.object_store).to receive(:background_upload) { false } + end + + it 'is skipped' do + expect { perform }.not_to change { lfs_object.reload.file_store } + end + end + end + + context 'when object storage is disabled' do + before do + stub_lfs_object_storage(enabled: false) + end + + it "doesn't migrate files" do + perform + + expect(lfs_object.reload.file_store).to eq(local) + end + end + end + + context 'for legacy artifacts' do + let(:build) { create(:ci_build, :legacy_artifacts) } + let(:uploader_class) { LegacyArtifactUploader } + let(:subject_class) { Ci::Build } + let(:file_field) { :artifacts_file } + let(:subject_id) { build.id } + + context 'when local storage is used' do + let(:store) { local } + + context 'and remote storage is defined' do + before do + stub_artifacts_object_storage(background_upload: true) + end + + it "migrates file to remote storage" do + perform + + expect(build.reload.artifacts_file_store).to eq(remote) + end + + context 'for artifacts_metadata' do + let(:file_field) { :artifacts_metadata } + + it 'migrates metadata to remote storage' do + perform + + expect(build.reload.artifacts_metadata_store).to eq(remote) + end + end + end + end + end + + context 'for job artifacts' do + let(:artifact) { create(:ci_job_artifact, :archive) } + let(:uploader_class) { JobArtifactUploader } + let(:subject_class) { Ci::JobArtifact } + let(:file_field) { :file } + let(:subject_id) { artifact.id } + + context 'when local storage is used' do + let(:store) { local } + + context 'and remote storage is defined' do + before do + stub_artifacts_object_storage(background_upload: true) + end + + it "migrates file to remote storage" do + perform + + expect(artifact.reload.file_store).to eq(remote) + end + end + end + end + + context 'for uploads' do + let!(:project) { create(:project, :with_avatar) } + let(:uploader_class) { AvatarUploader } + let(:file_field) { :avatar } + + context 'when local storage is used' do + let(:store) { local } + + context 'and remote storage is defined' do + before do + stub_uploads_object_storage(uploader_class, background_upload: true) + end + + describe 'supports using the model' do + let(:subject_class) { project.class } + let(:subject_id) { project.id } + + it "migrates file to remote storage" do + perform + + expect(project.reload.avatar.file_storage?).to be_falsey + end + end + + describe 'supports using the Upload' do + let(:subject_class) { Upload } + let(:subject_id) { project.avatar.upload.id } + + it "migrates file to remote storage" do + perform + + expect(project.reload.avatar.file_storage?).to be_falsey + end + end + end + end + end +end diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb new file mode 100644 index 00000000000..7a7dcb71680 --- /dev/null +++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb @@ -0,0 +1,119 @@ +require 'spec_helper' + +describe ObjectStorage::MigrateUploadsWorker, :sidekiq do + shared_context 'sanity_check! fails' do + before do + expect(described_class).to receive(:sanity_check!).and_raise(described_class::SanityCheckError) + end + end + + let!(:projects) { create_list(:project, 10, :with_avatar) } + let(:uploads) { Upload.all } + let(:model_class) { Project } + let(:mounted_as) { :avatar } + let(:to_store) { ObjectStorage::Store::REMOTE } + + before do + stub_uploads_object_storage(AvatarUploader) + end + + describe '.enqueue!' do + def enqueue! + described_class.enqueue!(uploads, Project, mounted_as, to_store) + end + + it 'is guarded by .sanity_check!' do + expect(described_class).to receive(:perform_async) + expect(described_class).to receive(:sanity_check!) + + enqueue! + end + + context 'sanity_check! fails' do + include_context 'sanity_check! fails' + + it 'does not enqueue a job' do + expect(described_class).not_to receive(:perform_async) + + expect { enqueue! }.to raise_error(described_class::SanityCheckError) + end + end + end + + describe '.sanity_check!' do + shared_examples 'raises a SanityCheckError' do + let(:mount_point) { nil } + + it do + expect { described_class.sanity_check!(uploads, model_class, mount_point) } + .to raise_error(described_class::SanityCheckError) + end + end + + context 'uploader types mismatch' do + let!(:outlier) { create(:upload, uploader: 'FileUploader') } + + include_examples 'raises a SanityCheckError' + end + + context 'model types mismatch' do + let!(:outlier) { create(:upload, model_type: 'Potato') } + + include_examples 'raises a SanityCheckError' + end + + context 'mount point not found' do + include_examples 'raises a SanityCheckError' do + let(:mount_point) { :potato } + end + end + end + + describe '#perform' do + def perform + described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store) + rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures + # swallow + end + + shared_examples 'outputs correctly' do |success: 0, failures: 0| + total = success + failures + + if success > 0 + it 'outputs the reports' do + expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files}) + + perform + end + end + + if failures > 0 + it 'outputs upload failures' do + expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/) + + perform + end + end + end + + it_behaves_like 'outputs correctly', success: 10 + + it 'migrates files' do + perform + + aggregate_failures do + projects.each do |project| + expect(project.reload.avatar.upload.local?).to be_falsey + end + end + end + + context 'migration is unsuccessful' do + before do + allow_any_instance_of(ObjectStorage::Concern).to receive(:migrate!).and_raise(CarrierWave::UploadError, "I am a teapot.") + end + + it_behaves_like 'outputs correctly', failures: 10 + end + end +end diff --git a/spec/workers/project_export_worker_spec.rb b/spec/workers/project_export_worker_spec.rb new file mode 100644 index 00000000000..8899969c178 --- /dev/null +++ b/spec/workers/project_export_worker_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe ProjectExportWorker do + let!(:user) { create(:user) } + let!(:project) { create(:project) } + + subject { described_class.new } + + describe '#perform' do + context 'when it succeeds' do + it 'calls the ExportService' do + expect_any_instance_of(::Projects::ImportExport::ExportService).to receive(:execute) + + subject.perform(user.id, project.id, { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' }) + end + end + + context 'when it fails' do + it 'raises an exception when params are invalid' do + expect_any_instance_of(::Projects::ImportExport::ExportService).not_to receive(:execute) + + expect { subject.perform(1234, project.id, {}) }.to raise_exception(ActiveRecord::RecordNotFound) + expect { subject.perform(user.id, 1234, {}) }.to raise_exception(ActiveRecord::RecordNotFound) + expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.to raise_exception(Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError) + end + end + end +end |