summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/import/google_code_controller_spec.rb65
-rw-r--r--spec/factories/sequences.rb1
-rw-r--r--spec/features/projects/active_tabs_spec.rb8
-rw-r--r--spec/features/projects/new_project_spec.rb12
-rw-r--r--spec/features/users/show_spec.rb28
-rw-r--r--spec/frontend/cycle_analytics/banner_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js9
-rw-r--r--spec/frontend/diffs/components/image_diff_overlay_spec.js14
-rw-r--r--spec/frontend/importer_status_spec.js141
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js68
-rw-r--r--spec/helpers/users_helper_spec.rb61
-rw-r--r--spec/lib/gitlab/ci/ansi2json/result_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/ansi2json/style_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/need_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/config/entry/needs_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/services_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb65
-rw-r--r--spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb4
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/client_spec.rb38
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb88
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb32
-rw-r--r--spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb2
-rw-r--r--spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb4
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb5
-rw-r--r--spec/models/ci/build_dependencies_spec.rb4
-rw-r--r--spec/models/project_spec.rb11
-rw-r--r--spec/policies/user_policy_spec.rb12
-rw-r--r--spec/requests/api/project_repository_storage_moves_spec.rb62
-rw-r--r--spec/routing/import_routing_spec.rb26
-rw-r--r--spec/rubocop/cop/lint/last_keyword_argument_spec.rb14
-rw-r--r--spec/services/ci/create_job_artifacts_service_spec.rb2
-rw-r--r--spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb47
-rw-r--r--spec/support/caching.rb2
-rw-r--r--spec/support/graphql/var.rb59
-rw-r--r--spec/support/helpers/gitlab_verify_helpers.rb2
-rw-r--r--spec/support/helpers/graphql_helpers.rb113
-rw-r--r--spec/support/shared_examples/models/concerns/shardable_shared_examples.rb6
-rw-r--r--spec/support_specs/graphql/var_spec.rb34
-rw-r--r--spec/support_specs/helpers/graphql_helpers_spec.rb219
-rw-r--r--spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb33
44 files changed, 871 insertions, 500 deletions
diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb
deleted file mode 100644
index 0fda111c029..00000000000
--- a/spec/controllers/import/google_code_controller_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Import::GoogleCodeController do
- include ImportSpecHelper
-
- let(:user) { create(:user) }
- let(:dump_file) { fixture_file_upload('spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') }
-
- before do
- sign_in(user)
- end
-
- describe "POST callback" do
- it "stores Google Takeout dump list in session" do
- post :callback, params: { dump_file: dump_file }
-
- expect(session[:google_code_dump]).to be_a(Hash)
- expect(session[:google_code_dump]["kind"]).to eq("projecthosting#user")
- expect(session[:google_code_dump]).to have_key("projects")
- end
- end
-
- describe "GET status" do
- before do
- @repo = OpenStruct.new(name: 'vim')
- stub_client(valid?: true)
- end
-
- it "assigns variables" do
- @project = create(:project, import_type: 'google_code', creator_id: user.id)
- stub_client(repos: [@repo], incompatible_repos: [])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo])
- expect(assigns(:incompatible_repos)).to eq([])
- end
-
- it "does not show already added project" do
- @project = create(:project, import_type: 'google_code', creator_id: user.id, import_source: 'vim')
- stub_client(repos: [@repo], incompatible_repos: [])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
- end
-
- it "does not show any invalid projects" do
- stub_client(repos: [], incompatible_repos: [@repo])
-
- get :status
-
- expect(assigns(:repos)).to be_empty
- expect(assigns(:incompatible_repos)).to eq([@repo])
- end
- end
-
- describe "POST create" do
- it_behaves_like 'project import rate limiter'
- end
-end
diff --git a/spec/factories/sequences.rb b/spec/factories/sequences.rb
index ca0804965df..4e66d7c125c 100644
--- a/spec/factories/sequences.rb
+++ b/spec/factories/sequences.rb
@@ -13,4 +13,5 @@ FactoryBot.define do
sequence(:past_time) { |n| 4.hours.ago + (2 * n).seconds }
sequence(:iid)
sequence(:sha) { |n| Digest::SHA1.hexdigest("commit-like-#{n}") }
+ sequence(:variable) { |n| "var#{n}" }
end
diff --git a/spec/features/projects/active_tabs_spec.rb b/spec/features/projects/active_tabs_spec.rb
index 349e5f5e177..8001ce0f454 100644
--- a/spec/features/projects/active_tabs_spec.rb
+++ b/spec/features/projects/active_tabs_spec.rb
@@ -124,15 +124,15 @@ RSpec.describe 'Project active tab' do
context 'on project Analytics' do
before do
- visit charts_project_graph_path(project, 'master')
+ visit project_cycle_analytics_path(project)
end
- context 'on project Analytics/Repository Analytics' do
+ context 'on project Analytics/Value Stream Analytics' do
it_behaves_like 'page has active tab', _('Analytics')
- it_behaves_like 'page has active sub tab', _('Repository')
+ it_behaves_like 'page has active sub tab', _('Value Stream')
end
- context 'on project Analytics/Cycle Analytics' do
+ context 'on project Analytics/"CI / CD"' do
before do
click_tab(_('CI / CD'))
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 796fd76cfdf..4aabf040655 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -56,7 +56,6 @@ RSpec.describe 'New project', :js do
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
- expect(page).to have_link('Google Code')
expect(page).to have_button('Repo by URL')
expect(page).to have_link('GitLab export')
end
@@ -292,17 +291,6 @@ RSpec.describe 'New project', :js do
end
end
- context 'from Google Code' do
- before do
- first('.import_google_code').click
- end
-
- it 'shows import instructions' do
- expect(page).to have_content('Import projects from Google Code')
- expect(current_path).to eq new_import_google_code_path
- end
- end
-
context 'from manifest file' do
before do
first('.import_manifest').click
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index aebe2cc602d..be2d71aef90 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe 'User page' do
let_it_be(:user) { create(:user, bio: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') }
- subject { visit(user_path(user)) }
+ subject(:visit_profile) { visit(user_path(user)) }
context 'with public profile' do
it 'shows all the tabs' do
@@ -123,6 +123,32 @@ RSpec.describe 'User page' do
end
end
+ context 'with unconfirmed user' do
+ let_it_be(:user) { create(:user, :unconfirmed) }
+
+ before do
+ visit_profile
+ end
+
+ it 'shows user name as unconfirmed' do
+ expect(page).to have_css(".cover-title", text: 'Unconfirmed user')
+ end
+
+ it 'shows no tab' do
+ expect(page).to have_css("div.profile-header")
+ expect(page).not_to have_css("ul.nav-links")
+ end
+
+ it 'shows no additional fields' do
+ expect(page).not_to have_css(".profile-user-bio")
+ expect(page).not_to have_css(".profile-link-holder")
+ end
+
+ it 'shows private profile message' do
+ expect(page).to have_content("This user has a private profile")
+ end
+ end
+
it 'shows the status if there was one' do
create(:user_status, user: user, message: "Working hard!")
diff --git a/spec/frontend/cycle_analytics/banner_spec.js b/spec/frontend/cycle_analytics/banner_spec.js
index f0b8cb18a90..0cae0298cee 100644
--- a/spec/frontend/cycle_analytics/banner_spec.js
+++ b/spec/frontend/cycle_analytics/banner_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
import banner from '~/cycle_analytics/components/banner.vue';
-describe('Cycle analytics banner', () => {
+describe('Value Stream Analytics banner', () => {
let vm;
beforeEach(() => {
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index c0532e392d1..43d295ff1b3 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -6,7 +6,6 @@ import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
-import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import NoteForm from '~/notes/components/note_form.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
@@ -166,14 +165,6 @@ describe('DiffContent', () => {
describe('with image files', () => {
const imageDiffFile = { ...defaultProps.diffFile, viewer: { name: diffViewerModes.image } };
- it('should have image diff view in place', () => {
- getCommentFormForDiffFileGetterMock.mockReturnValue(() => true);
- createComponent({ props: { diffFile: imageDiffFile } });
-
- expect(wrapper.find(InlineDiffView).exists()).toBe(false);
- expect(wrapper.find(ImageDiffOverlay).exists()).toBe(true);
- });
-
it('renders diff file discussions', () => {
getCommentFormForDiffFileGetterMock.mockReturnValue(() => true);
createComponent({
diff --git a/spec/frontend/diffs/components/image_diff_overlay_spec.js b/spec/frontend/diffs/components/image_diff_overlay_spec.js
index 5a88a3cabd1..087715111b4 100644
--- a/spec/frontend/diffs/components/image_diff_overlay_spec.js
+++ b/spec/frontend/diffs/components/image_diff_overlay_spec.js
@@ -24,6 +24,8 @@ describe('Diffs image diff overlay component', () => {
propsData: {
discussions: [...imageDiffDiscussions],
fileHash: 'ABC',
+ renderedWidth: 200,
+ renderedHeight: 200,
...props,
},
methods: {
@@ -71,8 +73,8 @@ describe('Diffs image diff overlay component', () => {
createComponent();
const imageBadges = getAllImageBadges();
- expect(imageBadges.at(0).attributes('style')).toBe('left: 10px; top: 10px;');
- expect(imageBadges.at(1).attributes('style')).toBe('left: 5px; top: 5px;');
+ expect(imageBadges.at(0).attributes('style')).toBe('left: 10%; top: 5%;');
+ expect(imageBadges.at(1).attributes('style')).toBe('left: 5%; top: 2.5%;');
});
it('renders single badge for discussion object', () => {
@@ -95,6 +97,8 @@ describe('Diffs image diff overlay component', () => {
y: 0,
width: 100,
height: 200,
+ xPercent: 0,
+ yPercent: 0,
});
});
@@ -120,11 +124,13 @@ describe('Diffs image diff overlay component', () => {
describe('comment form', () => {
const getCommentIndicator = () => wrapper.find('.comment-indicator');
beforeEach(() => {
- createComponent({}, store => {
+ createComponent({ canComment: true }, store => {
store.state.diffs.commentForms.push({
fileHash: 'ABC',
x: 20,
y: 10,
+ xPercent: 10,
+ yPercent: 10,
});
});
});
@@ -134,7 +140,7 @@ describe('Diffs image diff overlay component', () => {
});
it('sets comment form badge position', () => {
- expect(getCommentIndicator().attributes('style')).toBe('left: 20px; top: 10px;');
+ expect(getCommentIndicator().attributes('style')).toBe('left: 10%; top: 10%;');
});
});
});
diff --git a/spec/frontend/importer_status_spec.js b/spec/frontend/importer_status_spec.js
deleted file mode 100644
index 4ef74a2fe84..00000000000
--- a/spec/frontend/importer_status_spec.js
+++ /dev/null
@@ -1,141 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { ImporterStatus } from '~/importer_status';
-import axios from '~/lib/utils/axios_utils';
-
-describe('Importer Status', () => {
- let instance;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('addToImport', () => {
- const importUrl = '/import_url';
- const fixtures = `
- <table>
- <tr id="repo_123">
- <td class="import-target"></td>
- <td class="import-actions job-status">
- <button name="button" type="submit" class="btn btn-import js-add-to-import">
- </button>
- </td>
- </tr>
- </table>
- `;
-
- beforeEach(() => {
- setFixtures(fixtures);
- jest.spyOn(ImporterStatus.prototype, 'initStatusPage').mockImplementation(() => {});
- jest.spyOn(ImporterStatus.prototype, 'setAutoUpdate').mockImplementation(() => {});
- instance = new ImporterStatus({
- jobsUrl: '',
- importUrl,
- });
- });
-
- it('sets table row to active after post request', done => {
- mock.onPost(importUrl).reply(200, {
- id: 1,
- full_path: '/full_path',
- });
-
- instance
- .addToImport({
- currentTarget: document.querySelector('.js-add-to-import'),
- })
- .then(() => {
- expect(document.querySelector('tr').classList.contains('table-active')).toEqual(true);
- done();
- })
- .catch(done.fail);
- });
-
- it('shows error message after failed POST request', done => {
- setFixtures(`${fixtures}<div class="flash-container"></div>`);
-
- mock.onPost(importUrl).reply(422, {
- errors: 'You forgot your lunch',
- });
-
- instance
- .addToImport({
- currentTarget: document.querySelector('.js-add-to-import'),
- })
- .then(() => {
- const flashMessage = document.querySelector('.flash-text');
-
- expect(flashMessage.textContent.trim()).toEqual(
- 'An error occurred while importing project: You forgot your lunch',
- );
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('autoUpdate', () => {
- const jobsUrl = '/jobs_url';
-
- beforeEach(() => {
- const div = document.createElement('div');
- div.innerHTML = `
- <div id="project_1">
- <div class="job-status">
- </div>
- </div>
- `;
-
- document.body.appendChild(div);
-
- jest.spyOn(ImporterStatus.prototype, 'initStatusPage').mockImplementation(() => {});
- jest.spyOn(ImporterStatus.prototype, 'setAutoUpdate').mockImplementation(() => {});
- instance = new ImporterStatus({
- jobsUrl,
- });
- });
-
- function setupMock(importStatus) {
- mock.onGet(jobsUrl).reply(200, [
- {
- id: 1,
- import_status: importStatus,
- },
- ]);
- }
-
- function expectJobStatus(done, status) {
- instance
- .autoUpdate()
- .then(() => {
- expect(document.querySelector('#project_1').innerText.trim()).toEqual(status);
- done();
- })
- .catch(done.fail);
- }
-
- it('sets the job status to done', done => {
- setupMock('finished');
- expectJobStatus(done, 'Done');
- });
-
- it('sets the job status to scheduled', done => {
- setupMock('scheduled');
- expectJobStatus(done, 'Scheduled');
- });
-
- it('sets the job status to started', done => {
- setupMock('started');
- expectJobStatus(done, 'Started');
- });
-
- it('sets the job status to custom status', done => {
- setupMock('custom status');
- expectJobStatus(done, 'custom status');
- });
- });
-});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
index 3f03ebdb047..f45368bf443 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
@@ -1,40 +1,46 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
-
-describe('MRWidgetMissingBranch', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(missingBranchComponent);
- vm = mountComponent(Component, { mr: { sourceBranchRemoved: true } });
- });
-
- afterEach(() => {
- vm.$destroy();
+import { shallowMount } from '@vue/test-utils';
+import MissingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
+
+let wrapper;
+
+function factory(sourceBranchRemoved, mergeRequestWidgetGraphql) {
+ wrapper = shallowMount(MissingBranchComponent, {
+ propsData: {
+ mr: { sourceBranchRemoved },
+ },
+ provide: {
+ glFeatures: { mergeRequestWidgetGraphql },
+ },
});
- describe('computed', () => {
- describe('missingBranchName', () => {
- it('should return proper branch name', () => {
- expect(vm.missingBranchName).toEqual('source');
+ if (mergeRequestWidgetGraphql) {
+ wrapper.setData({ state: { sourceBranchExists: !sourceBranchRemoved } });
+ }
- vm.mr.sourceBranchRemoved = false;
+ return wrapper.vm.$nextTick();
+}
- expect(vm.missingBranchName).toEqual('target');
- });
- });
+describe('MRWidgetMissingBranch', () => {
+ afterEach(() => {
+ wrapper.destroy();
});
- describe('template', () => {
- it('should have correct elements', () => {
- const el = vm.$el;
- const content = el.textContent.replace(/\n(\s)+/g, ' ').trim();
-
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(content.replace(/\s\s+/g, ' ')).toContain('source branch does not exist.');
- expect(content).toContain('Please restore it or use a different source branch');
+ [true, false].forEach(mergeRequestWidgetGraphql => {
+ describe(`widget GraphQL feature flag is ${
+ mergeRequestWidgetGraphql ? 'enabled' : 'disabled'
+ }`, () => {
+ it.each`
+ sourceBranchRemoved | branchName
+ ${true} | ${'source'}
+ ${false} | ${'target'}
+ `(
+ 'should set missing branch name as $branchName when sourceBranchRemoved is $sourceBranchRemoved',
+ async ({ sourceBranchRemoved, branchName }) => {
+ await factory(sourceBranchRemoved, mergeRequestWidgetGraphql);
+
+ expect(wrapper.find('[data-testid="missingBranchName"]').text()).toContain(branchName);
+ },
+ );
});
});
});
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 9ebbf975903..6d869c50041 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -272,4 +272,65 @@ RSpec.describe UsersHelper do
end
end
end
+
+ describe '#user_display_name' do
+ subject { helper.user_display_name(user) }
+
+ before do
+ stub_current_user(nil)
+ end
+
+ context 'for a confirmed user' do
+ let(:user) { create(:user) }
+
+ before do
+ stub_profile_permission_allowed(true)
+ end
+
+ it { is_expected.to eq(user.name) }
+ end
+
+ context 'for an unconfirmed user' do
+ let(:user) { create(:user, :unconfirmed) }
+
+ before do
+ stub_profile_permission_allowed(false)
+ end
+
+ it { is_expected.to eq('Unconfirmed user') }
+
+ context 'when current user is an admin' do
+ before do
+ admin_user = create(:admin)
+ stub_current_user(admin_user)
+ stub_profile_permission_allowed(true, admin_user)
+ end
+
+ it { is_expected.to eq(user.name) }
+ end
+
+ context 'when the current user is self' do
+ before do
+ stub_current_user(user)
+ stub_profile_permission_allowed(true, user)
+ end
+
+ it { is_expected.to eq(user.name) }
+ end
+ end
+
+ context 'for a blocked user' do
+ let(:user) { create(:user, :blocked) }
+
+ it { is_expected.to eq('Blocked user') }
+ end
+
+ def stub_current_user(user)
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ def stub_profile_permission_allowed(allowed, current_user = nil)
+ allow(helper).to receive(:can?).with(user, :read_user_profile, current_user).and_return(allowed)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/ansi2json/result_spec.rb b/spec/lib/gitlab/ci/ansi2json/result_spec.rb
index 31c0da95f0a..b7b4d6de8b9 100644
--- a/spec/lib/gitlab/ci/ansi2json/result_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/result_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::Result do
{ lines: [], state: state, append: false, truncated: false, offset: offset, stream: stream }
end
- subject { described_class.new(params) }
+ subject { described_class.new(**params) }
describe '#size' do
before do
diff --git a/spec/lib/gitlab/ci/ansi2json/style_spec.rb b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
index d27a642ecf3..ff70ff69aaa 100644
--- a/spec/lib/gitlab/ci/ansi2json/style_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/style_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Ansi2json::Style do
describe '#set?' do
- subject { described_class.new(params).set? }
+ subject { described_class.new(**params).set? }
context 'when fg color is set' do
let(:params) { { fg: 'term-fg-black' } }
@@ -44,7 +44,7 @@ RSpec.describe Gitlab::Ci::Ansi2json::Style do
end
describe 'update formats to mimic terminals' do
- subject { described_class.new(params) }
+ subject { described_class.new(**params) }
context 'when fg color present' do
let(:params) { { fg: 'term-fg-black', mask: mask } }
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
index 77b8aa1d591..efe99cd276c 100644
--- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -142,7 +142,7 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata do
it 'reads expected number of entries' do
stream = File.open(tmpfile.path)
- metadata = described_class.new(stream, 'public', { recursive: true })
+ metadata = described_class.new(stream, 'public', recursive: true)
expect(metadata.find_entries!.count).to eq entry_count
end
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index c3d91057328..e810d65d560 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -81,7 +81,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) { { name: 'ruby:2.7', entrypoint: %w(/bin/sh run), ports: ports } }
- let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
diff --git a/spec/lib/gitlab/ci/config/entry/need_spec.rb b/spec/lib/gitlab/ci/config/entry/need_spec.rb
index 5a826bf8282..983e95fae42 100644
--- a/spec/lib/gitlab/ci/config/entry/need_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/need_spec.rb
@@ -165,6 +165,45 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Need do
end
end
+ context 'with cross pipeline artifacts needs' do
+ context 'when pipeline is provided' do
+ context 'when job is provided' do
+ let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID' } }
+
+ it { is_expected.to be_valid }
+
+ it 'sets artifacts:true by default' do
+ expect(need.value).to eq(job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: true)
+ end
+
+ it 'sets the type as cross_dependency' do
+ expect(need.type).to eq(:cross_dependency)
+ end
+ end
+
+ context 'when artifacts is provided' do
+ let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: false } }
+
+ it { is_expected.to be_valid }
+
+ it 'returns the correct value' do
+ expect(need.value).to eq(job: 'job_name', pipeline: '$THE_PIPELINE_ID', artifacts: false)
+ end
+ end
+ end
+
+ context 'when config contains not allowed keys' do
+ let(:config) { { job: 'job_name', pipeline: '$THE_PIPELINE_ID', something: 'else' } }
+
+ it { is_expected.not_to be_valid }
+
+ it 'returns an error' do
+ expect(need.errors)
+ .to contain_exactly('cross pipeline dependency config contains unknown keys: something')
+ end
+ end
+ end
+
context 'when need config is not a string or a hash' do
let(:config) { :job_name }
diff --git a/spec/lib/gitlab/ci/config/entry/needs_spec.rb b/spec/lib/gitlab/ci/config/entry/needs_spec.rb
index f3b9d0c3c84..f11f2a56f5f 100644
--- a/spec/lib/gitlab/ci/config/entry/needs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/needs_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do
subject(:needs) { described_class.new(config) }
before do
- needs.metadata[:allowed_needs] = %i[job]
+ needs.metadata[:allowed_needs] = %i[job cross_dependency]
end
describe 'validations' do
@@ -66,6 +66,27 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Needs do
end
end
end
+
+ context 'with too many cross pipeline dependencies' do
+ let(:limit) { described_class::NEEDS_CROSS_PIPELINE_DEPENDENCIES_LIMIT }
+
+ let(:config) do
+ Array.new(limit.next) do |index|
+ { pipeline: "$UPSTREAM_PIPELINE_#{index}", job: 'job-1' }
+ end
+ end
+
+ describe '#valid?' do
+ it { is_expected.not_to be_valid }
+ end
+
+ describe '#errors' do
+ it 'returns error about incorrect type' do
+ expect(needs.errors).to contain_exactly(
+ "needs config must be less than or equal to #{limit}")
+ end
+ end
+ end
end
describe '.compose!' do
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index ec137ef2ae4..2795cc9dddf 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -96,7 +96,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do
{ name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
end
- let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb
index e4f8a348d21..85e7f297b03 100644
--- a/spec/lib/gitlab/ci/config/entry/services_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Services do
context 'when configuration has ports' do
let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] }
- let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:entry) { described_class.new(config, with_image_ports: image_ports) }
let(:image_ports) { false }
context 'when with_image_ports metadata is not enabled' do
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index fb6395e888a..411337fc32f 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -2111,6 +2111,71 @@ module Gitlab
end
end
+ describe 'cross pipeline needs' do
+ context 'when configuration is valid' do
+ let(:config) do
+ <<~YAML
+ rspec:
+ stage: test
+ script: rspec
+ needs:
+ - pipeline: $THE_PIPELINE_ID
+ job: dependency-job
+ YAML
+ end
+
+ it 'returns a valid configuration and sets artifacts: true by default' do
+ expect(subject).to be_valid
+
+ rspec = subject.build_attributes(:rspec)
+ expect(rspec.dig(:options, :cross_dependencies)).to eq(
+ [{ pipeline: '$THE_PIPELINE_ID', job: 'dependency-job', artifacts: true }]
+ )
+ end
+
+ context 'when pipeline ID is hard-coded' do
+ let(:config) do
+ <<~YAML
+ rspec:
+ stage: test
+ script: rspec
+ needs:
+ - pipeline: "123"
+ job: dependency-job
+ YAML
+ end
+
+ it 'returns a valid configuration and sets artifacts: true by default' do
+ expect(subject).to be_valid
+
+ rspec = subject.build_attributes(:rspec)
+ expect(rspec.dig(:options, :cross_dependencies)).to eq(
+ [{ pipeline: '123', job: 'dependency-job', artifacts: true }]
+ )
+ end
+ end
+ end
+
+ context 'when configuration is not valid' do
+ let(:config) do
+ <<~YAML
+ rspec:
+ stage: test
+ script: rspec
+ needs:
+ - pipeline: $THE_PIPELINE_ID
+ job: dependency-job
+ something: else
+ YAML
+ end
+
+ it 'returns an error' do
+ expect(subject).not_to be_valid
+ expect(subject.errors).to include(/:need config contains unknown keys: something/)
+ end
+ end
+ end
+
describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute }
diff --git a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
index 719d4a69985..21503dc1501 100644
--- a/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
project.add_maintainer(user)
end
- let(:stage_summary) { described_class.new(project, options).data }
+ let(:stage_summary) { described_class.new(project, **options).data }
describe "#new_issues" do
subject { stage_summary.first }
@@ -121,7 +121,7 @@ RSpec.describe Gitlab::CycleAnalytics::StageSummary do
end
it 'does not include commit stats' do
- data = described_class.new(project, options).data
+ data = described_class.new(project, **options).data
expect(includes_commits?(data)).to be_falsy
end
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index 575ff7f357b..bc4c6cf007d 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -6,7 +6,7 @@ require "spec_helper"
RSpec.describe Gitlab::Email::ReplyParser do
describe '#execute' do
def test_parse_body(mail_string, params = {})
- described_class.new(Mail::Message.new(mail_string), params).execute
+ described_class.new(Mail::Message.new(mail_string), **params).execute
end
it "returns an empty string if the message is blank" do
diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb
deleted file mode 100644
index 402d2169432..00000000000
--- a/spec/lib/gitlab/google_code_import/client_spec.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-
-RSpec.describe Gitlab::GoogleCodeImport::Client do
- let(:raw_data) { Gitlab::Json.parse(fixture_file("GoogleCodeProjectHosting.json")) }
-
- subject { described_class.new(raw_data) }
-
- describe "#valid?" do
- context "when the data is valid" do
- it "returns true" do
- expect(subject).to be_valid
- end
- end
-
- context "when the data is invalid" do
- let(:raw_data) { "No clue" }
-
- it "returns true" do
- expect(subject).not_to be_valid
- end
- end
- end
-
- describe "#repos" do
- it "returns only Git repositories" do
- expect(subject.repos.length).to eq(1)
- expect(subject.incompatible_repos.length).to eq(1)
- end
- end
-
- describe "#repo" do
- it "returns the referenced repository" do
- expect(subject.repo("tint2").name).to eq("tint2")
- end
- end
-end
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
deleted file mode 100644
index a22e80ae1c0..00000000000
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-require "spec_helper"
-
-RSpec.describe Gitlab::GoogleCodeImport::Importer do
- let(:mapped_user) { create(:user, username: "thilo123") }
- let(:raw_data) { Gitlab::Json.parse(fixture_file("GoogleCodeProjectHosting.json")) }
- let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) }
- let(:import_data) do
- {
- 'repo' => client.repo('tint2').raw_data,
- 'user_map' => { 'thilo...' => "@#{mapped_user.username}" }
- }
- end
-
- let(:project) { create(:project) }
-
- subject { described_class.new(project) }
-
- before do
- project.add_maintainer(project.creator)
- project.create_import_data(data: import_data)
- end
-
- describe "#execute" do
- it "imports status labels" do
- subject.execute
-
- %w(New NeedInfo Accepted Wishlist Started Fixed Invalid Duplicate WontFix Incomplete).each do |status|
- expect(project.labels.find_by(name: "Status: #{status}")).not_to be_nil
- end
- end
-
- it "imports labels" do
- subject.execute
-
- %w(
- Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
- Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
- Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
- Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New
- ).each do |label|
- label = label.sub("-", ": ")
- expect(project.labels.find_by(name: label)).not_to be_nil
- end
- end
-
- it "imports issues" do
- subject.execute
-
- issue = project.issues.first
- expect(issue).not_to be_nil
- expect(issue.iid).to eq(169)
- expect(issue.author).to eq(project.creator)
- expect(issue.assignees).to eq([mapped_user])
- expect(issue.state).to eq("closed")
- expect(issue.label_names).to include("Priority: Medium")
- expect(issue.label_names).to include("Status: Fixed")
- expect(issue.label_names).to include("Type: Enhancement")
- expect(issue.title).to eq("Scrolling through tasks")
- expect(issue.state).to eq("closed")
- expect(issue.description).to include("schattenpr\\.\\.\\.")
- expect(issue.description).to include("November 18, 2009 00:20")
- expect(issue.description).to include("Google Code")
- expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel (like in fluxbox).')
- expect(issue.description).to include('Patch is attached that adds two new mouse-actions (next_task+prev_task)')
- expect(issue.description).to include('that can be used for exactly that purpose.')
- expect(issue.description).to include('all the best!')
- expect(issue.description).to include('[tint2_task_scrolling.diff](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff)')
- expect(issue.description).to include('![screenshot.png](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/screenshot.png)')
- expect(issue.description).to include('![screenshot1.PNG](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/screenshot1.PNG)')
- end
-
- it "imports issue comments" do
- subject.execute
-
- note = project.issues.first.notes.first
- expect(note).not_to be_nil
- expect(note.note).to include("Comment 1")
- expect(note.note).to include("@#{mapped_user.username}")
- expect(note.note).to include("November 18, 2009 05:14")
- expect(note.note).to include("applied, thanks.")
- expect(note.note).to include("Status: Fixed")
- expect(note.note).to include("~~Type: Defect~~")
- expect(note.note).to include("Type: Enhancement")
- end
- end
-end
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
deleted file mode 100644
index cfebe57aed3..00000000000
--- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::GoogleCodeImport::ProjectCreator do
- let(:user) { create(:user) }
- let(:repo) do
- Gitlab::GoogleCodeImport::Repository.new(
- "name" => 'vim',
- "summary" => 'VI Improved',
- "repositoryUrls" => ["https://vim.googlecode.com/git/"]
- )
- end
-
- let(:namespace) { create(:group) }
-
- before do
- namespace.add_owner(user)
- end
-
- it 'creates project' do
- expect_next_instance_of(Project) do |project|
- expect(project).to receive(:add_import_job)
- end
-
- project_creator = described_class.new(repo, namespace, user)
- project = project_creator.execute
-
- expect(project.import_url).to eq("https://vim.googlecode.com/git/")
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
- end
-end
diff --git a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
index 84e8f8b95e8..d2475d1edb9 100644
--- a/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/externally_paginated_array_connection_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe Gitlab::Graphql::Pagination::ExternallyPaginatedArrayConnection d
let(:arguments) { {} }
subject(:connection) do
- described_class.new(all_nodes, { max_page_size: values.size }.merge(arguments))
+ described_class.new(all_nodes, **{ max_page_size: values.size }.merge(arguments))
end
it_behaves_like 'a connection with collection methods'
diff --git a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
index 1fee24bdc1f..0ac54a20fcc 100644
--- a/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
+++ b/spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb
@@ -10,11 +10,11 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::Connection do
let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) }
subject(:connection) do
- described_class.new(nodes, { context: context, max_page_size: 3 }.merge(arguments))
+ described_class.new(nodes, **{ context: context, max_page_size: 3 }.merge(arguments))
end
def encoded_cursor(node)
- described_class.new(nodes, { context: context }).cursor_for(node)
+ described_class.new(nodes, context: context).cursor_for(node)
end
def decoded_cursor(cursor)
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 0dfd8a2ee50..b0bc1029a53 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -11,7 +11,6 @@ RSpec.describe Gitlab::ImportSources do
'Bitbucket Cloud' => 'bitbucket',
'Bitbucket Server' => 'bitbucket_server',
'GitLab.com' => 'gitlab',
- 'Google Code' => 'google_code',
'FogBugz' => 'fogbugz',
'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project',
@@ -32,7 +31,6 @@ RSpec.describe Gitlab::ImportSources do
bitbucket
bitbucket_server
gitlab
- google_code
fogbugz
git
gitlab_project
@@ -53,7 +51,6 @@ RSpec.describe Gitlab::ImportSources do
bitbucket
bitbucket_server
gitlab
- google_code
fogbugz
gitlab_project
gitea
@@ -70,7 +67,6 @@ RSpec.describe Gitlab::ImportSources do
'bitbucket' => Gitlab::BitbucketImport::Importer,
'bitbucket_server' => Gitlab::BitbucketServerImport::Importer,
'gitlab' => Gitlab::GitlabImport::Importer,
- 'google_code' => Gitlab::GoogleCodeImport::Importer,
'fogbugz' => Gitlab::FogbugzImport::Importer,
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
@@ -92,7 +88,6 @@ RSpec.describe Gitlab::ImportSources do
'bitbucket' => 'Bitbucket Cloud',
'bitbucket_server' => 'Bitbucket Server',
'gitlab' => 'GitLab.com',
- 'google_code' => 'Google Code',
'fogbugz' => 'FogBugz',
'git' => 'Repo by URL',
'gitlab_project' => 'GitLab export',
diff --git a/spec/models/ci/build_dependencies_spec.rb b/spec/models/ci/build_dependencies_spec.rb
index 4fa1b3eb5a5..623320370b1 100644
--- a/spec/models/ci/build_dependencies_spec.rb
+++ b/spec/models/ci/build_dependencies_spec.rb
@@ -155,9 +155,9 @@ RSpec.describe Ci::BuildDependencies do
subject { dependencies.all }
- it 'returns the union of all local dependencies and any cross pipeline dependencies' do
+ it 'returns the union of all local dependencies and any cross project dependencies' do
expect(dependencies).to receive(:local).and_return([1, 2, 3])
- expect(dependencies).to receive(:cross_pipeline).and_return([3, 4])
+ expect(dependencies).to receive(:cross_project).and_return([3, 4])
expect(subject).to contain_exactly(1, 2, 3, 4)
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index b4da57ed89b..26f8271a180 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -3024,6 +3024,17 @@ RSpec.describe Project, factory_default: :keep do
expect { project.set_repository_read_only! }.to raise_error(described_class::RepositoryReadOnlyError, /in progress/)
end
+
+ context 'skip_git_transfer_check is true' do
+ it 'makes the project read-only when git transfers are in progress' do
+ allow(project).to receive(:git_transfer_in_progress?) { true }
+
+ expect { project.set_repository_read_only!(skip_git_transfer_check: true) }
+ .to change(project, :repository_read_only?)
+ .from(false)
+ .to(true)
+ end
+ end
end
describe '#set_repository_writable!' do
diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb
index 17ac7d0e44d..78212f06526 100644
--- a/spec/policies/user_policy_spec.rb
+++ b/spec/policies/user_policy_spec.rb
@@ -160,4 +160,16 @@ RSpec.describe UserPolicy do
it { is_expected.not_to be_allowed(:read_group_count) }
end
end
+
+ describe ':read_user_profile' do
+ context 'when the user is unconfirmed' do
+ let(:user) { create(:user, :unconfirmed) }
+
+ it { is_expected.not_to be_allowed(:read_user_profile) }
+ end
+
+ context 'when the user is confirmed' do
+ it { is_expected.to be_allowed(:read_user_profile) }
+ end
+ end
end
diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb
index ecf4c75b52f..923cd158c98 100644
--- a/spec/requests/api/project_repository_storage_moves_spec.rb
+++ b/spec/requests/api/project_repository_storage_moves_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe API::ProjectRepositoryStorageMoves do
include AccessMatchersForRequest
let_it_be(:user) { create(:admin) }
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, :repository).tap { |project| project.track_project_repository } }
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, project: project) }
shared_examples 'get single project repository storage move' do
@@ -159,4 +159,64 @@ RSpec.describe API::ProjectRepositoryStorageMoves do
end
end
end
+
+ describe 'POST /project_repository_storage_moves' do
+ let(:source_storage_name) { 'default' }
+ let(:destination_storage_name) { 'test_second_storage' }
+
+ def create_project_repository_storage_moves
+ post api('/project_repository_storage_moves', user), params: {
+ source_storage_name: source_storage_name,
+ destination_storage_name: destination_storage_name
+ }
+ end
+
+ before do
+ stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
+ end
+
+ it 'schedules the worker' do
+ expect(ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
+
+ create_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+
+ context 'source_storage_name is invalid' do
+ let(:destination_storage_name) { 'not-a-real-storage' }
+
+ it 'gives an error' do
+ create_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'destination_storage_name is missing' do
+ let(:destination_storage_name) { nil }
+
+ it 'schedules the worker' do
+ expect(ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
+
+ create_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+
+ context 'destination_storage_name is invalid' do
+ let(:destination_storage_name) { 'not-a-real-storage' }
+
+ it 'gives an error' do
+ create_project_repository_storage_moves
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ describe 'normal user' do
+ it { expect { create_project_repository_storage_moves }.to be_denied_for(:user) }
+ end
+ end
end
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
index 15d2f32de78..b1da2eaa33b 100644
--- a/spec/routing/import_routing_spec.rb
+++ b/spec/routing/import_routing_spec.rb
@@ -126,32 +126,6 @@ RSpec.describe Import::BitbucketServerController, 'routing' do
end
end
-# status_import_google_code GET /import/google_code/status(.:format) import/google_code#status
-# callback_import_google_code POST /import/google_code/callback(.:format) import/google_code#callback
-# jobs_import_google_code GET /import/google_code/jobs(.:format) import/google_code#jobs
-# new_user_map_import_google_code GET /import/google_code/user_map(.:format) import/google_code#new_user_map
-# create_user_map_import_google_code POST /import/google_code/user_map(.:format) import/google_code#create_user_map
-# import_google_code POST /import/google_code(.:format) import/google_code#create
-# new_import_google_code GET /import/google_code/new(.:format) import/google_code#new
-RSpec.describe Import::GoogleCodeController, 'routing' do
- it_behaves_like 'importer routing' do
- let(:except_actions) { [:callback] }
- let(:provider) { 'google_code' }
- end
-
- it 'to #callback' do
- expect(post("/import/google_code/callback")).to route_to("import/google_code#callback")
- end
-
- it 'to #new_user_map' do
- expect(get('/import/google_code/user_map')).to route_to('import/google_code#new_user_map')
- end
-
- it 'to #create_user_map' do
- expect(post('/import/google_code/user_map')).to route_to('import/google_code#create_user_map')
- end
-end
-
# status_import_fogbugz GET /import/fogbugz/status(.:format) import/fogbugz#status
# callback_import_fogbugz POST /import/fogbugz/callback(.:format) import/fogbugz#callback
# realtime_changes_import_fogbugz GET /import/fogbugz/realtime_changes(.:format) import/fogbugz#realtime_changes
diff --git a/spec/rubocop/cop/lint/last_keyword_argument_spec.rb b/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
index f942390569b..5822bf74e8d 100644
--- a/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
+++ b/spec/rubocop/cop/lint/last_keyword_argument_spec.rb
@@ -46,6 +46,9 @@ RSpec.describe RuboCop::Cop::Lint::LastKeywordArgument, type: :rubocop do
---
test_api/projects_get_/projects_when_unauthenticated_behaves_like_projects_response_returns_an_array_of_projects:
- |
+ DEPRECATION WARNING: /Users/tkuah/code/ee-gdk/gitlab/projects_spec.rb:1: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
+ /Users/tkuah/code/ee-gdk/gitlab/lib/gitlab/project.rb:15: warning: The called method `initialize' is defined here
+ - |
DEPRECATION WARNING: /Users/tkuah/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/state_machines-activerecord-0.6.0/lib/state_machines/integrations/active_record.rb:511: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
/Users/tkuah/.rbenv/versions/2.7.2/lib/ruby/gems/2.7.0/gems/activerecord-6.0.3.3/lib/active_record/suppressor.rb:43: warning: The called method `save' is defined here
- |
@@ -70,6 +73,17 @@ RSpec.describe RuboCop::Cop::Lint::LastKeywordArgument, type: :rubocop do
SOURCE
end
+ it 'registers an offense for the new method call' do
+ expect_offense(<<~SOURCE, 'projects_spec.rb')
+ Project.new(params)
+ ^^^^^^ Using the last argument as keyword parameters is deprecated
+ SOURCE
+
+ expect_correction(<<~SOURCE)
+ Project.new(**params)
+ SOURCE
+ end
+
it 'registers an offense and corrects by converting hash to kwarg' do
expect_offense(<<~SOURCE, 'create_service.rb')
users.call(id, { a: :b, c: :d })
diff --git a/spec/services/ci/create_job_artifacts_service_spec.rb b/spec/services/ci/create_job_artifacts_service_spec.rb
index 72b0d220b11..29e51a23dea 100644
--- a/spec/services/ci/create_job_artifacts_service_spec.rb
+++ b/spec/services/ci/create_job_artifacts_service_spec.rb
@@ -24,7 +24,7 @@ RSpec.describe Ci::CreateJobArtifactsService do
upload = Tempfile.new('upload')
FileUtils.copy(path, upload.path)
- UploadedFile.new(upload.path, params)
+ UploadedFile.new(upload.path, **params)
end
describe '#execute' do
diff --git a/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb b/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
new file mode 100644
index 00000000000..5b76386bfab
--- /dev/null
+++ b/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ScheduleBulkRepositoryShardMovesService do
+ before do
+ stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
+ end
+
+ let!(:project) { create(:project, :repository).tap { |project| project.track_project_repository } }
+ let(:source_storage_name) { 'default' }
+ let(:destination_storage_name) { 'test_second_storage' }
+
+ describe '#execute' do
+ it 'schedules project repository storage moves' do
+ expect { subject.execute(source_storage_name, destination_storage_name) }
+ .to change(ProjectRepositoryStorageMove, :count).by(1)
+
+ storage_move = project.repository_storage_moves.last!
+
+ expect(storage_move).to have_attributes(
+ source_storage_name: source_storage_name,
+ destination_storage_name: destination_storage_name,
+ state_name: :scheduled
+ )
+ end
+
+ context 'read-only repository' do
+ let!(:project) { create(:project, :repository, :read_only).tap { |project| project.track_project_repository } }
+
+ it 'does not get scheduled' do
+ expect(subject).to receive(:log_info)
+ .with("Project #{project.full_path} (#{project.id}) was skipped: Project is read only")
+ expect { subject.execute(source_storage_name, destination_storage_name) }
+ .to change(ProjectRepositoryStorageMove, :count).by(0)
+ end
+ end
+ end
+
+ describe '.enqueue' do
+ it 'defers to the worker' do
+ expect(::ProjectScheduleBulkRepositoryShardMovesWorker).to receive(:perform_async).with(source_storage_name, destination_storage_name)
+
+ described_class.enqueue(source_storage_name, destination_storage_name)
+ end
+ end
+end
diff --git a/spec/support/caching.rb b/spec/support/caching.rb
index 883d531550a..11e4f534971 100644
--- a/spec/support/caching.rb
+++ b/spec/support/caching.rb
@@ -25,7 +25,7 @@ RSpec.configure do |config|
original_null_store = Rails.cache
caching_config_hash = Gitlab::Redis::Cache.params
caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE
- Rails.cache = ActiveSupport::Cache::RedisCacheStore.new(caching_config_hash)
+ Rails.cache = ActiveSupport::Cache::RedisCacheStore.new(**caching_config_hash)
redis_cache_cleanup!
diff --git a/spec/support/graphql/var.rb b/spec/support/graphql/var.rb
new file mode 100644
index 00000000000..4f2c774e898
--- /dev/null
+++ b/spec/support/graphql/var.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Graphql
+ # Helper to pass variables around generated queries.
+ #
+ # e.g.:
+ # first = var('Int')
+ # after = var('String')
+ #
+ # query = with_signature(
+ # [first, after],
+ # query_graphql_path([
+ # [:project, { full_path: project.full_path }],
+ # [:issues, { after: after, first: first }]
+ # :nodes
+ # ], all_graphql_fields_for('Issue'))
+ # )
+ #
+ # post_graphql(query, variables: [first.with(2), after.with(some_cursor)])
+ #
+ class Var
+ attr_reader :name, :type
+ attr_accessor :value
+
+ def initialize(name, type)
+ @name = name
+ @type = type
+ end
+
+ def sig
+ "#{to_graphql_value}: #{type}"
+ end
+
+ def to_graphql_value
+ "$#{name}"
+ end
+
+ # We return a new object so that running the same query twice with
+ # different values does not risk re-using the value
+ #
+ # e.g.
+ #
+ # x = var('Int')
+ # expect { post_graphql(query, variables: x) }
+ # .to issue_same_number_of_queries_as { post_graphql(query, variables: x.with(1)) }
+ #
+ # Here we post the `x` variable once with the value set to 1, and once with
+ # the value set to `nil`.
+ def with(value)
+ copy = Var.new(name, type)
+ copy.value = value
+ copy
+ end
+
+ def to_h
+ { name => value }
+ end
+ end
+end
diff --git a/spec/support/helpers/gitlab_verify_helpers.rb b/spec/support/helpers/gitlab_verify_helpers.rb
index 9901ce374ed..2a6ba8aaff4 100644
--- a/spec/support/helpers/gitlab_verify_helpers.rb
+++ b/spec/support/helpers/gitlab_verify_helpers.rb
@@ -2,7 +2,7 @@
module GitlabVerifyHelpers
def collect_ranges(args = {})
- verifier = described_class.new(args.merge(batch_size: 1))
+ verifier = described_class.new(**args.merge(batch_size: 1))
collect_results(verifier).map { |range, _| range }
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index 9b6505cc649..87c1e63feb6 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -4,6 +4,7 @@ module GraphqlHelpers
MutationDefinition = Struct.new(:query, :variables)
NoData = Class.new(StandardError)
+ UnauthorizedObject = Class.new(StandardError)
# makes an underscored string look like a fieldname
# "merge_request" => "mergeRequest"
@@ -17,7 +18,10 @@ module GraphqlHelpers
# ready, then the early return is returned instead.
#
# Then the resolve method is called.
- def resolve(resolver_class, args: {}, **resolver_args)
+ def resolve(resolver_class, args: {}, lookahead: :not_given, parent: :not_given, **resolver_args)
+ args = aliased_args(resolver_class, args)
+ args[:parent] = parent unless parent == :not_given
+ args[:lookahead] = lookahead unless lookahead == :not_given
resolver = resolver_instance(resolver_class, **resolver_args)
ready, early_return = sync_all { resolver.ready?(**args) }
@@ -26,6 +30,15 @@ module GraphqlHelpers
resolver.resolve(**args)
end
+ # TODO: Remove this method entirely when GraphqlHelpers uses real resolve_field
+ def aliased_args(resolver, args)
+ definitions = resolver.arguments
+
+ args.transform_keys do |k|
+ definitions[GraphqlHelpers.fieldnamerize(k)]&.keyword || k
+ end
+ end
+
def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema)
if ctx.is_a?(Hash)
q = double('Query', schema: schema)
@@ -111,24 +124,25 @@ module GraphqlHelpers
def variables_for_mutation(name, input)
graphql_input = prepare_input_for_mutation(input)
- result = { input_variable_name_for_mutation(name) => graphql_input }
+ { input_variable_name_for_mutation(name) => graphql_input }
+ end
- # Avoid trying to serialize multipart data into JSON
- if graphql_input.values.none? { |value| io_value?(value) }
- result.to_json
- else
- result
- end
+ def serialize_variables(variables)
+ return unless variables
+ return variables if variables.is_a?(String)
+
+ ::Gitlab::Utils::MergeHash.merge(Array.wrap(variables).map(&:to_h)).to_json
end
- def resolve_field(name, object, args = {})
- context = double("Context",
- schema: GitlabSchema,
- query: GraphQL::Query.new(GitlabSchema),
- parent: nil)
- field = described_class.fields[::GraphqlHelpers.fieldnamerize(name)]
+ def resolve_field(name, object, args = {}, current_user: nil)
+ q = GraphQL::Query.new(GitlabSchema)
+ context = GraphQL::Query::Context.new(query: q, object: object, values: { current_user: current_user })
+ allow(context).to receive(:parent).and_return(nil)
+ field = described_class.fields.fetch(GraphqlHelpers.fieldnamerize(name))
instance = described_class.authorized_new(object, context)
- field.resolve_field(instance, {}, context)
+ raise UnauthorizedObject unless instance
+
+ field.resolve_field(instance, args, context)
end
# Recursively convert a Hash with Ruby-style keys to GraphQL fieldname-style keys
@@ -165,10 +179,32 @@ module GraphqlHelpers
end
def query_graphql_field(name, attributes = {}, fields = nil)
- <<~QUERY
- #{field_with_params(name, attributes)}
- #{wrap_fields(fields || all_graphql_fields_for(name.to_s.classify))}
- QUERY
+ attributes, fields = [nil, attributes] if fields.nil? && !attributes.is_a?(Hash)
+
+ field = field_with_params(name, attributes)
+
+ field + wrap_fields(fields || all_graphql_fields_for(name.to_s.classify)).to_s
+ end
+
+ def page_info_selection
+ "pageInfo { hasNextPage hasPreviousPage endCursor startCursor }"
+ end
+
+ def query_nodes(name, fields = nil, args: nil, of: name, include_pagination_info: false, max_depth: 1)
+ fields ||= all_graphql_fields_for(of.to_s.classify, max_depth: max_depth)
+ node_selection = include_pagination_info ? "#{page_info_selection} nodes" : :nodes
+ query_graphql_path([[name, args], node_selection], fields)
+ end
+
+ # e.g:
+ # query_graphql_path(%i[foo bar baz], all_graphql_fields_for('Baz'))
+ # => foo { bar { baz { x y z } } }
+ def query_graphql_path(segments, fields = nil)
+ # we really want foldr here...
+ segments.reverse.reduce(fields) do |tail, segment|
+ name, args = Array.wrap(segment)
+ query_graphql_field(name, args, tail)
+ end
end
def wrap_fields(fields)
@@ -233,6 +269,14 @@ module GraphqlHelpers
end.join(", ")
end
+ def with_signature(variables, query)
+ %Q[query(#{variables.map(&:sig).join(', ')}) #{query}]
+ end
+
+ def var(type)
+ ::Graphql::Var.new(generate(:variable), type)
+ end
+
# Fairly dumb Ruby => GraphQL rendering function. Only suitable for testing.
# Use symbol for Enum values
def as_graphql_literal(value)
@@ -245,7 +289,12 @@ module GraphqlHelpers
when nil then 'null'
when true then 'true'
when false then 'false'
- else raise ArgumentError, "Cannot represent #{value} as GraphQL literal"
+ else
+ if value.respond_to?(:to_graphql_value)
+ value.to_graphql_value
+ else
+ raise ArgumentError, "Cannot represent #{value} as GraphQL literal"
+ end
end
end
@@ -254,7 +303,7 @@ module GraphqlHelpers
end
def post_graphql(query, current_user: nil, variables: nil, headers: {})
- params = { query: query, variables: variables&.to_json }
+ params = { query: query, variables: serialize_variables(variables) }
post api('/', current_user, version: 'graphql'), params: params, headers: headers
end
@@ -332,13 +381,19 @@ module GraphqlHelpers
graphql_dig_at(graphql_data, *path)
end
+ # Slightly more powerful than just `dig`:
+ # - also supports implicit flat-mapping (.e.g. :foo :nodes :bar :nodes)
def graphql_dig_at(data, *path)
keys = path.map { |segment| segment.is_a?(Integer) ? segment : GraphqlHelpers.fieldnamerize(segment) }
# Allows for array indexing, like this
# ['project', 'boards', 'edges', 0, 'node', 'lists']
keys.reduce(data) do |memo, key|
- memo.is_a?(Array) ? memo[key] : memo&.dig(key)
+ if memo.is_a?(Array)
+ key.is_a?(Integer) ? memo[key] : memo.flat_map { |e| Array.wrap(e[key]) }
+ else
+ memo&.dig(key)
+ end
end
end
@@ -498,6 +553,20 @@ module GraphqlHelpers
variables: {}
)
end
+
+ # A lookahead that selects everything
+ def positive_lookahead
+ double(selects?: true).tap do |selection|
+ allow(selection).to receive(:selection).and_return(selection)
+ end
+ end
+
+ # A lookahead that selects nothing
+ def negative_lookahead
+ double(selects?: false).tap do |selection|
+ allow(selection).to receive(:selection).and_return(selection)
+ end
+ end
end
# This warms our schema, doing this as part of loading the helpers to avoid
diff --git a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
index fa929d5b791..fd0639b628e 100644
--- a/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/shardable_shared_examples.rb
@@ -18,4 +18,10 @@ RSpec.shared_examples 'shardable scopes' do
expect(described_class.excluding_repository_storage('default')).to eq([record_2])
end
end
+
+ describe '.for_shard' do
+ it 'returns the objects for a given shard' do
+ expect(described_class.for_shard(record_1.shard)).to eq([record_1])
+ end
+ end
end
diff --git a/spec/support_specs/graphql/var_spec.rb b/spec/support_specs/graphql/var_spec.rb
new file mode 100644
index 00000000000..f708f01a11e
--- /dev/null
+++ b/spec/support_specs/graphql/var_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Graphql::Var do
+ subject(:var) { described_class.new('foo', 'Int') }
+
+ it 'associates a name with a type and an initially empty value' do
+ expect(var).to have_attributes(
+ name: 'foo',
+ type: 'Int',
+ value: be_nil
+ )
+ end
+
+ it 'has a correct signature' do
+ expect(var).to have_attributes(sig: '$foo: Int')
+ end
+
+ it 'implements to_graphql_value as $name' do
+ expect(var.to_graphql_value).to eq('$foo')
+ end
+
+ it 'can set a value using with, returning a new object' do
+ with_value = var.with(42)
+
+ expect(with_value).to have_attributes(name: 'foo', type: 'Int', value: 42)
+ expect(var).to have_attributes(value: be_nil)
+ end
+
+ it 'returns an object suitable for passing to post_graphql(variables:)' do
+ expect(var.with(17).to_h).to eq('foo' => 17)
+ end
+end
diff --git a/spec/support_specs/helpers/graphql_helpers_spec.rb b/spec/support_specs/helpers/graphql_helpers_spec.rb
index bc777621674..47e5192cdc9 100644
--- a/spec/support_specs/helpers/graphql_helpers_spec.rb
+++ b/spec/support_specs/helpers/graphql_helpers_spec.rb
@@ -5,6 +5,223 @@ require 'spec_helper'
RSpec.describe GraphqlHelpers do
include GraphqlHelpers
+ # Normalize irrelevant whitespace to make comparison easier
+ def norm(query)
+ query.tr("\n", ' ').gsub(/\s+/, ' ').strip
+ end
+
+ describe 'graphql_dig_at' do
+ it 'transforms symbol keys to graphql field names' do
+ data = { 'camelCased' => 'names' }
+
+ expect(graphql_dig_at(data, :camel_cased)).to eq('names')
+ end
+
+ it 'supports integer indexing' do
+ data = { 'array' => [:boom, { 'id' => :hooray! }, :boom] }
+
+ expect(graphql_dig_at(data, :array, 1, :id)).to eq(:hooray!)
+ end
+
+ it 'gracefully degrades to nil' do
+ data = { 'project' => { 'mergeRequest' => nil } }
+
+ expect(graphql_dig_at(data, :project, :merge_request, :id)).to be_nil
+ end
+
+ it 'supports implicitly flat-mapping traversals' do
+ data = {
+ 'foo' => {
+ 'nodes' => [
+ { 'bar' => { 'nodes' => [{ 'id' => 1 }, { 'id' => 2 }] } },
+ { 'bar' => { 'nodes' => [{ 'id' => 3 }, { 'id' => 4 }] } },
+ { 'bar' => nil }
+ ]
+ },
+ 'irrelevant' => 'the field is a red-herring'
+ }
+
+ expect(graphql_dig_at(data, :foo, :nodes, :bar, :nodes, :id)).to eq([1, 2, 3, 4])
+ end
+ end
+
+ describe 'var' do
+ it 'allocates a fresh name for each var' do
+ a = var('Int')
+ b = var('Int')
+
+ expect(a.name).not_to eq(b.name)
+ end
+
+ it 'can be used to construct correct signatures' do
+ a = var('Int')
+ b = var('String!')
+
+ q = with_signature([a, b], '{ foo bar }')
+
+ expect(q).to eq("query(#{a.to_graphql_value}: Int, #{b.to_graphql_value}: String!) { foo bar }")
+ end
+
+ it 'can be used to pass arguments to fields' do
+ a = var('ID!')
+
+ q = graphql_query_for(:project, { full_path: a }, :id)
+
+ expect(norm(q)).to eq("{ project(fullPath: #{a.to_graphql_value}){ id } }")
+ end
+
+ it 'can associate values with variables' do
+ a = var('Int')
+
+ expect(a.with(3).to_h).to eq(a.name => 3)
+ end
+
+ it 'does not mutate the variable when providing a value' do
+ a = var('Int')
+ three = a.with(3)
+
+ expect(three.value).to eq(3)
+ expect(a.value).to be_nil
+ end
+
+ it 'can associate many values with variables' do
+ a = var('Int').with(3)
+ b = var('String').with('foo')
+
+ expect(serialize_variables([a, b])).to eq({ a.name => 3, b.name => 'foo' }.to_json)
+ end
+ end
+
+ describe '.query_nodes' do
+ it 'can produce a basic connection selection' do
+ selection = query_nodes(:users)
+
+ expected = query_graphql_path([:users, :nodes], all_graphql_fields_for('User', max_depth: 1))
+
+ expect(selection).to eq(expected)
+ end
+
+ it 'allows greater depth' do
+ selection = query_nodes(:users, max_depth: 2)
+
+ expected = query_graphql_path([:users, :nodes], all_graphql_fields_for('User', max_depth: 2))
+
+ expect(selection).to eq(expected)
+ end
+
+ it 'accepts fields' do
+ selection = query_nodes(:users, :id)
+
+ expected = query_graphql_path([:users, :nodes], :id)
+
+ expect(selection).to eq(expected)
+ end
+
+ it 'accepts arguments' do
+ args = { username: 'foo' }
+ selection = query_nodes(:users, args: args)
+
+ expected = query_graphql_path([[:users, args], :nodes], all_graphql_fields_for('User', max_depth: 1))
+
+ expect(selection).to eq(expected)
+ end
+
+ it 'accepts arguments and fields' do
+ selection = query_nodes(:users, :id, args: { username: 'foo' })
+
+ expected = query_graphql_path([[:users, { username: 'foo' }], :nodes], :id)
+
+ expect(selection).to eq(expected)
+ end
+
+ it 'accepts explicit type name' do
+ selection = query_nodes(:members, of: 'User')
+
+ expected = query_graphql_path([:members, :nodes], all_graphql_fields_for('User', max_depth: 1))
+
+ expect(selection).to eq(expected)
+ end
+
+ it 'can optionally provide pagination info' do
+ selection = query_nodes(:users, include_pagination_info: true)
+
+ expected = query_graphql_path([:users, "#{page_info_selection} nodes"], all_graphql_fields_for('User', max_depth: 1))
+
+ expect(selection).to eq(expected)
+ end
+ end
+
+ describe '.query_graphql_path' do
+ it 'can build nested paths' do
+ selection = query_graphql_path(%i[foo bar wibble_wobble], :id)
+
+ expected = norm(<<-GQL)
+ foo{
+ bar{
+ wibbleWobble{
+ id
+ }
+ }
+ }
+ GQL
+
+ expect(norm(selection)).to eq(expected)
+ end
+
+ it 'can insert arguments at any point' do
+ selection = query_graphql_path(
+ [:foo, [:bar, { quux: true }], [:wibble_wobble, { eccentricity: :HIGH }]],
+ :id
+ )
+
+ expected = norm(<<-GQL)
+ foo{
+ bar(quux: true){
+ wibbleWobble(eccentricity: HIGH){
+ id
+ }
+ }
+ }
+ GQL
+
+ expect(norm(selection)).to eq(expected)
+ end
+ end
+
+ describe '.attributes_to_graphql' do
+ it 'can serialize hashes to literal arguments' do
+ x = var('Int')
+ args = {
+ an_array: [1, nil, "foo", true, [:foo, :bar]],
+ a_hash: {
+ nested: true,
+ value: "bar"
+ },
+ an_int: 42,
+ a_float: 0.1,
+ a_string: "wibble",
+ an_enum: :LOW,
+ null: nil,
+ a_bool: false,
+ a_var: x
+ }
+
+ literal = attributes_to_graphql(args)
+
+ expect(norm(literal)).to eq(norm(<<~EXP))
+ anArray: [1,null,"foo",true,[foo,bar]],
+ aHash: {nested: true, value: "bar"},
+ anInt: 42,
+ aFloat: 0.1,
+ aString: "wibble",
+ anEnum: LOW,
+ null: null,
+ aBool: false,
+ aVar: #{x.to_graphql_value}
+ EXP
+ end
+ end
+
describe '.graphql_mutation' do
shared_examples 'correct mutation definition' do
it 'returns correct mutation definition' do
@@ -15,7 +232,7 @@ RSpec.describe GraphqlHelpers do
}
}
MUTATION
- variables = %q({"updateAlertStatusInput":{"projectPath":"test/project"}})
+ variables = { "updateAlertStatusInput" => { "projectPath" => "test/project" } }
is_expected.to eq(GraphqlHelpers::MutationDefinition.new(query, variables))
end
diff --git a/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb b/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb
new file mode 100644
index 00000000000..aadfae51906
--- /dev/null
+++ b/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProjectScheduleBulkRepositoryShardMovesWorker do
+ describe "#perform" do
+ before do
+ stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' })
+
+ allow(ProjectUpdateRepositoryStorageWorker).to receive(:perform_async)
+ end
+
+ let!(:project) { create(:project, :repository).tap { |project| project.track_project_repository } }
+ let(:source_storage_name) { 'default' }
+ let(:destination_storage_name) { 'test_second_storage' }
+
+ include_examples 'an idempotent worker' do
+ let(:job_args) { [source_storage_name, destination_storage_name] }
+
+ it 'schedules project repository storage moves' do
+ expect { subject }.to change(ProjectRepositoryStorageMove, :count).by(1)
+
+ storage_move = project.repository_storage_moves.last!
+
+ expect(storage_move).to have_attributes(
+ source_storage_name: source_storage_name,
+ destination_storage_name: destination_storage_name,
+ state_name: :scheduled
+ )
+ end
+ end
+ end
+end