diff options
Diffstat (limited to 'spec')
66 files changed, 1507 insertions, 625 deletions
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index 149b690ff70..8c10ea53a7a 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -2,19 +2,12 @@ require 'spec_helper' describe Oauth::AuthorizationsController do let(:user) { create(:user) } - - let(:doorkeeper) do - Doorkeeper::Application.create( - name: "MyApp", - redirect_uri: 'http://example.com', - scopes: "") - end - + let!(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') } let(:params) do { response_type: "code", - client_id: doorkeeper.uid, - redirect_uri: doorkeeper.redirect_uri, + client_id: application.uid, + redirect_uri: application.redirect_uri, state: 'state' } end @@ -44,7 +37,7 @@ describe Oauth::AuthorizationsController do end it 'deletes session.user_return_to and redirects when skip authorization' do - doorkeeper.update(trusted: true) + application.update(trusted: true) request.session['user_return_to'] = 'http://example.com' get :new, params @@ -52,6 +45,25 @@ describe Oauth::AuthorizationsController do expect(request.session['user_return_to']).to be_nil expect(response).to have_gitlab_http_status(302) end + + context 'when there is already an access token for the application' do + context 'when the request scope matches any of the created token scopes' do + before do + scopes = Doorkeeper::OAuth::Scopes.from_string('api') + + allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes) + + create :oauth_access_token, application: application, resource_owner_id: user.id, scopes: scopes + end + + it 'authorizes the request and redirects' do + get :new, params + + expect(request.session['user_return_to']).to be_nil + expect(response).to have_gitlab_http_status(302) + end + end + end end end end diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb index 02b30f9bc6d..b1d83246238 100644 --- a/spec/controllers/projects/milestones_controller_spec.rb +++ b/spec/controllers/projects/milestones_controller_spec.rb @@ -124,7 +124,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 <a href=\"#{group_milestone_path(project.group, milestone.iid)}\">group milestone</a>.") + expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\"><u>group milestone</u></a>.") expect(response).to redirect_to(project_milestones_path(project)) end end diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb index 04217fec06c..5828d833ae9 100644 --- a/spec/features/groups/empty_states_spec.rb +++ b/spec/features/groups/empty_states_spec.rb @@ -59,6 +59,18 @@ feature 'Group empty states' do end end + shared_examples "no projects" do + it 'displays an empty state' do + expect(page).to have_selector('.empty-state') + end + + it "does not show a new #{issuable_name} button" do + within '.empty-state' do + expect(page).not_to have_link("create #{issuable_name}") + end + end + end + context 'group without a project' do context 'group has a subgroup', :nested_groups do let(:subgroup) { create(:group, parent: group) } @@ -92,16 +104,18 @@ feature 'Group empty states' do visit path end - it 'displays an empty state' do - expect(page).to have_selector('.empty-state') - end + it_behaves_like "no projects" + end + end - it "shows a new #{issuable_name} button" do - within '.empty-state' do - expect(page).not_to have_link("create #{issuable_name}") - end - end + context 'group has only a project with issues disabled' do + let(:project_with_issues_disabled) { create(:empty_project, :issues_disabled, group: group) } + + before do + visit path end + + it_behaves_like "no projects" end end end diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb index 111a24c0d94..e131ded3688 100644 --- a/spec/features/groups/issues_spec.rb +++ b/spec/features/groups/issues_spec.rb @@ -5,6 +5,7 @@ feature 'Group issues page' do let(:group) { create(:group) } let(:project) { create(:project, :public, group: group)} + let(:project_with_issues_disabled) { create(:project, :issues_disabled, group: group) } let(:path) { issues_group_path(group) } context 'with shared examples' do @@ -76,4 +77,25 @@ feature 'Group issues page' do end end end + + context 'projects with issues disabled' do + describe 'issue dropdown' do + let(:user_in_group) { create(:group_member, :master, user: create(:user), group: group ).user } + + before do + [project, project_with_issues_disabled].each { |project| project.add_master(user_in_group) } + sign_in(user_in_group) + visit issues_group_path(group) + end + + it 'shows projects only with issues feature enabled', :js do + find('.new-project-item-link').click + + page.within('.select2-results') do + expect(page).to have_content(project.full_name) + expect(page).not_to have_content(project_with_issues_disabled.full_name) + end + end + end + end end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 672ae785c2d..921a447f6ee 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -56,4 +56,21 @@ feature 'Group merge requests page' do expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name) end end + + describe 'new merge request dropdown' do + let(:project_with_merge_requests_disabled) { create(:project, :merge_requests_disabled, group: group) } + + before do + visit path + end + + it 'shows projects only with merge requests feature enabled', :js do + find('.new-project-item-link').click + + page.within('.select2-results') do + expect(page).to have_content(project.name_with_namespace) + expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace) + end + end + end end diff --git a/spec/features/groups/milestone_spec.rb b/spec/features/groups/milestone_spec.rb index 20337f1d3b0..2108d763028 100644 --- a/spec/features/groups/milestone_spec.rb +++ b/spec/features/groups/milestone_spec.rb @@ -107,19 +107,6 @@ feature 'Group milestones' do expect(page).to have_selector("#milestone_#{legacy_milestone.milestones.first.id}", count: 1) end - it 'updates milestone' do - page.within(".milestones #milestone_#{active_group_milestone.id}") do - click_link('Edit') - end - - page.within('.milestone-form') do - fill_in 'milestone_title', with: 'new title' - click_button('Update milestone') - end - - expect(find('#content-body h2')).to have_content('new title') - end - it 'shows milestone detail and supports its edit' do page.within(".milestones #milestone_#{active_group_milestone.id}") do click_link(active_group_milestone.title) diff --git a/spec/features/ics/dashboard_issues_spec.rb b/spec/features/ics/dashboard_issues_spec.rb index 90d02f7e40f..a4d05c25a90 100644 --- a/spec/features/ics/dashboard_issues_spec.rb +++ b/spec/features/ics/dashboard_issues_spec.rb @@ -5,6 +5,7 @@ describe 'Dashboard Issues Calendar Feed' do let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') } let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') } let!(:project) { create(:project) } + let(:milestone) { create(:milestone, project_id: project.id, title: 'v1.0') } before do project.add_master(user) @@ -14,7 +15,9 @@ describe 'Dashboard Issues Calendar Feed' do context 'with no referer' do it 'renders calendar feed' do sign_in user - visit issues_dashboard_path(:ics) + visit issues_dashboard_path(:ics, + due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name, + sort: 'closest_future_date') expect(response_headers['Content-Type']).to have_content('text/calendar') expect(body).to have_text('BEGIN:VCALENDAR') @@ -25,19 +28,37 @@ describe 'Dashboard Issues Calendar Feed' do it 'renders calendar feed as text/plain' do sign_in user page.driver.header('Referer', issues_dashboard_url(host: Settings.gitlab.base_url)) - visit issues_dashboard_path(:ics) + visit issues_dashboard_path(:ics, + due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name, + sort: 'closest_future_date') expect(response_headers['Content-Type']).to have_content('text/plain') expect(body).to have_text('BEGIN:VCALENDAR') end end + + context 'when filtered by milestone' do + it 'renders calendar feed' do + sign_in user + visit issues_dashboard_path(:ics, + due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name, + sort: 'closest_future_date', + milestone_title: milestone.title) + + expect(response_headers['Content-Type']).to have_content('text/calendar') + expect(body).to have_text('BEGIN:VCALENDAR') + end + end end context 'when authenticated via personal access token' do it 'renders calendar feed' do personal_access_token = create(:personal_access_token, user: user) - visit issues_dashboard_path(:ics, private_token: personal_access_token.token) + visit issues_dashboard_path(:ics, + due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name, + sort: 'closest_future_date', + private_token: personal_access_token.token) expect(response_headers['Content-Type']).to have_content('text/calendar') expect(body).to have_text('BEGIN:VCALENDAR') @@ -46,7 +67,10 @@ describe 'Dashboard Issues Calendar Feed' do context 'when authenticated via feed token' do it 'renders calendar feed' do - visit issues_dashboard_path(:ics, feed_token: user.feed_token) + visit issues_dashboard_path(:ics, + due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name, + sort: 'closest_future_date', + feed_token: user.feed_token) expect(response_headers['Content-Type']).to have_content('text/calendar') expect(body).to have_text('BEGIN:VCALENDAR') @@ -60,7 +84,10 @@ describe 'Dashboard Issues Calendar Feed' do end it 'renders issue fields' do - visit issues_dashboard_path(:ics, feed_token: user.feed_token) + visit issues_dashboard_path(:ics, + due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name, + sort: 'closest_future_date', + feed_token: user.feed_token) expect(body).to have_text("SUMMARY:test title (in #{project.full_path})") # line length for ics is 75 chars diff --git a/spec/features/milestones/user_deletes_milestone_spec.rb b/spec/features/milestones/user_deletes_milestone_spec.rb index 414702daba4..9d4a68239d3 100644 --- a/spec/features/milestones/user_deletes_milestone_spec.rb +++ b/spec/features/milestones/user_deletes_milestone_spec.rb @@ -13,6 +13,7 @@ describe "User deletes milestone", :js do end it "deletes milestone" do + click_link(milestone.title) click_button("Delete") click_button("Delete milestone") diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index c85b82b2090..3db384e5b65 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -157,6 +157,19 @@ feature 'Gcp Cluster', :js do end end + context 'when a user cannot edit the environment scope' do + before do + visit project_clusters_path(project) + + click_link 'Add Kubernetes cluster' + click_link 'Add an existing Kubernetes cluster' + end + + it 'user does not see the "Environment scope" field' do + expect(page).not_to have_css('#cluster_environment_scope') + end + end + context 'when user has not dismissed GCP signup offer' do before do visit project_clusters_path(project) diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex 72ab2d71f35..ceba4dfec57 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index c8a43ddf410..669ec602f11 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -19,7 +19,7 @@ describe MergeRequestsFinder do let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') } - let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) } + let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked') } let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) } let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) } @@ -35,7 +35,7 @@ describe MergeRequestsFinder do it 'filters by scope' do params = { scope: 'authored', state: 'opened' } merge_requests = described_class.new(user, params).execute - expect(merge_requests.size).to eq(4) + expect(merge_requests.size).to eq(3) end it 'filters by project' do @@ -90,6 +90,14 @@ describe MergeRequestsFinder do expect(merge_requests).to contain_exactly(merge_request2) end + it 'filters by state' do + params = { state: 'locked' } + + merge_requests = described_class.new(user, params).execute + + expect(merge_requests).to contain_exactly(merge_request3) + end + context 'filtering by group milestone' do let!(:group) { create(:group, :public) } let(:group_milestone) { create(:milestone, group: group) } @@ -199,7 +207,7 @@ describe MergeRequestsFinder do it 'returns the number of rows for the default state' do finder = described_class.new(user) - expect(finder.row_count).to eq(4) + expect(finder.row_count).to eq(3) end it 'returns the number of rows for a given state' do diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz Binary files differdeleted file mode 100644 index bef7e2ff8ee..00000000000 --- a/spec/fixtures/exported-project.gz +++ /dev/null diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 3008528e60c..885204062fe 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -54,7 +54,7 @@ describe MergeRequestsHelper do let(:options) { { force_link: true } } it 'removes the data-toggle attributes' do - is_expected.not_to match(/data-toggle="tab"/) + is_expected.not_to match(/data-toggle="tabvue"/) end end end diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js index c53b6da4b48..54cb6d84109 100644 --- a/spec/javascripts/api_spec.js +++ b/spec/javascripts/api_spec.js @@ -242,7 +242,7 @@ describe('Api', () => { }, ]); - Api.groupProjects(groupId, query, response => { + Api.groupProjects(groupId, query, {}, response => { expect(response.length).toBe(1); expect(response[0].name).toBe('test'); done(); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index f0bd098f698..6829c1e956a 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -5,7 +5,6 @@ import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE, } from '~/diffs/constants'; -import store from '~/diffs/store'; import * as actions from '~/diffs/store/actions'; import * as types from '~/diffs/store/mutation_types'; import axios from '~/lib/utils/axios_utils'; @@ -28,22 +27,6 @@ describe('DiffsStoreActions', () => { }); }); - describe('setLoadingState', () => { - it('should set loading state', done => { - expect(store.state.diffs.isLoading).toEqual(true); - const loadingState = false; - - testAction( - actions.setLoadingState, - loadingState, - {}, - [{ type: types.SET_LOADING, payload: loadingState }], - [], - done, - ); - }); - }); - describe('fetchDiffFiles', () => { it('should fetch diff files', done => { const endpoint = '/fetch/diff/files'; diff --git a/spec/javascripts/helpers/init_vue_mr_page_helper.js b/spec/javascripts/helpers/init_vue_mr_page_helper.js index 05c6d587e9c..fc4288eb15b 100644 --- a/spec/javascripts/helpers/init_vue_mr_page_helper.js +++ b/spec/javascripts/helpers/init_vue_mr_page_helper.js @@ -5,12 +5,16 @@ import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_dat import diffFileMockData from '../diffs/mock_data/diff_file'; export default function initVueMRPage() { + const mrTestEl = document.createElement('div'); + mrTestEl.className = 'js-merge-request-test'; + document.body.appendChild(mrTestEl); + const diffsAppEndpoint = '/diffs/app/endpoint'; const diffsAppProjectPath = 'testproject'; const mrEl = document.createElement('div'); mrEl.className = 'merge-request fixture-mr'; mrEl.setAttribute('data-mr-action', 'diffs'); - document.body.appendChild(mrEl); + mrTestEl.appendChild(mrEl); const mrDiscussionsEl = document.createElement('div'); mrDiscussionsEl.id = 'js-vue-mr-discussions'; @@ -18,18 +22,18 @@ export default function initVueMRPage() { mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock)); mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock)); mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request'); - document.body.appendChild(mrDiscussionsEl); + mrTestEl.appendChild(mrDiscussionsEl); const discussionCounterEl = document.createElement('div'); discussionCounterEl.id = 'js-vue-discussion-counter'; - document.body.appendChild(discussionCounterEl); + mrTestEl.appendChild(discussionCounterEl); const diffsAppEl = document.createElement('div'); diffsAppEl.id = 'js-diffs-app'; diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint); diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath); diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock)); - document.body.appendChild(diffsAppEl); + mrTestEl.appendChild(diffsAppEl); const mock = new MockAdapter(axios); mock.onGet(diffsAppEndpoint).reply(200, { diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js index 70d13a6ceb2..430e8e2baa3 100644 --- a/spec/javascripts/ide/components/error_message_spec.js +++ b/spec/javascripts/ide/components/error_message_spec.js @@ -36,13 +36,15 @@ describe('IDE error message component', () => { }); describe('with action', () => { + let actionSpy; + beforeEach(done => { - vm.message.action = 'testAction'; + actionSpy = jasmine.createSpy('action').and.returnValue(Promise.resolve()); + + vm.message.action = actionSpy; vm.message.actionText = 'test action'; vm.message.actionPayload = 'testActionPayload'; - spyOn(vm.$store, 'dispatch').and.returnValue(Promise.resolve()); - vm.$nextTick(done); }); @@ -63,7 +65,7 @@ describe('IDE error message component', () => { vm.$el.querySelector('.flash-action').click(); vm.$nextTick(() => { - expect(vm.$store.dispatch).toHaveBeenCalledWith('testAction', 'testActionPayload'); + expect(actionSpy).toHaveBeenCalledWith('testActionPayload'); done(); }); @@ -74,7 +76,7 @@ describe('IDE error message component', () => { vm.$el.querySelector('.flash-action').click(); - expect(vm.$store.dispatch).not.toHaveBeenCalledWith(); + expect(actionSpy).not.toHaveBeenCalledWith(); }); it('resets isLoading after click', done => { diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index 6bf309fb4bf..30cd92b2ca4 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -1,6 +1,5 @@ import Vue from 'vue'; import store from '~/ide/stores'; -import service from '~/ide/services'; import router from '~/ide/ide_router'; import repoCommitSection from '~/ide/components/repo_commit_section.vue'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; @@ -68,23 +67,6 @@ describe('RepoCommitSection', () => { vm.$mount(); - spyOn(service, 'getTreeData').and.returnValue( - Promise.resolve({ - headers: { - 'page-title': 'test', - }, - json: () => - Promise.resolve({ - last_commit_path: 'last_commit_path', - parent_tree_url: 'parent_tree_url', - path: '/', - trees: [{ name: 'tree' }], - blobs: [{ name: 'blob' }], - submodules: [{ name: 'submodule' }], - }), - }), - ); - Vue.nextTick(done); }); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js index 5746683917e..58d3ffc6d94 100644 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ b/spec/javascripts/ide/stores/actions/file_spec.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; import * as actions from '~/ide/stores/actions/file'; import * as types from '~/ide/stores/mutation_types'; @@ -9,11 +11,16 @@ import { file, resetStore } from '../../helpers'; import testAction from '../../../helpers/vuex_action_helper'; describe('IDE store file actions', () => { + let mock; + beforeEach(() => { + mock = new MockAdapter(axios); + spyOn(router, 'push'); }); afterEach(() => { + mock.restore(); resetStore(store); }); @@ -183,94 +190,125 @@ describe('IDE store file actions', () => { let localFile; beforeEach(() => { - spyOn(service, 'getFileData').and.returnValue( - Promise.resolve({ - headers: { - 'page-title': 'testing getFileData', - }, - json: () => - Promise.resolve({ - blame_path: 'blame_path', - commits_path: 'commits_path', - permalink: 'permalink', - raw_path: 'raw_path', - binary: false, - html: '123', - render_error: '', - }), - }), - ); + spyOn(service, 'getFileData').and.callThrough(); localFile = file(`newCreate-${Math.random()}`); - localFile.url = 'getFileDataURL'; + localFile.url = `${gl.TEST_HOST}/getFileDataURL`; store.state.entries[localFile.path] = localFile; }); - it('calls the service', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL'); + describe('success', () => { + beforeEach(() => { + mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).replyOnce( + 200, + { + blame_path: 'blame_path', + commits_path: 'commits_path', + permalink: 'permalink', + raw_path: 'raw_path', + binary: false, + html: '123', + render_error: '', + }, + { + 'page-title': 'testing getFileData', + }, + ); + }); - done(); - }) - .catch(done.fail); - }); + it('calls the service', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(service.getFileData).toHaveBeenCalledWith(`${gl.TEST_HOST}/getFileDataURL`); - it('sets the file data', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(localFile.blamePath).toBe('blame_path'); + done(); + }) + .catch(done.fail); + }); - done(); - }) - .catch(done.fail); - }); + it('sets the file data', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(localFile.blamePath).toBe('blame_path'); - it('sets document title', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(document.title).toBe('testing getFileData'); + done(); + }) + .catch(done.fail); + }); - done(); - }) - .catch(done.fail); - }); + it('sets document title', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(document.title).toBe('testing getFileData'); - it('sets the file as active', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(localFile.active).toBeTruthy(); + done(); + }) + .catch(done.fail); + }); - done(); - }) - .catch(done.fail); - }); + it('sets the file as active', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(localFile.active).toBeTruthy(); - 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); + }); - done(); - }) - .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', { path: localFile.path }) + .then(() => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].name).toBe(localFile.name); + + done(); + }) + .catch(done.fail); + }); }); - it('adds the file to open files', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].name).toBe(localFile.name); + describe('error', () => { + beforeEach(() => { + mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).networkError(); + }); - done(); - }) - .catch(done.fail); + it('dispatches error action', done => { + const dispatch = jasmine.createSpy('dispatch'); + + actions + .getFileData({ state: store.state, commit() {}, dispatch }, { path: localFile.path }) + .then(() => { + expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { + text: 'An error occured whilst loading the file.', + action: jasmine.any(Function), + actionText: 'Please try again', + actionPayload: { + path: localFile.path, + makeFileActive: true, + }, + }); + + done(); + }) + .catch(done.fail); + }); }); }); @@ -278,48 +316,84 @@ describe('IDE store file actions', () => { let tmpFile; beforeEach(() => { - spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw')); + spyOn(service, 'getRawFileData').and.callThrough(); tmpFile = file('tmpFile'); store.state.entries[tmpFile.path] = tmpFile; }); - it('calls getRawFileData service method', done => { - store - .dispatch('getRawFileData', { path: tmpFile.path }) - .then(() => { - expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); + describe('success', () => { + beforeEach(() => { + mock.onGet(/(.*)/).replyOnce(200, 'raw'); + }); - done(); - }) - .catch(done.fail); - }); + it('calls getRawFileData service method', done => { + store + .dispatch('getRawFileData', { path: tmpFile.path }) + .then(() => { + expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); - it('updates file raw data', done => { - store - .dispatch('getRawFileData', { path: tmpFile.path }) - .then(() => { - expect(tmpFile.raw).toBe('raw'); + done(); + }) + .catch(done.fail); + }); - done(); - }) - .catch(done.fail); - }); + it('updates file raw data', done => { + store + .dispatch('getRawFileData', { path: tmpFile.path }) + .then(() => { + expect(tmpFile.raw).toBe('raw'); - it('calls also getBaseRawFileData service method', done => { - spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw')); + done(); + }) + .catch(done.fail); + }); - tmpFile.mrChange = { new_file: false }; + it('calls also getBaseRawFileData service method', done => { + spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw')); - store - .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' }) - .then(() => { - expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA'); - expect(tmpFile.baseRaw).toBe('baseraw'); + tmpFile.mrChange = { new_file: false }; - done(); - }) - .catch(done.fail); + 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('error', () => { + beforeEach(() => { + mock.onGet(/(.*)/).networkError(); + }); + + it('dispatches error action', done => { + const dispatch = jasmine.createSpy('dispatch'); + + actions + .getRawFileData( + { state: store.state, commit() {}, dispatch }, + { path: tmpFile.path, baseSha: tmpFile.baseSha }, + ) + .then(done.fail) + .catch(() => { + expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { + text: 'An error occured whilst loading the file content.', + action: jasmine.any(Function), + actionText: 'Please try again', + actionPayload: { + path: tmpFile.path, + baseSha: tmpFile.baseSha, + }, + }); + + done(); + }); + }); }); }); diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js index b4ec4a0b173..c99ccc70c6a 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js @@ -1,110 +1,239 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; +import { + getMergeRequestData, + getMergeRequestChanges, + getMergeRequestVersions, +} from '~/ide/stores/actions/merge_request'; import service from '~/ide/services'; import { resetStore } from '../../helpers'; describe('IDE store merge request actions', () => { + let mock; + beforeEach(() => { + mock = new MockAdapter(axios); + store.state.projects.abcproject = { mergeRequests: {}, }; }); afterEach(() => { + mock.restore(); resetStore(store); }); describe('getMergeRequestData', () => { - beforeEach(() => { - spyOn(service, 'getProjectMergeRequestData').and.returnValue( - Promise.resolve({ data: { title: 'mergerequest' } }), - ); + describe('success', () => { + beforeEach(() => { + spyOn(service, 'getProjectMergeRequestData').and.callThrough(); + + mock + .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/) + .reply(200, { 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); + }); }); - 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('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/).networkError(); + }); + + it('dispatches error action', done => { + const dispatch = jasmine.createSpy('dispatch'); + + getMergeRequestData( + { + commit() {}, + dispatch, + state: store.state, + }, + { projectId: 'abcproject', mergeRequestId: 1 }, + ) + .then(done.fail) + .catch(() => { + expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { + text: 'An error occured whilst loading the merge request.', + action: jasmine.any(Function), + actionText: 'Please try again', + actionPayload: { + projectId: 'abcproject', + mergeRequestId: 1, + force: false, + }, + }); + + done(); + }); + }); }); }); 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); + describe('success', () => { + beforeEach(() => { + spyOn(service, 'getProjectMergeRequestChanges').and.callThrough(); + + mock + .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/) + .reply(200, { title: 'mergerequest' }); + }); + + 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); + }); }); - 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('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/).networkError(); + }); + + it('dispatches error action', done => { + const dispatch = jasmine.createSpy('dispatch'); + + getMergeRequestChanges( + { + commit() {}, + dispatch, + state: store.state, + }, + { projectId: 'abcproject', mergeRequestId: 1 }, + ) + .then(done.fail) + .catch(() => { + expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { + text: 'An error occured whilst loading the merge request changes.', + action: jasmine.any(Function), + actionText: 'Please try again', + actionPayload: { + projectId: 'abcproject', + mergeRequestId: 1, + force: false, + }, + }); + + done(); + }); + }); }); }); 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); + describe('success', () => { + beforeEach(() => { + mock + .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/) + .reply(200, [{ id: 789 }]); + spyOn(service, 'getProjectMergeRequestVersions').and.callThrough(); + }); + + 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); + }); }); - 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); + describe('error', () => { + beforeEach(() => { + mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/).networkError(); + }); + + it('dispatches error action', done => { + const dispatch = jasmine.createSpy('dispatch'); + + getMergeRequestVersions( + { + commit() {}, + dispatch, + state: store.state, + }, + { projectId: 'abcproject', mergeRequestId: 1 }, + ) + .then(done.fail) + .catch(() => { + expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { + text: 'An error occured whilst loading the merge request version data.', + action: jasmine.any(Function), + actionText: 'Please try again', + actionPayload: { + projectId: 'abcproject', + mergeRequestId: 1, + force: false, + }, + }); + + done(); + }); + }); }); }); }); diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js index c1f9fc4f148..ca79edafb7e 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/javascripts/ide/stores/actions/project_spec.js @@ -110,7 +110,7 @@ describe('IDE store project actions', () => { type: 'setErrorMessage', payload: { text: "Branch <strong>master</strong> was not found in this project's repository.", - action: 'createNewBranchFromDefault', + action: jasmine.any(Function), actionText: 'Create branch', actionPayload: 'master', }, diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js index 612a439cfbf..6860e6cdb91 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/javascripts/ide/stores/actions/tree_spec.js @@ -1,5 +1,4 @@ import MockAdapter from 'axios-mock-adapter'; -import Vue from 'vue'; import testAction from 'spec/helpers/vuex_action_helper'; import { showTreeEntry, getFiles } from '~/ide/stores/actions/tree'; import * as types from '~/ide/stores/mutation_types'; @@ -117,6 +116,40 @@ describe('Multi-file store tree actions', () => { done(); }); }); + + it('dispatches error action', done => { + const dispatch = jasmine.createSpy('dispatchSpy'); + + store.state.projects = { + 'abc/def': { + web_url: `${gl.TEST_HOST}/files`, + }, + }; + + mock.onGet(/(.*)/).replyOnce(500); + + getFiles( + { + commit() {}, + dispatch, + state: store.state, + }, + { + projectId: 'abc/def', + branchId: 'master-testing', + }, + ) + .then(done.fail) + .catch(() => { + expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { + text: 'An error occured whilst loading all the files.', + action: jasmine.any(Function), + actionText: 'Please try again', + actionPayload: { projectId: 'abc/def', branchId: 'master-testing' }, + }); + done(); + }); + }); }); }); @@ -168,72 +201,4 @@ describe('Multi-file store tree actions', () => { ); }); }); - - describe('getLastCommitData', () => { - beforeEach(() => { - spyOn(service, 'getTreeLastCommit').and.returnValue( - Promise.resolve({ - headers: { - 'more-logs-url': null, - }, - json: () => - Promise.resolve([ - { - type: 'tree', - file_name: 'testing', - commit: { - message: 'commit message', - authored_date: '123', - }, - }, - ]), - }), - ); - - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - - projectTree = store.state.trees['abcproject/mybranch']; - projectTree.tree.push(file('testing', '1', 'tree')); - projectTree.lastCommitPath = 'lastcommitpath'; - }); - - it('calls service with lastCommitPath', done => { - store - .dispatch('getLastCommitData', projectTree) - .then(() => { - expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath'); - - done(); - }) - .catch(done.fail); - }); - - it('updates trees last commit data', done => { - store - .dispatch('getLastCommitData', projectTree) - .then(Vue.nextTick) - .then(() => { - expect(projectTree.tree[0].lastCommit.message).toBe('commit message'); - - done(); - }) - .catch(done.fail); - }); - - it('does not update entry if not found', done => { - projectTree.tree[0].name = 'a'; - - store - .dispatch('getLastCommitData', projectTree) - .then(Vue.nextTick) - .then(() => { - expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message'); - - done(); - }) - .catch(done.fail); - }); - }); }); diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 22eb0ad7143..7502f1fa2e1 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -19,9 +19,11 @@ import IssuablesHelper from '~/helpers/issuables_helper'; spyOn(axios, 'patch').and.callThrough(); mock = new MockAdapter(axios); - mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {}); + mock + .onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`) + .reply(200, {}); - return this.merge = new MergeRequest(); + return (this.merge = new MergeRequest()); }); afterEach(() => { @@ -32,17 +34,22 @@ import IssuablesHelper from '~/helpers/issuables_helper'; spyOn($, 'ajax').and.stub(); const changeEvent = document.createEvent('HTMLEvents'); changeEvent.initEvent('change', true, true); - $('input[type=checkbox]').attr('checked', true)[0].dispatchEvent(changeEvent); + $('input[type=checkbox]') + .attr('checked', true)[0] + .dispatchEvent(changeEvent); return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - it('submits an ajax request on tasklist:changed', (done) => { + it('submits an ajax request on tasklist:changed', done => { $('.js-task-list-field').trigger('tasklist:changed'); setTimeout(() => { - expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, { - merge_request: { description: '- [ ] Task List Item' }, - }); + expect(axios.patch).toHaveBeenCalledWith( + `${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, + { + merge_request: { description: '- [ ] Task List Item' }, + }, + ); done(); }); }); @@ -119,4 +126,4 @@ import IssuablesHelper from '~/helpers/issuables_helper'; }); }); }); -}).call(window); +}.call(window)); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 08928e13985..7251ce19a90 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -40,6 +40,7 @@ describe('MergeRequestTabs', function() { this.class.unbindEvents(); this.class.destroyPipelinesView(); mrPageMock.restore(); + $('.js-merge-request-test').remove(); }); describe('opensInNewTab', function() { diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index 985c2f81ef3..71ef3aa9b03 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -291,4 +291,17 @@ describe('Actions Notes Store', () => { .catch(done.fail); }); }); + + describe('setNotesFetchedState', () => { + it('should set notes fetched state', done => { + testAction( + actions.setNotesFetchedState, + true, + {}, + [{ type: 'SET_NOTES_FETCHED_STATE', payload: true }], + [], + done, + ); + }); + }); }); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js index 5501e50e97b..815cc09621f 100644 --- a/spec/javascripts/notes/stores/getters_spec.js +++ b/spec/javascripts/notes/stores/getters_spec.js @@ -15,6 +15,7 @@ describe('Getters Notes Store', () => { discussions: [individualNote], targetNoteHash: 'hash', lastFetchedAt: 'timestamp', + isNotesFetched: false, notesData: notesDataMock, userData: userDataMock, @@ -84,4 +85,10 @@ describe('Getters Notes Store', () => { expect(getters.openState(state)).toEqual(noteableDataMock.state); }); }); + + describe('isNotesFetched', () => { + it('should return the state for the fetching notes', () => { + expect(getters.isNotesFetched(state)).toBeFalsy(); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index 556a1c244c0..ccc7328447b 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -318,4 +318,15 @@ describe('Notes Store mutations', () => { expect(state.isToggleStateButtonLoading).toEqual(false); }); }); + + describe('SET_NOTES_FETCHING_STATE', () => { + it('should set the given state', () => { + const state = { + isNotesFetched: false, + }; + + mutations.SET_NOTES_FETCHED_STATE(state, true); + expect(state.isNotesFetched).toEqual(true); + }); + }); }); diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index 073dae56c25..9c55a19ebc7 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -135,4 +135,34 @@ describe('pipeline graph job component', () => { expect(component.$el.querySelector('.js-job-component-tooltip').getAttribute('data-original-title')).toEqual('test - success'); }); }); + + describe('tooltip placement', () => { + const tooltipBoundary = 'a[data-boundary="viewport"]'; + + it('does not set tooltip boundary by default', () => { + component = mountComponent(JobComponent, { + job: mockJob, + }); + + expect(component.$el.querySelector(tooltipBoundary)).toBeNull(); + }); + + it('sets tooltip boundary to viewport for small dropdowns', () => { + component = mountComponent(JobComponent, { + job: mockJob, + dropdownLength: 1, + }); + + expect(component.$el.querySelector(tooltipBoundary)).not.toBeNull(); + }); + + it('does not set tooltip boundary for large lists', () => { + component = mountComponent(JobComponent, { + job: mockJob, + dropdownLength: 7, + }); + + expect(component.$el.querySelector(tooltipBoundary)).toBeNull(); + }); + }); }); diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb index 10910f22d4a..85a4619e33d 100644 --- a/spec/lib/banzai/filter/emoji_filter_spec.rb +++ b/spec/lib/banzai/filter/emoji_filter_spec.rb @@ -3,15 +3,6 @@ require 'spec_helper' describe Banzai::Filter::EmojiFilter do include FilterSpecHelper - before do - @original_asset_host = ActionController::Base.asset_host - ActionController::Base.asset_host = 'https://foo.com' - end - - after do - ActionController::Base.asset_host = @original_asset_host - end - it 'replaces supported name emoji' do doc = filter('<p>:heart:</p>') expect(doc.css('gl-emoji').first.text).to eq '❤' diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb new file mode 100644 index 00000000000..a251ab323d8 --- /dev/null +++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, schema: 20180619121030 do + describe '#perform' do + context 'when diff files can be deleted' do + let(:merge_request) { create(:merge_request, :merged) } + let(:merge_request_diff) do + merge_request.create_merge_request_diff + merge_request.merge_request_diffs.first + end + + it 'deletes all merge request diff files' do + expect { described_class.new.perform(merge_request_diff.id) } + .to change { merge_request_diff.merge_request_diff_files.count } + .from(20).to(0) + end + + it 'updates state to without_files' do + expect { described_class.new.perform(merge_request_diff.id) } + .to change { merge_request_diff.reload.state } + .from('collected').to('without_files') + end + + it 'rollsback if something goes wrong' do + expect(MergeRequestDiffFile).to receive_message_chain(:where, :delete_all) + .and_raise + + expect { described_class.new.perform(merge_request_diff.id) } + .to raise_error + + merge_request_diff.reload + + expect(merge_request_diff.state).to eq('collected') + expect(merge_request_diff.merge_request_diff_files.count).to eq(20) + end + end + + it 'deletes no merge request diff files when MR is not merged' do + merge_request = create(:merge_request, :opened) + merge_request.create_merge_request_diff + merge_request_diff = merge_request.merge_request_diffs.first + + expect { described_class.new.perform(merge_request_diff.id) } + .not_to change { merge_request_diff.merge_request_diff_files.count } + .from(20) + end + + it 'deletes no merge request diff files when diff is marked as "without_files"' do + merge_request = create(:merge_request, :merged) + merge_request.create_merge_request_diff + merge_request_diff = merge_request.merge_request_diffs.first + + merge_request_diff.clean! + + expect { described_class.new.perform(merge_request_diff.id) } + .not_to change { merge_request_diff.merge_request_diff_files.count } + .from(20) + end + + it 'deletes no merge request diff files when diff is the latest' do + merge_request = create(:merge_request, :merged) + merge_request_diff = merge_request.merge_request_diff + + expect { described_class.new.perform(merge_request_diff.id) } + .not_to change { merge_request_diff.merge_request_diff_files.count } + .from(20) + end + end +end diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb index 4f8412108ba..b236c1a9c49 100644 --- a/spec/lib/gitlab/data_builder/note_spec.rb +++ b/spec/lib/gitlab/data_builder/note_spec.rb @@ -52,7 +52,7 @@ describe Gitlab::DataBuilder::Note do expect(data[:issue].except('updated_at')) .to eq(issue.reload.hook_attrs.except('updated_at')) expect(data[:issue]['updated_at']) - .to be > issue.hook_attrs['updated_at'] + .to be >= issue.hook_attrs['updated_at'] end context 'with confidential issue' do @@ -84,7 +84,7 @@ describe Gitlab::DataBuilder::Note do expect(data[:merge_request].except('updated_at')) .to eq(merge_request.reload.hook_attrs.except('updated_at')) expect(data[:merge_request]['updated_at']) - .to be > merge_request.hook_attrs['updated_at'] + .to be >= merge_request.hook_attrs['updated_at'] end include_examples 'project hook data' @@ -107,7 +107,7 @@ describe Gitlab::DataBuilder::Note do expect(data[:merge_request].except('updated_at')) .to eq(merge_request.reload.hook_attrs.except('updated_at')) expect(data[:merge_request]['updated_at']) - .to be > merge_request.hook_attrs['updated_at'] + .to be >= merge_request.hook_attrs['updated_at'] end include_examples 'project hook data' @@ -130,7 +130,7 @@ describe Gitlab::DataBuilder::Note do expect(data[:snippet].except('updated_at')) .to eq(snippet.reload.hook_attrs.except('updated_at')) expect(data[:snippet]['updated_at']) - .to be > snippet.hook_attrs['updated_at'] + .to be >= snippet.hook_attrs['updated_at'] end include_examples 'project hook data' diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb index 122dcd9634c..68abcb3520a 100644 --- a/spec/lib/gitlab/favicon_spec.rb +++ b/spec/lib/gitlab/favicon_spec.rb @@ -32,7 +32,7 @@ RSpec.describe Gitlab::Favicon, :request_store do end it 'returns a full url when the asset host is configured' do - allow(Gitlab::Application.config).to receive(:asset_host).and_return('http://assets.local') + allow(ActionController::Base).to receive(:asset_host).and_return('http://assets.local') expect(described_class.main).to match %r{^http://localhost/assets/favicon-(?:\h+).png$} end end diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb index 44695acbe7d..51fad6c6838 100644 --- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb @@ -164,7 +164,7 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do Timecop.freeze do importer.update_repository - expect(project.last_repository_updated_at).to eq(Time.zone.now) + expect(project.last_repository_updated_at).to be_like_time(Time.zone.now) end end end diff --git a/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb new file mode 100644 index 00000000000..6a803c48b34 --- /dev/null +++ b/spec/lib/gitlab/import_export/group_project_object_builder_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::GroupProjectObjectBuilder do + let(:project) do + create(:project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + context 'labels' do + it 'finds the right group label' do + group_label = create(:group_label, 'name': 'group label', 'group': project.group) + + expect(described_class.build(Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group)).to eq(group_label) + end + + it 'creates a new label' do + label = described_class.build(Label, + 'title' => 'group label', + 'project' => project, + 'group' => project.group) + + expect(label.persisted?).to be true + end + end + + context 'milestones' do + it 'finds the right group milestone' do + milestone = create(:milestone, 'name' => 'group milestone', 'group' => project.group) + + expect(described_class.build(Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => project.group)).to eq(milestone) + end + + it 'creates a new milestone' do + milestone = described_class.build(Milestone, + 'title' => 'group milestone', + 'project' => project, + 'group' => project.group) + + expect(milestone.persisted?).to be true + end + end +end diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb index 991e354f499..c074e61da26 100644 --- a/spec/lib/gitlab/import_export/importer_spec.rb +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -4,14 +4,14 @@ describe Gitlab::ImportExport::Importer do let(:user) { create(:user) } let(:test_path) { "#{Dir.tmpdir}/importer_spec" } let(:shared) { project.import_export_shared } - let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) } + let(:project) { create(:project, import_source: File.join(test_path, 'test_project_export.tar.gz')) } subject(:importer) { described_class.new(project) } before do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) FileUtils.mkdir_p(shared.export_path) - FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path) + FileUtils.cp(Rails.root.join('spec/features/projects/import_export/test_project_export.tar.gz'), test_path) allow(subject).to receive(:remove_import_file) end diff --git a/spec/lib/gitlab/import_export/project.light.json b/spec/lib/gitlab/import_export/project.light.json index c13cf4a0507..ba2248073f5 100644 --- a/spec/lib/gitlab/import_export/project.light.json +++ b/spec/lib/gitlab/import_export/project.light.json @@ -7,7 +7,7 @@ "milestones": [ { "id": 1, - "title": "Project milestone", + "title": "A milestone", "project_id": 8, "description": "Project-level milestone", "due_date": null, @@ -66,8 +66,8 @@ "group_milestone_id": null, "milestone": { "id": 1, - "title": "Project milestone", - "project_id": 8, + "title": "A milestone", + "group_id": 8, "description": "Project-level milestone", "due_date": null, "created_at": "2016-06-14T15:02:04.415Z", @@ -86,7 +86,7 @@ "updated_at": "2017-08-15T18:37:40.795Z", "label": { "id": 6, - "title": "Another project label", + "title": "Another label", "color": "#A8D695", "project_id": null, "created_at": "2017-08-15T18:37:19.698Z", diff --git a/spec/lib/gitlab/import_export/project.milestone-iid.json b/spec/lib/gitlab/import_export/project.milestone-iid.json new file mode 100644 index 00000000000..b028147b5eb --- /dev/null +++ b/spec/lib/gitlab/import_export/project.milestone-iid.json @@ -0,0 +1,80 @@ +{ + "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", + "import_type": "gitlab_project", + "creator_id": 123, + "visibility_level": 10, + "archived": false, + "issues": [ + { + "id": 1, + "title": "Fugiat est minima quae maxime non similique.", + "assignee_id": null, + "project_id": 8, + "author_id": 1, + "created_at": "2017-07-07T18:13:01.138Z", + "updated_at": "2017-08-15T18:37:40.807Z", + "branch_name": null, + "description": "Quam totam fuga numquam in eveniet.", + "state": "opened", + "iid": 20, + "updated_by_id": 1, + "confidential": false, + "due_date": null, + "moved_to_id": null, + "lock_version": null, + "time_estimate": 0, + "closed_at": null, + "last_edited_at": null, + "last_edited_by_id": null, + "group_milestone_id": null, + "milestone": { + "id": 1, + "title": "Group-level milestone", + "description": "Group-level milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": 8 + } + }, + { + "id": 2, + "title": "est minima quae maxime non similique.", + "assignee_id": null, + "project_id": 8, + "author_id": 1, + "created_at": "2017-07-07T18:13:01.138Z", + "updated_at": "2017-08-15T18:37:40.807Z", + "branch_name": null, + "description": "Quam totam fuga numquam in eveniet.", + "state": "opened", + "iid": 21, + "updated_by_id": 1, + "confidential": false, + "due_date": null, + "moved_to_id": null, + "lock_version": null, + "time_estimate": 0, + "closed_at": null, + "last_edited_at": null, + "last_edited_by_id": null, + "group_milestone_id": null, + "milestone": { + "id": 2, + "title": "Another milestone", + "project_id": 8, + "description": "milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "group_id": null + } + } + ], + "snippets": [], + "hooks": [] +} diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 68ddc947e02..bac5693c830 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -189,8 +189,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do @project.pipelines.zip([2, 2, 2, 2, 2]) .each do |(pipeline, expected_status_size)| - expect(pipeline.statuses.size).to eq(expected_status_size) - end + expect(pipeline.statuses.size).to eq(expected_status_size) + end end end @@ -246,13 +246,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(project.issues.size).to eq(results.fetch(:issues, 0)) end - it 'has issue with group label and project label' do - labels = project.issues.first.labels - - expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0)) - expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0) - end - it 'does not set params that are excluded from import_export settings' do expect(project.import_type).to be_nil expect(project.creator_id).not_to eq 123 @@ -268,12 +261,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do it 'has group milestone' do expect(project.group.milestones.size).to eq(results.fetch(:milestones, 0)) end - - it 'has issue with group label' do - labels = project.issues.first.labels - - expect(labels.where(type: "GroupLabel").count).to eq(results.fetch(:first_issue_labels, 0)) - end end context 'Light JSON' do @@ -360,13 +347,72 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do it_behaves_like 'restores project correctly', issues: 2, labels: 1, - milestones: 1, + milestones: 2, first_issue_labels: 1 it_behaves_like 'restores group correctly', - labels: 1, - milestones: 1, + labels: 0, + milestones: 0, first_issue_labels: 1 end + + context 'with existing group models' do + let!(:project) do + create(:project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + before do + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.light.json") + end + + it 'imports labels' do + create(:group_label, name: 'Another label', group: project.group) + + expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) + + restored_project_json + + expect(project.labels.count).to eq(1) + end + + it 'imports milestones' do + create(:milestone, name: 'A milestone', group: project.group) + + expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) + + restored_project_json + + expect(project.group.milestones.count).to eq(1) + expect(project.milestones.count).to eq(0) + end + end + + context 'with clashing milestones on IID' do + let!(:project) do + create(:project, + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) + end + + it 'preserves the project milestone IID' do + project_tree_restorer.instance_variable_set(:@path, "spec/lib/gitlab/import_export/project.milestone-iid.json") + + expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) + + restored_project_json + + expect(project.milestones.count).to eq(2) + expect(Milestone.find_by_title('Another milestone').iid).to eq(1) + expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2) + end + end end end diff --git a/spec/lib/gitlab/repository_cache_adapter_spec.rb b/spec/lib/gitlab/repository_cache_adapter_spec.rb index 85971f2a7ef..5bd4d6c6a48 100644 --- a/spec/lib/gitlab/repository_cache_adapter_spec.rb +++ b/spec/lib/gitlab/repository_cache_adapter_spec.rb @@ -67,10 +67,18 @@ describe Gitlab::RepositoryCacheAdapter do describe '#expire_method_caches' do it 'expires the caches of the given methods' do - expect(cache).to receive(:expire).with(:readme) + expect(cache).to receive(:expire).with(:rendered_readme) expect(cache).to receive(:expire).with(:gitignore) - repository.expire_method_caches(%i(readme gitignore)) + repository.expire_method_caches(%i(rendered_readme gitignore)) + end + + it 'does not expire caches for non-existent methods' do + expect(cache).not_to receive(:expire).with(:nonexistent) + expect(Rails.logger).to( + receive(:error).with("Requested to expire non-existent method 'nonexistent' for Repository")) + + repository.expire_method_caches(%i(nonexistent)) end end end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index 5410bfbeb31..b7687d48c68 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Mattermost::Session, type: :request do + include ExclusiveLeaseHelpers + let(:user) { create(:user) } let(:gitlab_url) { "http://gitlab.com" } @@ -97,26 +99,20 @@ describe Mattermost::Session, type: :request do end end - context 'with lease' do - before do - allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') - end + context 'exclusive lease' do + let(:lease_key) { 'mattermost:session' } it 'tries to obtain a lease' do - expect(subject).to receive(:lease_try_obtain) - expect(Gitlab::ExclusiveLease).to receive(:cancel) + expect_to_obtain_exclusive_lease(lease_key, 'uuid') + expect_to_cancel_exclusive_lease(lease_key, 'uuid') # Cannot setup a session, but we should still cancel the lease expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) end - end - context 'without lease' do - before do - allow(subject).to receive(:lease_try_obtain).and_return(nil) - end + it 'returns a NoSessionError error without lease' do + stub_exclusive_lease_taken(lease_key) - it 'returns a NoSessionError error' do expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) end end diff --git a/spec/migrations/cleanup_stages_position_migration_spec.rb b/spec/migrations/cleanup_stages_position_migration_spec.rb new file mode 100644 index 00000000000..dde5a777487 --- /dev/null +++ b/spec/migrations/cleanup_stages_position_migration_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180604123514_cleanup_stages_position_migration.rb') + +describe CleanupStagesPositionMigration, :migration, :sidekiq, :redis do + let(:migration) { spy('migration') } + + before do + allow(Gitlab::BackgroundMigration::MigrateStageIndex) + .to receive(:new).and_return(migration) + end + + context 'when there are pending background migrations' do + it 'processes pending jobs synchronously' do + Sidekiq::Testing.disable! do + BackgroundMigrationWorker + .perform_in(2.minutes, 'MigrateStageIndex', [1, 1]) + BackgroundMigrationWorker + .perform_async('MigrateStageIndex', [1, 1]) + + migrate! + + expect(migration).to have_received(:perform).with(1, 1).twice + end + end + end + + context 'when there are no background migrations pending' do + it 'does nothing' do + Sidekiq::Testing.disable! do + migrate! + + expect(migration).not_to have_received(:perform) + end + end + end + + context 'when there are still unmigrated stages present' do + let(:stages) { table('ci_stages') } + let(:builds) { table('ci_builds') } + + let!(:entities) do + %w[build test broken].map do |name| + stages.create(name: name) + end + end + + before do + stages.update_all(position: nil) + + builds.create(name: 'unit', stage_id: entities.first.id, stage_idx: 1, ref: 'master') + builds.create(name: 'unit', stage_id: entities.second.id, stage_idx: 1, ref: 'master') + end + + it 'migrates stages sequentially for every stage' do + expect(stages.all).to all(have_attributes(position: nil)) + + migrate! + + expect(migration).to have_received(:perform) + .with(entities.first.id, entities.first.id) + expect(migration).to have_received(:perform) + .with(entities.second.id, entities.second.id) + expect(migration).not_to have_received(:perform) + .with(entities.third.id, entities.third.id) + end + end +end diff --git a/spec/migrations/enqueue_delete_diff_files_workers_spec.rb b/spec/migrations/enqueue_delete_diff_files_workers_spec.rb new file mode 100644 index 00000000000..686027822b8 --- /dev/null +++ b/spec/migrations/enqueue_delete_diff_files_workers_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180619121030_enqueue_delete_diff_files_workers.rb') + +describe EnqueueDeleteDiffFilesWorkers, :migration, :sidekiq do + let(:merge_request_diffs) { table(:merge_request_diffs) } + let(:merge_requests) { table(:merge_requests) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + + namespaces.create!(id: 1, name: 'gitlab', path: 'gitlab') + projects.create!(id: 1, namespace_id: 1, name: 'gitlab', path: 'gitlab') + + merge_requests.create!(id: 1, target_project_id: 1, source_project_id: 1, target_branch: 'feature', source_branch: 'master', state: 'merged') + + merge_request_diffs.create!(id: 1, merge_request_id: 1, state: 'collected') + merge_request_diffs.create!(id: 2, merge_request_id: 1, state: 'without_files') + merge_request_diffs.create!(id: 3, merge_request_id: 1, state: 'collected') + merge_request_diffs.create!(id: 4, merge_request_id: 1, state: 'collected') + merge_request_diffs.create!(id: 5, merge_request_id: 1, state: 'empty') + merge_request_diffs.create!(id: 6, merge_request_id: 1, state: 'collected') + + merge_requests.update(1, latest_merge_request_diff_id: 6) + end + + it 'correctly schedules diff file deletion workers' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + # 1st batch + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(8.minutes, 1) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(9.minutes, 3) + # 2nd batch + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(16.minutes, 4) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(17.minutes, 6) + expect(BackgroundMigrationWorker.jobs.size).to eq(4) + end + end + end + + it 'migrates the data' do + expect { migrate! }.to change { merge_request_diffs.where(state: 'without_files').count } + .from(1).to(4) + end +end diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index c5d550cba1b..464897de306 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do + include ExclusiveLeaseHelpers + set(:build) { create(:ci_build, :running) } let(:chunk_index) { 0 } let(:data_store) { :redis } @@ -322,7 +324,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do describe 'ExclusiveLock' do before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil } + stub_exclusive_lease_taken stub_const('Ci::BuildTraceChunk::WRITE_LOCK_RETRY', 1) end diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb index f2a3df50c1a..0f156619e9e 100644 --- a/spec/models/concerns/reactive_caching_spec.rb +++ b/spec/models/concerns/reactive_caching_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe ReactiveCaching, :use_clean_rails_memory_store_caching do + include ExclusiveLeaseHelpers include ReactiveCachingHelpers class CacheTest @@ -106,8 +107,8 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end it 'takes and releases the lease' do - expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return("000000") - expect(Gitlab::ExclusiveLease).to receive(:cancel).with(cache_key, "000000") + expect_to_obtain_exclusive_lease(cache_key, 'uuid') + expect_to_cancel_exclusive_lease(cache_key, 'uuid') go! end @@ -153,11 +154,9 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do end context 'when the lease is already taken' do - before do - expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(nil) - end - it 'skips the calculation' do + stub_exclusive_lease_taken(cache_key) + expect(instance).to receive(:calculate_reactive_cache).never go! diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb index 2a2ef5a304d..2f9f63ce7e0 100644 --- a/spec/models/concerns/resolvable_discussion_spec.rb +++ b/spec/models/concerns/resolvable_discussion_spec.rb @@ -534,11 +534,18 @@ describe Discussion, ResolvableDiscussion do describe "#last_resolved_note" do let(:current_user) { create(:user) } + let(:time) { Time.now.utc } before do - first_note.resolve!(current_user) - third_note.resolve!(current_user) - second_note.resolve!(current_user) + Timecop.freeze(time - 1.second) do + first_note.resolve!(current_user) + end + Timecop.freeze(time) do + third_note.resolve!(current_user) + end + Timecop.freeze(time + 1.second) do + second_note.resolve!(current_user) + end end it "returns the last note that was resolved" do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a2f8fac2f38..abdc65336ca 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -571,13 +571,13 @@ describe Project do last_activity_at: timestamp, last_repository_updated_at: timestamp - 1.hour) - expect(project.last_activity_date).to eq(timestamp) + expect(project.last_activity_date).to be_like_time(timestamp) project.update_attributes(updated_at: timestamp, last_activity_at: timestamp - 1.hour, last_repository_updated_at: nil) - expect(project.last_activity_date).to eq(timestamp) + expect(project.last_activity_date).to be_like_time(timestamp) end end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 27a14ff5d5b..cfa78c4472c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -479,6 +479,14 @@ describe Repository do end end + context 'when ref is not specified' do + it 'is using a root ref' do + expect(repository).to receive(:find_commit).with('master') + + repository.commit + end + end + context 'when ref is not valid' do context 'when preceding tree element exists' do it 'returns nil' do @@ -1689,19 +1697,29 @@ describe Repository do end describe '#after_change_head' do - it 'flushes the readme cache' do + it 'flushes the method caches' do expect(repository).to receive(:expire_method_caches).with([ - :readme, + :size, + :commit_count, + :rendered_readme, + :contribution_guide, :changelog, - :license, - :contributing, + :license_blob, + :license_key, :gitignore, - :koding, - :gitlab_ci, + :koding_yml, + :gitlab_ci_yml, + :branch_names, + :tag_names, + :branch_count, + :tag_count, :avatar, - :issue_template, - :merge_request_template, - :xcode_config + :exists?, + :root_ref, + :has_visible_content?, + :issue_template_names, + :merge_request_template_names, + :xcode_project? ]) repository.after_change_head diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index d4ebfc3f782..eba39bb6ccc 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -14,6 +14,7 @@ describe API::MergeRequests do let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) } let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) } let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } + let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } let!(:label) do @@ -85,7 +86,7 @@ describe API::MergeRequests do get api('/merge_requests', user), scope: :all - expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request) + expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request, merge_request_locked) expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id) end @@ -158,7 +159,7 @@ describe API::MergeRequests do it 'returns merge requests with the given source branch' do get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all' - expect_response_contain_exactly(merge_request_closed, merge_request_merged) + expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked) end end @@ -166,7 +167,7 @@ describe API::MergeRequests do it 'returns merge requests with the given target branch' do get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all' - expect_response_contain_exactly(merge_request_closed, merge_request_merged) + expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked) end end @@ -219,6 +220,14 @@ describe API::MergeRequests do expect_response_ordered_exactly(merge_request) end end + + context 'state param' do + it 'returns merge requests with the given state' do + get api('/merge_requests', user), state: 'locked' + + expect_response_contain_exactly(merge_request_locked) + end + end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 99103039f77..abf9ad738bd 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1990,6 +1990,38 @@ describe API::Projects do end end + describe 'PUT /projects/:id/transfer' do + context 'when authenticated as owner' do + let(:group) { create :group } + + it 'transfers the project to the new namespace' do + group.add_owner(user) + + put api("/projects/#{project.id}/transfer", user), namespace: group.id + + expect(response).to have_gitlab_http_status(200) + end + + it 'fails when transferring to a non owned namespace' do + put api("/projects/#{project.id}/transfer", user), namespace: group.id + + expect(response).to have_gitlab_http_status(404) + end + + it 'fails when transferring to an unknown namespace' do + put api("/projects/#{project.id}/transfer", user), namespace: 'unknown' + + expect(response).to have_gitlab_http_status(404) + end + + it 'fails on missing namespace' do + put api("/projects/#{project.id}/transfer", user) + + expect(response).to have_gitlab_http_status(400) + end + end + end + it_behaves_like 'custom attributes endpoints', 'projects' do let(:attributable) { project } let(:other_attributable) { project2 } diff --git a/spec/requests/oauth_tokens_spec.rb b/spec/requests/oauth_tokens_spec.rb new file mode 100644 index 00000000000..000c3a2b868 --- /dev/null +++ b/spec/requests/oauth_tokens_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe 'OAuth Tokens requests' do + let(:user) { create :user } + let(:application) { create :oauth_application, scopes: 'api' } + + def request_access_token(user) + post '/oauth/token', + grant_type: 'authorization_code', + code: generate_access_grant(user).token, + redirect_uri: application.redirect_uri, + client_id: application.uid, + client_secret: application.secret + end + + def generate_access_grant(user) + create :oauth_access_grant, application: application, resource_owner_id: user.id + end + + context 'when there is already a token for the application' do + let!(:existing_token) { create :oauth_access_token, application: application, resource_owner_id: user.id } + + context 'and the request is done by the resource owner' do + it 'reuses and returns the stored token' do + expect do + request_access_token(user) + end.not_to change { Doorkeeper::AccessToken.count } + + expect(json_response['access_token']).to eq existing_token.token + end + end + + context 'and the request is done by a different user' do + let(:other_user) { create :user } + + it 'generates and returns a different token for a different owner' do + expect do + request_access_token(other_user) + end.to change { Doorkeeper::AccessToken.count }.by(1) + + expect(json_response['access_token']).not_to be_nil + end + end + end + + context 'when there is no token stored for the application' do + it 'generates and returns a new token' do + expect do + request_access_token(user) + end.to change { Doorkeeper::AccessToken.count }.by(1) + + expect(json_response['access_token']).not_to be_nil + end + end +end diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index bcb8d6c2bfc..b14d4b8fb6e 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -1,11 +1,49 @@ require 'spec_helper' describe 'OpenID Connect requests' do - let(:user) { create :user } + let(:user) do + create( + :user, + name: 'Alice', + username: 'alice', + email: 'private@example.com', + emails: [public_email], + public_email: public_email.email, + website_url: 'https://example.com', + avatar: fixture_file_upload('spec/fixtures/dk.png') + ) + end + + let(:public_email) { build :email, email: 'public@example.com' } + let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id } let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id } - def request_access_token + let(:hashed_subject) do + Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}") + end + + let(:id_token_claims) do + { + 'sub' => user.id.to_s, + 'sub_legacy' => hashed_subject + } + end + + let(:user_info_claims) do + { + 'name' => 'Alice', + 'nickname' => 'alice', + 'email' => 'public@example.com', + 'email_verified' => true, + 'website' => 'https://example.com', + 'profile' => 'http://localhost/alice', + 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png", + 'groups' => kind_of(Array) + } + end + + def request_access_token! login_as user post '/oauth/token', @@ -16,26 +54,22 @@ describe 'OpenID Connect requests' do client_secret: application.secret end - def request_user_info + def request_user_info! get '/oauth/userinfo', nil, 'Authorization' => "Bearer #{access_token.token}" end - def hashed_subject - Digest::SHA256.hexdigest("#{user.id}-#{Rails.application.secrets.secret_key_base}") - end - context 'Application without OpenID scope' do let(:application) { create :oauth_application, scopes: 'api' } it 'token response does not include an ID token' do - request_access_token + request_access_token! expect(json_response).to include 'access_token' expect(json_response).not_to include 'id_token' end it 'userinfo response is unauthorized' do - request_user_info + request_user_info! expect(response).to have_gitlab_http_status 403 expect(response.body).to be_blank @@ -46,28 +80,12 @@ describe 'OpenID Connect requests' do let(:application) { create :oauth_application, scopes: 'openid' } it 'token response includes an ID token' do - request_access_token + request_access_token! expect(json_response).to include 'id_token' end context 'UserInfo payload' do - let(:user) do - create( - :user, - name: 'Alice', - username: 'alice', - emails: [private_email, public_email], - email: private_email.email, - public_email: public_email.email, - website_url: 'https://example.com', - avatar: fixture_file_upload('spec/fixtures/dk.png') - ) - end - - let!(:public_email) { build :email, email: 'public@example.com' } - let!(:private_email) { build :email, email: 'private@example.com' } - let!(:group1) { create :group } let!(:group2) { create :group } let!(:group3) { create :group, parent: group2 } @@ -76,41 +94,35 @@ describe 'OpenID Connect requests' do before do group1.add_user(user, GroupMember::OWNER) group3.add_user(user, Gitlab::Access::DEVELOPER) + + request_user_info! end it 'includes all user information and group memberships' do - request_user_info - - expect(json_response).to match(a_hash_including({ - 'sub' => hashed_subject, - 'name' => 'Alice', - 'nickname' => 'alice', - 'email' => 'public@example.com', - 'email_verified' => true, - 'website' => 'https://example.com', - 'profile' => 'http://localhost/alice', - 'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png", - 'groups' => anything - })) + expect(json_response).to match(id_token_claims.merge(user_info_claims)) expected_groups = [group1.full_path, group3.full_path] expected_groups << group4.full_path if Group.supports_nested_groups? expect(json_response['groups']).to match_array(expected_groups) end + + it 'does not include any unknown claims' do + expect(json_response.keys).to eq %w[sub sub_legacy] + user_info_claims.keys + end end context 'ID token payload' do before do - request_access_token + request_access_token! @payload = JSON::JWT.decode(json_response['id_token'], :skip_verification) end - it 'includes the Gitlab root URL' do - expect(@payload['iss']).to eq Gitlab.config.gitlab.url + it 'includes the subject claims' do + expect(@payload).to match(a_hash_including(id_token_claims)) end - it 'includes the hashed user ID' do - expect(@payload['sub']).to eq hashed_subject + it 'includes the Gitlab root URL' do + expect(@payload['iss']).to eq Gitlab.config.gitlab.url end it 'includes the time of the last authentication', :clean_gitlab_redis_shared_state do @@ -118,7 +130,7 @@ describe 'OpenID Connect requests' do end it 'does not include any unknown properties' do - expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time] + expect(@payload.keys).to eq %w[iss sub aud exp iat auth_time sub_legacy] end end @@ -134,10 +146,10 @@ describe 'OpenID Connect requests' do context 'when user is blocked' do it 'returns authentication error' do access_grant - user.block + user.block! expect do - request_access_token + request_access_token! end.to raise_error UncaughtThrowError end end @@ -145,10 +157,10 @@ describe 'OpenID Connect requests' do context 'when user is ldap_blocked' do it 'returns authentication error' do access_grant - user.ldap_block + user.ldap_block! expect do - request_access_token + request_access_token! end.to raise_error UncaughtThrowError end end diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb index bf038595a4d..eb0bdb61ee3 100644 --- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb +++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb @@ -1,11 +1,13 @@ require 'spec_helper' describe Clusters::Applications::CheckIngressIpAddressService do + include ExclusiveLeaseHelpers + let(:application) { create(:clusters_applications_ingress, :installed) } let(:service) { described_class.new(application) } let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) } let(:ingress) { [{ ip: '111.222.111.222' }] } - let(:exclusive_lease) { instance_double(Gitlab::ExclusiveLease, try_obtain: true) } + let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" } let(:kube_service) do ::Kubeclient::Resource.new( @@ -22,11 +24,8 @@ describe Clusters::Applications::CheckIngressIpAddressService do subject { service.execute } before do + stub_exclusive_lease(lease_key, timeout: 15.seconds.to_i) allow(application.cluster).to receive(:kubeclient).and_return(kubeclient) - allow(Gitlab::ExclusiveLease) - .to receive(:new) - .with("check_ingress_ip_address_service:#{application.id}", timeout: 15.seconds.to_i) - .and_return(exclusive_lease) end describe '#execute' do @@ -47,13 +46,9 @@ describe Clusters::Applications::CheckIngressIpAddressService do end context 'when the exclusive lease cannot be obtained' do - before do - allow(exclusive_lease) - .to receive(:try_obtain) - .and_return(false) - end - it 'does not call kubeclient' do + stub_exclusive_lease_taken(lease_key, timeout: 15.seconds.to_i) + subject expect(kubeclient).not_to have_received(:get_service) diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index a9aee9e100f..609eef76d2c 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -5,8 +5,11 @@ describe Issues::MoveService do let(:author) { create(:user) } let(:title) { 'Some issue' } let(:description) { 'Some issue description' } - let(:old_project) { create(:project) } - let(:new_project) { create(:project) } + let(:group) { create(:group, :private) } + let(:sub_group_1) { create(:group, :private, parent: group) } + let(:sub_group_2) { create(:group, :private, parent: group) } + let(:old_project) { create(:project, namespace: sub_group_1) } + let(:new_project) { create(:project, namespace: sub_group_2) } let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') } let(:old_issue) do @@ -14,7 +17,7 @@ describe Issues::MoveService do project: old_project, author: author, milestone: milestone1) end - let(:move_service) do + subject(:move_service) do described_class.new(old_project, user) end @@ -102,6 +105,23 @@ describe Issues::MoveService do end end + context 'issue with group labels', :nested_groups do + it 'assigns group labels to new issue' do + label = create(:group_label, group: group) + label_issue = create(:labeled_issue, description: description, project: old_project, + milestone: milestone1, labels: [label]) + old_project.add_reporter(user) + new_project.add_reporter(user) + + new_issue = move_service.execute(label_issue, new_project) + + expect(new_issue).to have_attributes( + project: new_project, + labels: include(label) + ) + end + end + context 'generic issue' do include_context 'issue move executed' diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb index bb0fb6acf39..8e553c2f1fa 100644 --- a/spec/services/keys/last_used_service_spec.rb +++ b/spec/services/keys/last_used_service_spec.rb @@ -8,7 +8,7 @@ describe Keys::LastUsedService do Timecop.freeze(time) { described_class.new(key).execute } - expect(key.last_used_at).to eq(time) + expect(key.reload.last_used_at).to be_like_time(time) end it 'does not update the key when it has been used recently' do @@ -17,7 +17,7 @@ describe Keys::LastUsedService do described_class.new(key).execute - expect(key.last_used_at).to eq(time) + expect(key.last_used_at).to be_like_time(time) end it 'does not update the updated_at field' do diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb index b5fb999381d..812dd42934d 100644 --- a/spec/services/update_merge_request_metrics_service_spec.rb +++ b/spec/services/update_merge_request_metrics_service_spec.rb @@ -12,7 +12,7 @@ describe MergeRequestMetricsService do service.merge(event) expect(metrics.merged_by).to eq(user) - expect(metrics.merged_at).to eq(event.created_at) + expect(metrics.merged_at).to be_like_time(event.created_at) end end @@ -25,7 +25,7 @@ describe MergeRequestMetricsService do service.close(event) expect(metrics.latest_closed_by).to eq(user) - expect(metrics.latest_closed_at).to eq(event.created_at) + expect(metrics.latest_closed_at).to be_like_time(event.created_at) end end diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index 08fd26d67fd..e5fde07a6eb 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Users::RefreshAuthorizedProjectsService do + include ExclusiveLeaseHelpers + # We're using let! here so that any expectations for the service class are not # triggered twice. let!(:project) { create(:project) } @@ -10,12 +12,10 @@ describe Users::RefreshAuthorizedProjectsService do describe '#execute', :clean_gitlab_redis_shared_state do it 'refreshes the authorizations using a lease' do - expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) - .and_return('foo') - - expect(Gitlab::ExclusiveLease).to receive(:cancel) - .with(an_instance_of(String), 'foo') + lease_key = "refresh_authorized_projects:#{user.id}" + expect_to_obtain_exclusive_lease(lease_key, 'uuid') + expect_to_cancel_exclusive_lease(lease_key, 'uuid') expect(service).to receive(:execute_without_lease) service.execute diff --git a/spec/support/helpers/exclusive_lease_helpers.rb b/spec/support/helpers/exclusive_lease_helpers.rb new file mode 100644 index 00000000000..383cc7dee81 --- /dev/null +++ b/spec/support/helpers/exclusive_lease_helpers.rb @@ -0,0 +1,36 @@ +module ExclusiveLeaseHelpers + def stub_exclusive_lease(key = nil, uuid = 'uuid', renew: false, timeout: nil) + key ||= instance_of(String) + timeout ||= instance_of(Integer) + + lease = instance_double( + Gitlab::ExclusiveLease, + try_obtain: uuid, + exists?: true, + renew: renew + ) + + allow(Gitlab::ExclusiveLease) + .to receive(:new) + .with(key, timeout: timeout) + .and_return(lease) + + lease + end + + def stub_exclusive_lease_taken(key = nil, timeout: nil) + stub_exclusive_lease(key, nil, timeout: timeout) + end + + def expect_to_obtain_exclusive_lease(key, uuid = 'uuid', timeout: nil) + lease = stub_exclusive_lease(key, uuid, timeout: timeout) + + expect(lease).to receive(:try_obtain) + end + + def expect_to_cancel_exclusive_lease(key, uuid) + expect(Gitlab::ExclusiveLease) + .to receive(:cancel) + .with(key, uuid) + end +end diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb index 6dbe0f6f980..db723a323f8 100644 --- a/spec/support/shared_examples/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/ci_trace_shared_examples.rb @@ -247,8 +247,10 @@ shared_examples_for 'common trace features' do end context 'when another process has already been archiving', :clean_gitlab_redis_shared_state do + include ExclusiveLeaseHelpers + before do - Gitlab::ExclusiveLease.new("trace:archive:#{trace.job.id}", timeout: 1.hour).try_obtain + stub_exclusive_lease_taken("trace:archive:#{trace.job.id}", timeout: 1.hour) end it 'blocks concurrent archiving' do diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb index d5e22b8cb56..a401f7541f0 100644 --- a/spec/support/shared_examples/requests/api/merge_requests_list.rb +++ b/spec/support/shared_examples/requests/api/merge_requests_list.rb @@ -29,7 +29,7 @@ shared_examples 'merge requests list' do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) @@ -53,7 +53,7 @@ shared_examples 'merge requests list' do expect(response).to include_pagination_headers expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at)) expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) expect(json_response.last['iid']).to eq(merge_request.iid) expect(json_response.last['title']).to eq(merge_request.title) expect(json_response.last).to have_key('web_url') @@ -70,7 +70,7 @@ shared_examples 'merge requests list' do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) expect(json_response.last['title']).to eq(merge_request.title) end @@ -216,7 +216,7 @@ shared_examples 'merge requests list' do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort) end @@ -229,7 +229,7 @@ shared_examples 'merge requests list' do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort.reverse) end @@ -242,7 +242,7 @@ shared_examples 'merge requests list' do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) response_dates = json_response.map { |merge_request| merge_request['updated_at'] } expect(response_dates).to eq(response_dates.sort.reverse) end @@ -255,7 +255,7 @@ shared_examples 'merge requests list' do expect(response).to have_gitlab_http_status(200) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(json_response.length).to eq(4) response_dates = json_response.map { |merge_request| merge_request['created_at'] } expect(response_dates).to eq(response_dates.sort) end @@ -265,7 +265,7 @@ shared_examples 'merge requests list' do it 'returns merge requests with the given source branch' do get api(endpoint_path, user), source_branch: merge_request_closed.source_branch, state: 'all' - expect_response_contain_exactly(merge_request_closed, merge_request_merged) + expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked) end end @@ -273,7 +273,7 @@ shared_examples 'merge requests list' do it 'returns merge requests with the given target branch' do get api(endpoint_path, user), target_branch: merge_request_closed.target_branch, state: 'all' - expect_response_contain_exactly(merge_request_closed, merge_request_merged) + expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked) end end end diff --git a/spec/support/shared_examples/throttled_touch.rb b/spec/support/shared_examples/throttled_touch.rb index 4a25bb9b750..eba990d4037 100644 --- a/spec/support/shared_examples/throttled_touch.rb +++ b/spec/support/shared_examples/throttled_touch.rb @@ -3,7 +3,7 @@ shared_examples_for 'throttled touch' do it 'updates the updated_at timestamp' do Timecop.freeze do subject.touch - expect(subject.updated_at).to eq(Time.zone.now) + expect(subject.updated_at).to be_like_time(Time.zone.now) end end @@ -14,7 +14,7 @@ shared_examples_for 'throttled touch' do Timecop.freeze(first_updated_at) { subject.touch } Timecop.freeze(second_updated_at) { subject.touch } - expect(subject.updated_at).to eq(first_updated_at) + expect(subject.updated_at).to be_like_time(first_updated_at) end end end 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 19800c6638f..1bd176280c5 100644 --- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb +++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb @@ -76,8 +76,10 @@ shared_examples "migrates" do |to_store:, from_store: nil| end context 'when migrate! is occupied by another process' do + include ExclusiveLeaseHelpers + before do - @uuid = Gitlab::ExclusiveLease.new(subject.exclusive_lease_key, timeout: 1.hour.to_i).try_obtain + stub_exclusive_lease_taken(subject.exclusive_lease_key, timeout: 1.hour.to_i) end it 'does not execute migrate!' do @@ -91,10 +93,6 @@ shared_examples "migrates" do |to_store:, from_store: nil| expect { subject.use_file }.to raise_error(ObjectStorage::ExclusiveLeaseTaken) end - - after do - Gitlab::ExclusiveLease.cancel(subject.exclusive_lease_key, @uuid) - end end context 'migration is unsuccessful' do diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb index 6b1f2ff3227..8c4daac5f80 100644 --- a/spec/workers/project_cache_worker_spec.rb +++ b/spec/workers/project_cache_worker_spec.rb @@ -1,49 +1,58 @@ require 'spec_helper' describe ProjectCacheWorker do + include ExclusiveLeaseHelpers + let(:worker) { described_class.new } let(:project) { create(:project, :repository) } let(:statistics) { project.statistics } + let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" } + let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT } - describe '#perform' do - before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) - .and_return(true) - end + before do + stub_exclusive_lease(lease_key, timeout: lease_timeout) + allow(Project).to receive(:find_by) + .with(id: project.id) + .and_return(project) + end + + describe '#perform' do context 'with a non-existing project' do - it 'does nothing' do - expect(worker).not_to receive(:update_statistics) + it 'does not update statistic' do + allow(Project).to receive(:find_by).with(id: -1).and_return(nil) - worker.perform(-1) + expect(subject).not_to receive(:update_statistics) + + subject.perform(-1) end end context 'with an existing project without a repository' do - it 'does nothing' do - allow_any_instance_of(Repository).to receive(:exists?).and_return(false) + it 'does not update statistics' do + allow(project.repository).to receive(:exists?).and_return(false) - expect(worker).not_to receive(:update_statistics) + expect(subject).not_to receive(:update_statistics) - worker.perform(project.id) + subject.perform(project.id) end end context 'with an existing project' do it 'updates the project statistics' do - expect(worker).to receive(:update_statistics) - .with(kind_of(Project), %i(repository_size)) - .and_call_original + expect(subject).to receive(:update_statistics) + .with(%w(repository_size)) + .and_call_original - worker.perform(project.id, [], %w(repository_size)) + subject.perform(project.id, [], %w(repository_size)) end it 'refreshes the method caches' do - expect_any_instance_of(Repository).to receive(:refresh_method_caches) - .with(%i(readme)) - .and_call_original + expect(project.repository).to receive(:refresh_method_caches) + .with(%i(readme)) + .and_call_original - worker.perform(project.id, %w(readme)) + subject.perform(project.id, %w(readme)) end context 'with plain readme' do @@ -51,39 +60,40 @@ describe ProjectCacheWorker do allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false) allow(MarkupHelper).to receive(:plain?).and_return(true) - expect_any_instance_of(Repository).to receive(:refresh_method_caches) - .with(%i(readme)) - .and_call_original - worker.perform(project.id, %w(readme)) + expect(project.repository).to receive(:refresh_method_caches) + .with(%i(readme)) + .and_call_original + + subject.perform(project.id, %w(readme)) end end end - end - describe '#update_statistics' do context 'when a lease could not be obtained' do it 'does not update the repository size' do - allow(worker).to receive(:try_obtain_lease_for) - .with(project.id, :update_statistics) - .and_return(false) + stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) - expect(statistics).not_to receive(:refresh!) + expect(project.statistics).not_to receive(:refresh!) - worker.update_statistics(project) + subject.perform(project.id, [], %w(repository_size)) end end context 'when a lease could be obtained' do it 'updates the project statistics' do - allow(worker).to receive(:try_obtain_lease_for) - .with(project.id, :update_statistics) - .and_return(true) + stub_exclusive_lease(lease_key, timeout: lease_timeout) + + expect(project.statistics).to receive(:refresh!) + .with(only: %i(repository_size)) + .and_call_original + + subject.perform(project.id, [], %i(repository_size)) + end - expect(statistics).to receive(:refresh!) - .with(only: %i(repository_size)) - .and_call_original + it 'cancels the lease after statistics has been updated' do + expect(subject).to receive(:release_lease).with('uuid') - worker.update_statistics(project, %i(repository_size)) + subject.perform(project.id, [], %i(repository_size)) end end end diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb index 2e3951e7afc..9551e358af1 100644 --- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb +++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb @@ -1,53 +1,47 @@ require 'spec_helper' describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do + include ExclusiveLeaseHelpers + describe '#perform' do let(:project) { create(:project, :empty_repo) } - let(:pending_delete_project) { create(:project, :empty_repo, pending_delete: true) } + let(:lease_key) { "project_migrate_hashed_storage_worker:#{project.id}" } + let(:lease_timeout) { ProjectMigrateHashedStorageWorker::LEASE_TIMEOUT } + + it 'skips when project no longer exists' do + expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + + subject.perform(-1) + end - context 'when have exclusive lease' do - before do - lease = subject.lease_for(project.id) + it 'skips when project is pending delete' do + pending_delete_project = create(:project, :empty_repo, pending_delete: true) - allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease) - allow(lease).to receive(:try_obtain).and_return(true) - end + expect(::Projects::HashedStorageMigrationService).not_to receive(:new) - it 'skips when project no longer exists' do - nonexistent_id = 999999999999 + subject.perform(pending_delete_project.id) + end - expect(::Projects::HashedStorageMigrationService).not_to receive(:new) - subject.perform(nonexistent_id) - end + it 'delegates removal to service class when have exclusive lease' do + stub_exclusive_lease(lease_key, 'uuid', timeout: lease_timeout) - it 'skips when project is pending delete' do - expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + migration_service = spy - subject.perform(pending_delete_project.id) - end + allow(::Projects::HashedStorageMigrationService) + .to receive(:new).with(project, subject.logger) + .and_return(migration_service) - it 'delegates removal to service class' do - service = double('service') - expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service) - expect(service).to receive(:execute) + subject.perform(project.id) - subject.perform(project.id) - end + expect(migration_service).to have_received(:execute) end - context 'when dont have exclusive lease' do - before do - lease = subject.lease_for(project.id) - - allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease) - allow(lease).to receive(:try_obtain).and_return(false) - end + it 'skips when dont have lease when dont have exclusive lease' do + stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) - it 'skips when dont have lease' do - expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + expect(::Projects::HashedStorageMigrationService).not_to receive(:new) - subject.perform(project.id) - end + subject.perform(project.id) end end end diff --git a/spec/workers/propagate_service_template_worker_spec.rb b/spec/workers/propagate_service_template_worker_spec.rb index b8b65ead9b3..af1fb80a51d 100644 --- a/spec/workers/propagate_service_template_worker_spec.rb +++ b/spec/workers/propagate_service_template_worker_spec.rb @@ -1,29 +1,29 @@ require 'spec_helper' describe PropagateServiceTemplateWorker do - let!(:service_template) do - PushoverService.create( - template: true, - active: true, - properties: { - device: 'MyDevice', - sound: 'mic', - priority: 4, - user_key: 'asdf', - api_key: '123456789' - }) - end - - before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) - .and_return(true) - end + include ExclusiveLeaseHelpers describe '#perform' do it 'calls the propagate service with the template' do - expect(Projects::PropagateServiceTemplate).to receive(:propagate).with(service_template) + template = PushoverService.create( + template: true, + active: true, + properties: { + device: 'MyDevice', + sound: 'mic', + priority: 4, + user_key: 'asdf', + api_key: '123456789' + }) + + stub_exclusive_lease("propagate_service_template_worker:#{template.id}", + timeout: PropagateServiceTemplateWorker::LEASE_TIMEOUT) + + expect(Projects::PropagateServiceTemplate) + .to receive(:propagate) + .with(template) - subject.perform(service_template.id) + subject.perform(template.id) end end end diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb index 5968c5da3c9..a653f6f926c 100644 --- a/spec/workers/repository_remove_remote_worker_spec.rb +++ b/spec/workers/repository_remove_remote_worker_spec.rb @@ -1,44 +1,50 @@ require 'rails_helper' describe RepositoryRemoveRemoteWorker do - subject(:worker) { described_class.new } + include ExclusiveLeaseHelpers describe '#perform' do - let(:remote_name) { 'joe'} let!(:project) { create(:project, :repository) } + let(:remote_name) { 'joe'} + let(:lease_key) { "remove_remote_#{project.id}_#{remote_name}" } + let(:lease_timeout) { RepositoryRemoveRemoteWorker::LEASE_TIMEOUT } - context 'when it cannot obtain lease' do - it 'logs error' do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil } - - expect_any_instance_of(Repository).not_to receive(:remove_remote) - expect(worker).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.') - - worker.perform(project.id, remote_name) - end + it 'returns nil when project does not exist' do + expect(subject.perform(-1, 'remote_name')).to be_nil end - context 'when it gets the lease' do + context 'when project exists' do before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true) + allow(Project) + .to receive(:find_by) + .with(id: project.id) + .and_return(project) end - context 'when project does not exist' do - it 'returns nil' do - expect(worker.perform(-1, 'remote_name')).to be_nil - end - end + it 'does not remove remote when cannot obtain lease' do + stub_exclusive_lease_taken(lease_key, timeout: lease_timeout) + + expect(project.repository) + .not_to receive(:remove_remote) - context 'when project exists' do - it 'removes remote from repository' do - masterrev = project.repository.find_branch('master').dereferenced_target + expect(subject) + .to receive(:log_error) + .with('Cannot obtain an exclusive lease. There must be another instance already in execution.') - create_remote_branch(remote_name, 'remote_branch', masterrev) + subject.perform(project.id, remote_name) + end - expect_any_instance_of(Repository).to receive(:remove_remote).with(remote_name).and_call_original + it 'removes remote from repository when obtain a lease' do + stub_exclusive_lease(lease_key, timeout: lease_timeout) + masterrev = project.repository.find_branch('master').dereferenced_target + create_remote_branch(remote_name, 'remote_branch', masterrev) - worker.perform(project.id, remote_name) - end + expect(project.repository) + .to receive(:remove_remote) + .with(remote_name) + .and_call_original + + subject.perform(project.id, remote_name) end end end @@ -47,6 +53,7 @@ describe RepositoryRemoveRemoteWorker do rugged = Gitlab::GitalyClient::StorageSettings.allow_disk_access do project.repository.rugged end + rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id) end end diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index 2605c14334f..856886e3df5 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -1,14 +1,21 @@ require 'spec_helper' describe StuckCiJobsWorker do + include ExclusiveLeaseHelpers + let!(:runner) { create :ci_runner } let!(:job) { create :ci_build, runner: runner } - let(:worker) { described_class.new } - let(:exclusive_lease_uuid) { SecureRandom.uuid } + let(:trace_lease_key) { "trace:archive:#{job.id}" } + let(:trace_lease_uuid) { SecureRandom.uuid } + let(:worker_lease_key) { StuckCiJobsWorker::EXCLUSIVE_LEASE_KEY } + let(:worker_lease_uuid) { SecureRandom.uuid } + + subject(:worker) { described_class.new } before do + stub_exclusive_lease(worker_lease_key, worker_lease_uuid) + stub_exclusive_lease(trace_lease_key, trace_lease_uuid) job.update!(status: status, updated_at: updated_at) - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid) end shared_examples 'job is dropped' do @@ -44,16 +51,19 @@ describe StuckCiJobsWorker do context 'when job was not updated for more than 1 day ago' do let(:updated_at) { 2.days.ago } + it_behaves_like 'job is dropped' end context 'when job was updated in less than 1 day ago' do let(:updated_at) { 6.hours.ago } + it_behaves_like 'job is unchanged' end context 'when job was not updated for more than 1 hour ago' do let(:updated_at) { 2.hours.ago } + it_behaves_like 'job is unchanged' end end @@ -65,11 +75,14 @@ describe StuckCiJobsWorker do context 'when job was not updated for more than 1 hour ago' do let(:updated_at) { 2.hours.ago } + it_behaves_like 'job is dropped' end - context 'when job was updated in less than 1 hour ago' do + context 'when job was updated in less than 1 + hour ago' do let(:updated_at) { 30.minutes.ago } + it_behaves_like 'job is unchanged' end end @@ -80,11 +93,13 @@ describe StuckCiJobsWorker do context 'when job was not updated for more than 1 hour ago' do let(:updated_at) { 2.hours.ago } + it_behaves_like 'job is dropped' end context 'when job was updated in less than 1 hour ago' do let(:updated_at) { 30.minutes.ago } + it_behaves_like 'job is unchanged' end end @@ -93,6 +108,7 @@ describe StuckCiJobsWorker do context "when job is #{status}" do let(:status) { status } let(:updated_at) { 2.days.ago } + it_behaves_like 'job is unchanged' end end @@ -119,23 +135,27 @@ describe StuckCiJobsWorker do it 'is guard by exclusive lease when executed concurrently' do expect(worker).to receive(:drop).at_least(:once).and_call_original expect(worker2).not_to receive(:drop) + worker.perform - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false) + + stub_exclusive_lease_taken(worker_lease_key) + worker2.perform end it 'can be executed in sequence' do expect(worker).to receive(:drop).at_least(:once).and_call_original expect(worker2).to receive(:drop).at_least(:once).and_call_original + worker.perform worker2.perform end - it 'cancels exclusive lease after worker perform' do - worker.perform + it 'cancels exclusive leases after worker perform' do + expect_to_cancel_exclusive_lease(trace_lease_key, trace_lease_uuid) + expect_to_cancel_exclusive_lease(worker_lease_key, worker_lease_uuid) - expect(Gitlab::ExclusiveLease.new(described_class::EXCLUSIVE_LEASE_KEY, timeout: 1.hour)) - .not_to be_exists + worker.perform end end end |
