diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-21 21:10:08 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-21 21:10:08 +0300 |
commit | 5d41ea8c8e83ff6054ba4303ec8dc9bc33556602 (patch) | |
tree | f69079c9fac34f1e5e785d1f18d2f9ad339e4cbc /spec | |
parent | a09c6d7e91de9abab7e2ea8dffce1cbb89bf95d8 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
34 files changed, 601 insertions, 478 deletions
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb index 55e8ab7885e..4d1a07a6a75 100644 --- a/spec/commands/sidekiq_cluster/cli_spec.rb +++ b/spec/commands/sidekiq_cluster/cli_spec.rb @@ -245,9 +245,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo it 'expands multiple queue groups correctly' do expected_workers = if Gitlab.ee? - [%w[chat_notification], %w[project_export project_template_export]] + [%w[chat_notification], %w[project_export projects_import_export_relation_export project_template_export]] else - [%w[chat_notification], %w[project_export]] + [%w[chat_notification], %w[project_export projects_import_export_relation_export]] end expect(Gitlab::SidekiqCluster) diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb index fa866beb773..cf9760bcd7f 100644 --- a/spec/features/merge_requests/user_mass_updates_spec.rb +++ b/spec/features/merge_requests/user_mass_updates_spec.rb @@ -9,8 +9,6 @@ RSpec.describe 'Merge requests > User mass updates', :js do let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) } before do - stub_feature_flags(mr_attention_requests: false) - project.add_maintainer(user) project.add_maintainer(user2) sign_in(user) @@ -63,18 +61,6 @@ RSpec.describe 'Merge requests > User mass updates', :js do expect(find('.merge-request')).to have_link "Assigned to #{user.name}" end - - describe 'with attention requests feature flag on' do - before do - stub_feature_flags(mr_attention_requests: true) - end - - it 'updates merge request with assignee' do - change_assignee(user2.name) - - expect(find('.issuable-meta a.author-link')[:title]).to eq "Attention requested from assignee #{user2.name}" - end - end end describe 'remove assignee' do diff --git a/spec/frontend/attention_requests/components/navigation_popover_spec.js b/spec/frontend/attention_requests/components/navigation_popover_spec.js deleted file mode 100644 index e4d53d5dbdb..00000000000 --- a/spec/frontend/attention_requests/components/navigation_popover_spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlPopover, GlButton, GlSprintf, GlIcon } from '@gitlab/ui'; -import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; -import NavigationPopover from '~/attention_requests/components/navigation_popover.vue'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser'; - -let wrapper; -let dismiss; - -function createComponent(provideData = {}, shouldShowCallout = true) { - wrapper = shallowMount(NavigationPopover, { - provide: { - message: ['Test'], - observerElSelector: '.js-test', - observerElToggledClass: 'show', - featureName: 'attention_requests', - popoverTarget: '.js-test-popover', - ...provideData, - }, - stubs: { - UserCalloutDismisser: makeMockUserCalloutDismisser({ - dismiss, - shouldShowCallout, - }), - GlSprintf, - }, - }); -} - -describe('Attention requests navigation popover', () => { - beforeEach(() => { - setHTMLFixture('<div><div class="js-test-popover"></div><div class="js-test"></div></div>'); - dismiss = jest.fn(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - resetHTMLFixture(); - }); - - it('hides popover if callout is disabled', () => { - createComponent({}, false); - - expect(wrapper.findComponent(GlPopover).exists()).toBe(false); - }); - - it('shows popover if callout is enabled', () => { - createComponent(); - - expect(wrapper.findComponent(GlPopover).exists()).toBe(true); - }); - - it.each` - isDesktop | device | expectedPlacement - ${true} | ${'desktop'} | ${'left'} - ${false} | ${'mobile'} | ${'bottom'} - `( - 'sets popover position to $expectedPlacement on $device', - ({ isDesktop, expectedPlacement }) => { - jest.spyOn(bp, 'isDesktop').mockReturnValue(isDesktop); - - createComponent(); - - expect(wrapper.findComponent(GlPopover).props('placement')).toBe(expectedPlacement); - }, - ); - - it('calls dismiss when clicking action button', () => { - createComponent(); - - wrapper - .findComponent(GlButton) - .vm.$emit('click', { preventDefault() {}, stopPropagation() {} }); - - expect(dismiss).toHaveBeenCalled(); - }); - - it('shows icon in text', () => { - createComponent({ showAttentionIcon: true, message: ['%{strongStart}Test%{strongEnd}'] }); - - const icon = wrapper.findComponent(GlIcon); - - expect(icon.exists()).toBe(true); - expect(icon.props('name')).toBe('attention'); - }); -}); diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index 87a7f07f7d4..d990d5ad22b 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -1,5 +1,5 @@ import { GlAlert, GlBadge, GlLoadingIcon, GlTabs } from '@gitlab/ui'; -import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; +import { mount, shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import Vue, { nextTick } from 'vue'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -30,8 +30,7 @@ import { mockLintResponseWithoutMerged, } from '../mock_data'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); Vue.config.ignoredElements = ['gl-emoji']; @@ -88,7 +87,6 @@ describe('Pipeline editor tabs component', () => { provide, mountFn, options: { - localVue, apolloProvider: mockApollo, }, }); diff --git a/spec/frontend/runner/components/registration/registration_token_spec.js b/spec/frontend/runner/components/registration/registration_token_spec.js index ed1a698d36f..19344a68f79 100644 --- a/spec/frontend/runner/components/registration/registration_token_spec.js +++ b/spec/frontend/runner/components/registration/registration_token_spec.js @@ -1,5 +1,5 @@ import { GlToast } from '@gitlab/ui'; -import { createLocalVue } from '@vue/test-utils'; +import Vue from 'vue'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper'; import RegistrationToken from '~/runner/components/registration/registration_token.vue'; import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue'; @@ -11,28 +11,17 @@ describe('RegistrationToken', () => { let wrapper; let showToast; - const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility); - - const vueWithGlToast = () => { - const localVue = createLocalVue(); - localVue.use(GlToast); - return localVue; - }; + Vue.use(GlToast); - const createComponent = ({ - props = {}, - withGlToast = true, - mountFn = shallowMountExtended, - } = {}) => { - const localVue = withGlToast ? vueWithGlToast() : undefined; + const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility); + const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RegistrationToken, { propsData: { value: mockToken, inputId: 'token-value', ...props, }, - localVue, }); showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null; @@ -69,13 +58,5 @@ describe('RegistrationToken', () => { expect(showToast).toHaveBeenCalledTimes(1); expect(showToast).toHaveBeenCalledWith('Registration token copied!'); }); - - it('does not fail when toast is not defined', () => { - createComponent({ withGlToast: false }); - findInputCopyToggleVisibility().vm.$emit('copy'); - - // This block also tests for unhandled errors - expect(showToast).toBeNull(); - }); }); }); diff --git a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js b/spec/frontend/sidebar/components/attention_requested_toggle_spec.js deleted file mode 100644 index 58fa878a189..00000000000 --- a/spec/frontend/sidebar/components/attention_requested_toggle_spec.js +++ /dev/null @@ -1,121 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue'; - -let wrapper; - -function factory(propsData = {}) { - wrapper = mount(AttentionRequestedToggle, { propsData }); -} - -const findToggle = () => wrapper.findComponent(GlButton); - -describe('Attention require toggle', () => { - afterEach(() => { - wrapper.destroy(); - }); - - it('renders button', () => { - factory({ - type: 'reviewer', - user: { attention_requested: false, can_update_merge_request: true }, - }); - - expect(findToggle().exists()).toBe(true); - }); - - it.each` - attentionRequested | icon - ${true} | ${'attention-solid'} - ${false} | ${'attention'} - `( - 'renders $icon icon when attention_requested is $attentionRequested', - ({ attentionRequested, icon }) => { - factory({ - type: 'reviewer', - user: { attention_requested: attentionRequested, can_update_merge_request: true }, - }); - - expect(findToggle().props('icon')).toBe(icon); - }, - ); - - it.each` - attentionRequested | selected - ${true} | ${true} - ${false} | ${false} - `( - 'renders button with as selected when $selected when attention_requested is $attentionRequested', - ({ attentionRequested, selected }) => { - factory({ - type: 'reviewer', - user: { attention_requested: attentionRequested, can_update_merge_request: true }, - }); - - expect(findToggle().props('selected')).toBe(selected); - }, - ); - - it('emits toggle-attention-requested on click', async () => { - factory({ - type: 'reviewer', - user: { attention_requested: true, can_update_merge_request: true }, - }); - - await findToggle().trigger('click'); - - expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual([ - { - user: { attention_requested: true, can_update_merge_request: true }, - callback: expect.anything(), - direction: 'remove', - }, - ]); - }); - - it('does not emit toggle-attention-requested on click if can_update_merge_request is false', async () => { - factory({ - type: 'reviewer', - user: { attention_requested: true, can_update_merge_request: false }, - }); - - await findToggle().trigger('click'); - - expect(wrapper.emitted('toggle-attention-requested')).toBe(undefined); - }); - - it('sets loading on click', async () => { - factory({ - type: 'reviewer', - user: { attention_requested: true, can_update_merge_request: true }, - }); - - await findToggle().trigger('click'); - - expect(findToggle().props('loading')).toBe(true); - }); - - it.each` - type | attentionRequested | tooltip | canUpdateMergeRequest - ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequest} | ${true} - ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true} - ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true} - ${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false} - ${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false} - ${'assignee'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false} - ${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false} - `( - 'sets tooltip as $tooltip when attention_requested is $attentionRequested, type is $type and, can_update_merge_request is $canUpdateMergeRequest', - ({ type, attentionRequested, tooltip, canUpdateMergeRequest }) => { - factory({ - type, - user: { - attention_requested: attentionRequested, - can_update_merge_request: canUpdateMergeRequest, - }, - }); - - expect(findToggle().attributes('aria-label')).toBe(tooltip); - }, - ); -}); diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js index 8999f120a0f..8ac85d4da81 100644 --- a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js +++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js @@ -1,6 +1,5 @@ import { shallowMount } from '@vue/test-utils'; import { TEST_HOST } from 'helpers/test_constants'; -import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue'; import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue'; import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue'; import userDataMock from '../../user_data_mock'; @@ -119,18 +118,4 @@ describe('UncollapsedReviewerList component', () => { expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true); }); }); - - it('hides re-request review button when attentionRequired feature flag is enabled', () => { - createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true }); - - expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0); - }); - - it('emits toggle-attention-requested', () => { - createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true }); - - wrapper.find(AttentionRequestedToggle).vm.$emit('toggle-attention-requested', 'data'); - - expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual(['data']); - }); }); diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js index 82fb10ab1d2..e32694abcce 100644 --- a/spec/frontend/sidebar/sidebar_mediator_spec.js +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -1,12 +1,9 @@ import MockAdapter from 'axios-mock-adapter'; -import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import * as urlUtility from '~/lib/utils/url_utility'; import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; -import toast from '~/vue_shared/plugins/global_toast'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import Mock from './mock_data'; jest.mock('~/flash'); @@ -122,93 +119,4 @@ describe('Sidebar mediator', () => { urlSpy.mockRestore(); }); }); - - describe('toggleAttentionRequested', () => { - let requestAttentionMock; - let removeAttentionRequestMock; - - beforeEach(() => { - requestAttentionMock = jest.spyOn(mediator.service, 'requestAttention').mockResolvedValue(); - removeAttentionRequestMock = jest - .spyOn(mediator.service, 'removeAttentionRequest') - .mockResolvedValue(); - }); - - it.each` - attentionIsCurrentlyRequested | serviceMethod - ${true} | ${'remove'} - ${false} | ${'add'} - `( - "calls the $serviceMethod service method when the user's attention request is set to $attentionIsCurrentlyRequested", - async ({ serviceMethod }) => { - const methods = { - add: requestAttentionMock, - remove: removeAttentionRequestMock, - }; - mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }]; - - await mediator.toggleAttentionRequested('reviewer', { - user: { id: 1, username: 'root' }, - callback: jest.fn(), - direction: serviceMethod, - }); - - expect(methods[serviceMethod]).toHaveBeenCalledWith(1); - expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); - }, - ); - - it.each` - type | method - ${'reviewer'} | ${'findReviewer'} - `('finds $type', ({ type, method }) => { - const methodSpy = jest.spyOn(mediator.store, method); - - mediator.toggleAttentionRequested(type, { user: { id: 1 }, callback: jest.fn() }); - - expect(methodSpy).toHaveBeenCalledWith({ id: 1 }); - }); - - it.each` - attentionRequested | toastMessage - ${true} | ${'Removed attention request from @root'} - ${false} | ${'Requested attention from @root'} - `( - 'it creates toast $toastMessage when attention_requested is $attentionRequested', - async ({ attentionRequested, toastMessage }) => { - mediator.store.reviewers = [ - { id: 1, attention_requested: attentionRequested, username: 'root' }, - ]; - - await mediator.toggleAttentionRequested('reviewer', { - user: { id: 1, username: 'root' }, - callback: jest.fn(), - }); - - expect(toast).toHaveBeenCalledWith(toastMessage); - }, - ); - - describe('errors', () => { - beforeEach(() => { - jest - .spyOn(mediator.service, 'removeAttentionRequest') - .mockRejectedValueOnce(new Error('Something went wrong')); - }); - - it('shows an error message', async () => { - await mediator.toggleAttentionRequested('reviewer', { - user: { id: 1, username: 'root' }, - callback: jest.fn(), - direction: 'remove', - }); - - expect(createFlash).toHaveBeenCalledWith( - expect.objectContaining({ - message: 'Updating the attention request for root failed.', - }), - ); - }); - }); - }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 46d90ddc83c..188582d2e05 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,5 +1,5 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; import { GlSprintf } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import produce from 'immer'; @@ -71,8 +71,8 @@ const createTestService = () => ({ merge: jest.fn(), poll: jest.fn().mockResolvedValue(), }); -const localVue = createLocalVue(); -localVue.use(VueApollo); + +Vue.use(VueApollo); let wrapper; let readyToMergeResponseSpy; @@ -93,7 +93,6 @@ const createComponent = ( restructuredMrWidget = false, ) => { wrapper = shallowMount(ReadyToMerge, { - localVue, propsData: { mr: createTestMr(customConfig), service: createTestService(), diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js index 092e9c90553..1426fbfab80 100644 --- a/spec/frontend/work_items_hierarchy/components/app_spec.js +++ b/spec/frontend/work_items_hierarchy/components/app_spec.js @@ -1,19 +1,17 @@ -import { nextTick } from 'vue'; -import { createLocalVue, mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import { GlBanner } from '@gitlab/ui'; import App from '~/work_items_hierarchy/components/app.vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); describe('WorkItemsHierarchy App', () => { let wrapper; const createComponent = (props = {}, data = {}) => { wrapper = extendedWrapper( mount(App, { - localVue, provide: { illustrationPath: '/foo.svg', licensePlan: 'free', diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js index 74774e38d6b..67420e7fc2a 100644 --- a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js +++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js @@ -1,4 +1,5 @@ -import { createLocalVue, mount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { GlBadge } from '@gitlab/ui'; import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue'; @@ -6,8 +7,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import RESPONSE from '~/work_items_hierarchy/static_response'; import { workItemTypes } from '~/work_items_hierarchy/constants'; -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); describe('WorkItemsHierarchy Hierarchy', () => { let wrapper; @@ -32,7 +32,6 @@ describe('WorkItemsHierarchy Hierarchy', () => { const createComponent = (props = {}) => { wrapper = extendedWrapper( mount(Hierarchy, { - localVue, propsData: { workItemTypes: props.workItemTypes, ...props, diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index ed93d31da0f..6b769230d92 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -195,8 +195,8 @@ RSpec.describe GitlabSchema.types['Project'] do expect(secure_analyzers['type']).to eq('string') expect(secure_analyzers['field']).to eq('SECURE_ANALYZERS_PREFIX') expect(secure_analyzers['label']).to eq('Image prefix') - expect(secure_analyzers['defaultValue']).to eq(secure_analyzers_prefix) - expect(secure_analyzers['value']).to eq(secure_analyzers_prefix) + expect(secure_analyzers['defaultValue']).to eq('$TEMPLATE_REGISTRY_HOST/security-products') + expect(secure_analyzers['value']).to eq('$TEMPLATE_REGISTRY_HOST/security-products') expect(secure_analyzers['size']).to eq('LARGE') expect(secure_analyzers['options']).to be_nil end diff --git a/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb deleted file mode 100644 index 7c78d8b0305..00000000000 --- a/spec/lib/gitlab/background_migration/backfill_ci_runner_semver_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::BackgroundMigration::BackfillCiRunnerSemver, :migration, schema: 20220601151900 do - let(:ci_runners) { table(:ci_runners, database: :ci) } - - subject do - described_class.new( - start_id: 10, - end_id: 15, - batch_table: :ci_runners, - batch_column: :id, - sub_batch_size: 10, - pause_ms: 0, - connection: Ci::ApplicationRecord.connection) - end - - describe '#perform' do - it 'populates semver column on all runners in range' do - ci_runners.create!(id: 10, runner_type: 1, version: %q(HEAD-fd84d97)) - ci_runners.create!(id: 11, runner_type: 1, version: %q(v1.2.3)) - ci_runners.create!(id: 12, runner_type: 1, version: %q(2.1.0)) - ci_runners.create!(id: 13, runner_type: 1, version: %q(11.8.0~beta.935.g7f6d2abc)) - ci_runners.create!(id: 14, runner_type: 1, version: %q(13.2.2/1.1.0)) - ci_runners.create!(id: 15, runner_type: 1, version: %q('14.3.4')) - - subject.perform - - expect(ci_runners.all).to contain_exactly( - an_object_having_attributes(id: 10, semver: nil), - an_object_having_attributes(id: 11, semver: '1.2.3'), - an_object_having_attributes(id: 12, semver: '2.1.0'), - an_object_having_attributes(id: 13, semver: '11.8.0'), - an_object_having_attributes(id: 14, semver: '13.2.2'), - an_object_having_attributes(id: 15, semver: '14.3.4') - ) - end - - it 'skips runners that already have semver value' do - ci_runners.create!(id: 10, runner_type: 1, version: %q(1.2.4), semver: '1.2.3') - ci_runners.create!(id: 11, runner_type: 1, version: %q(1.2.5)) - ci_runners.create!(id: 12, runner_type: 1, version: %q(HEAD), semver: '1.2.4') - - subject.perform - - expect(ci_runners.all).to contain_exactly( - an_object_having_attributes(id: 10, semver: '1.2.3'), - an_object_having_attributes(id: 11, semver: '1.2.5'), - an_object_having_attributes(id: 12, semver: '1.2.4') - ) - end - end -end diff --git a/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb b/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb new file mode 100644 index 00000000000..f113ffcd0a7 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::BaseImporter do + let(:project) { instance_double('Project') } + let(:user_finder) { instance_double('Gitlab::GithubImport::UserFinder') } + let(:issue_event) { instance_double('Gitlab::GithubImport::Representation::IssueEvent') } + let(:importer_class) { Class.new(described_class) } + let(:importer_instance) { importer_class.new(project, user_finder) } + + describe '#execute' do + it { expect { importer_instance.execute(issue_event) }.to raise_error(NotImplementedError) } + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb new file mode 100644 index 00000000000..a1918dd0da8 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do + subject(:importer) { described_class.new(project, user_finder) } + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:assignee) { create(:user) } + let_it_be(:assigner) { create(:user) } + + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } + let(:issue) { create(:issue, project: project) } + + let(:issue_event) do + Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( + 'id' => 6501124486, + 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'event' => event_type, + 'commit_id' => nil, + 'created_at' => '2022-04-26 18:30:53 UTC', + 'assigner' => { 'id' => assigner.id, 'login' => assigner.username }, + 'assignee' => { 'id' => assignee.id, 'login' => assignee.username }, + 'issue_db_id' => issue.id + ) + end + + let(:note_attrs) do + { + noteable_id: issue.id, + noteable_type: Issue.name, + project_id: project.id, + author_id: assigner.id, + system: true, + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + let(:expected_system_note_metadata_attrs) do + { + action: "assignee", + created_at: issue_event.created_at, + updated_at: issue_event.created_at + }.stringify_keys + end + + shared_examples 'new note' do + it 'creates expected note' do + expect { importer.execute(issue_event) }.to change { issue.notes.count } + .from(0).to(1) + + expect(issue.notes.last) + .to have_attributes(expected_note_attrs) + end + + it 'creates expected system note metadata' do + expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count } + .from(0).to(1) + + expect(SystemNoteMetadata.last) + .to have_attributes( + expected_system_note_metadata_attrs.merge( + note_id: Note.last.id + ) + ) + end + end + + describe '#execute' do + before do + allow(user_finder).to receive(:find).with(assignee.id, assignee.username).and_return(assignee.id) + allow(user_finder).to receive(:find).with(assigner.id, assigner.username).and_return(assigner.id) + end + + context 'when importing an assigned event' do + let(:event_type) { 'assigned' } + let(:expected_note_attrs) { note_attrs.merge(note: "assigned to @#{assignee.username}") } + + it_behaves_like 'new note' + end + + context 'when importing an unassigned event' do + let(:event_type) { 'unassigned' } + let(:expected_note_attrs) { note_attrs.merge(note: "unassigned @#{assigner.username}") } + + it_behaves_like 'new note' + end + end +end diff --git a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb index b773598853d..98a8daf1653 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_label_spec.rb @@ -3,18 +3,20 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, user_finder) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let!(:label) { create(:label, project: project) } let(:issue_event) do Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( 'id' => 6501124486, - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => event_type, 'commit_id' => nil, 'label_title' => label.title, @@ -43,6 +45,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do before do allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id) + allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) end context 'when importing a labeled event' do diff --git a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb index 5db708b9049..a5852c967df 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_milestone_spec.rb @@ -3,18 +3,20 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, user_finder) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let!(:milestone) { create(:milestone, project: project) } let(:issue_event) do Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( 'id' => 6501124486, - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => event_type, 'commit_id' => nil, 'milestone_title' => milestone.title, @@ -45,6 +47,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do describe '#execute' do before do allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(milestone.id) + allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) end context 'when importing a milestoned event' do diff --git a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb index 116917d3e06..749c52a215e 100644 --- a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb @@ -3,11 +3,13 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, user_finder) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let(:commit_id) { nil } @@ -16,7 +18,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do 'id' => 6501124486, 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'closed', 'created_at' => '2022-04-26 18:30:53 UTC', 'commit_id' => commit_id, @@ -45,6 +47,10 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do }.stringify_keys end + before do + allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end + it 'creates expected event and state event' do importer.execute(issue_event) diff --git a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb index 118c482a7d9..bf785d27f05 100644 --- a/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/cross_referenced_spec.rb @@ -3,12 +3,14 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_gitlab_redis_cache do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, user_finder) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:sawyer_stub) { Struct.new(:iid, :issuable_type, keyword_init: true) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let(:referenced_in) { build_stubbed(:issue, project: project) } @@ -19,7 +21,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g 'id' => 6501124486, 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'cross-referenced', 'source' => { 'type' => 'issue', @@ -53,6 +55,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g other_issue_resource = sawyer_stub.new(iid: referenced_in.iid, issuable_type: 'Issue') Gitlab::GithubImport::IssuableFinder.new(project, other_issue_resource) .cache_database_id(referenced_in.iid) + allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) end it 'creates expected note' do @@ -75,6 +78,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g sawyer_stub.new(iid: referenced_in.iid, issuable_type: 'MergeRequest') Gitlab::GithubImport::IssuableFinder.new(project, other_issue_resource) .cache_database_id(referenced_in.iid) + allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) end it 'creates expected note' do @@ -87,7 +91,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g end context 'when referenced in out of project issue/pull_request' do - it 'creates expected note' do + it 'does not create expected note' do importer.execute(issue_event) expect(issue.notes.count).to eq 0 diff --git a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb index a8c3fbcb05d..8acf82af40c 100644 --- a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb @@ -3,17 +3,19 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, user_finder) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:issue) { create(:issue, project: project) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue_event) do Gitlab::GithubImport::Representation::IssueEvent.from_json_hash( 'id' => 6501124486, - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'renamed', 'commit_id' => nil, 'created_at' => '2022-04-26 18:30:53 UTC', @@ -45,6 +47,10 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do end describe '#execute' do + before do + allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end + it 'creates expected note' do expect { importer.execute(issue_event) }.to change { issue.notes.count } .from(0).to(1) diff --git a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb index 81653b0ecdc..39b8809dfa4 100644 --- a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb @@ -3,11 +3,13 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_failures do - subject(:importer) { described_class.new(project, user.id) } + subject(:importer) { described_class.new(project, user_finder) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } + let(:client) { instance_double('Gitlab::GithubImport::Client') } + let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) } let(:issue) { create(:issue, project: project) } let(:issue_event) do @@ -15,7 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail 'id' => 6501124486, 'node_id' => 'CE_lADOHK9fA85If7x0zwAAAAGDf0mG', 'url' => 'https://api.github.com/repos/elhowm/test-import/issues/events/6501124486', - 'actor' => { 'id' => 4, 'login' => 'alice' }, + 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'reopened', 'created_at' => '2022-04-26 18:30:53 UTC', 'issue_db_id' => issue.id @@ -42,6 +44,10 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail }.stringify_keys end + before do + allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end + it 'creates expected event and state event' do importer.execute(issue_event) diff --git a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb index 41d777fb466..fee7c2708a4 100644 --- a/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/issue_event_importer_spec.rb @@ -33,7 +33,7 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab specific_importer = double(importer_class.name) # rubocop:disable RSpec/VerifiedDoubles expect(importer_class) - .to receive(:new).with(project, user.id) + .to receive(:new).with(project, anything) .and_return(specific_importer) expect(specific_importer).to receive(:execute).with(issue_event) @@ -43,12 +43,6 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab describe '#execute' do before do - allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| - allow(finder).to receive(:author_id_for) - .with(issue_event, author_key: :actor) - .and_return(user.id, true) - end - issue_event.attributes[:issue_db_id] = issue.id end @@ -108,6 +102,20 @@ RSpec.describe Gitlab::GithubImport::Importer::IssueEventImporter, :clean_gitlab Gitlab::GithubImport::Importer::Events::CrossReferenced end + context "when it's assigned issue event" do + let(:event_name) { 'assigned' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedAssignee + end + + context "when it's unassigned issue event" do + let(:event_name) { 'unassigned' } + + it_behaves_like 'triggers specific event importer', + Gitlab::GithubImport::Importer::Events::ChangedAssignee + end + context "when it's unknown issue event" do let(:event_name) { 'fake' } diff --git a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb index 7382b0e2fff..cf796b55b14 100644 --- a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb +++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb @@ -91,6 +91,29 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do end end + context 'when assignee and assigner data is present' do + it 'includes assignee and assigner details' do + expect(issue_event.assignee) + .to be_an_instance_of(Gitlab::GithubImport::Representation::User) + expect(issue_event.assignee.id).to eq(5) + expect(issue_event.assignee.login).to eq('tom') + + expect(issue_event.assigner) + .to be_an_instance_of(Gitlab::GithubImport::Representation::User) + expect(issue_event.assigner.id).to eq(6) + expect(issue_event.assigner.login).to eq('jerry') + end + end + + context 'when assignee and assigner data is empty' do + let(:with_assignee) { false } + + it 'does not return such info' do + expect(issue_event.assignee).to eq nil + expect(issue_event.assigner).to eq nil + end + end + it 'includes the created timestamp' do expect(issue_event.created_at).to eq('2022-04-26 18:30:53 UTC') end @@ -106,8 +129,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do describe '.from_api_response' do let(:response) do event_resource = Struct.new( - :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, - :rename, :milestone, :source, :issue_db_id, :created_at, :performed_via_github_app, + :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, + :source, :assignee, :assigner, :issue_db_id, :created_at, :performed_via_github_app, keyword_init: true ) user_resource = Struct.new(:id, :login, keyword_init: true) @@ -124,6 +147,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do rename: with_rename ? { from: 'old title', to: 'new title' } : nil, milestone: with_milestone ? { title: 'milestone title' } : nil, source: { type: 'issue', id: 123456 }, + assignee: with_assignee ? user_resource.new(id: 5, login: 'tom') : nil, + assigner: with_assignee ? user_resource.new(id: 6, login: 'jerry') : nil, issue_db_id: 100500, created_at: '2022-04-26 18:30:53 UTC', performed_via_github_app: nil @@ -134,6 +159,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_label) { true } let(:with_rename) { true } let(:with_milestone) { true } + let(:with_assignee) { true } it_behaves_like 'an IssueEvent' do let(:issue_event) { described_class.from_api_response(response) } @@ -157,6 +183,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do 'new_title' => with_rename ? 'new title' : nil, 'milestone_title' => (with_milestone ? 'milestone title' : nil), 'source' => { 'type' => 'issue', 'id' => 123456 }, + 'assignee' => (with_assignee ? { 'id' => 5, 'login' => 'tom' } : nil), + 'assigner' => (with_assignee ? { 'id' => 6, 'login' => 'jerry' } : nil), "issue_db_id" => 100500, 'created_at' => '2022-04-26 18:30:53 UTC', 'performed_via_github_app' => nil @@ -167,6 +195,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_label) { true } let(:with_rename) { true } let(:with_milestone) { true } + let(:with_assignee) { true } let(:issue_event) { described_class.from_json_hash(hash) } end diff --git a/spec/lib/gitlab/github_import/user_finder_spec.rb b/spec/lib/gitlab/github_import/user_finder_spec.rb index 8eb6eedd72d..d85e298785c 100644 --- a/spec/lib/gitlab/github_import/user_finder_spec.rb +++ b/spec/lib/gitlab/github_import/user_finder_spec.rb @@ -15,32 +15,64 @@ RSpec.describe Gitlab::GithubImport::UserFinder, :clean_gitlab_redis_cache do let(:finder) { described_class.new(project, client) } describe '#author_id_for' do - it 'returns the user ID for the author of an object' do - user = double(:user, id: 4, login: 'kittens') - note = double(:note, author: user) + context 'with default author_key' do + it 'returns the user ID for the author of an object' do + user = double(:user, id: 4, login: 'kittens') + note = double(:note, author: user) - expect(finder).to receive(:user_id_for).with(user).and_return(42) + expect(finder).to receive(:user_id_for).with(user).and_return(42) - expect(finder.author_id_for(note)).to eq([42, true]) - end + expect(finder.author_id_for(note)).to eq([42, true]) + end - it 'returns the ID of the project creator if no user ID could be found' do - user = double(:user, id: 4, login: 'kittens') - note = double(:note, author: user) + it 'returns the ID of the project creator if no user ID could be found' do + user = double(:user, id: 4, login: 'kittens') + note = double(:note, author: user) - expect(finder).to receive(:user_id_for).with(user).and_return(nil) + expect(finder).to receive(:user_id_for).with(user).and_return(nil) - expect(finder.author_id_for(note)).to eq([project.creator_id, false]) - end + expect(finder.author_id_for(note)).to eq([project.creator_id, false]) + end + + it 'returns the ID of the ghost user when the object has no user' do + note = double(:note, author: nil) - it 'returns the ID of the ghost user when the object has no user' do - note = double(:note, author: nil) + expect(finder.author_id_for(note)).to eq([User.ghost.id, true]) + end - expect(finder.author_id_for(note)).to eq([User.ghost.id, true]) + it 'returns the ID of the ghost user when the given object is nil' do + expect(finder.author_id_for(nil)).to eq([User.ghost.id, true]) + end end - it 'returns the ID of the ghost user when the given object is nil' do - expect(finder.author_id_for(nil)).to eq([User.ghost.id, true]) + context 'with a non-default author_key' do + let(:user) { double(:user, id: 4, login: 'kittens') } + + shared_examples 'user ID finder' do |author_key| + it 'returns the user ID for an object' do + expect(finder).to receive(:user_id_for).with(user).and_return(42) + + expect(finder.author_id_for(issue_event, author_key: author_key)).to eq([42, true]) + end + end + + context 'when the author_key parameter is :actor' do + let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', actor: user) } + + it_behaves_like 'user ID finder', :actor + end + + context 'when the author_key parameter is :assignee' do + let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', assignee: user) } + + it_behaves_like 'user ID finder', :assignee + end + + context 'when the author_key parameter is :assigner' do + let(:issue_event) { double('Gitlab::GithubImport::Representation::IssueEvent', assigner: user) } + + it_behaves_like 'user ID finder', :assigner + end end end diff --git a/spec/lib/gitlab/import_export/project/relation_saver_spec.rb b/spec/lib/gitlab/import_export/project/relation_saver_spec.rb new file mode 100644 index 00000000000..dec51b3afd1 --- /dev/null +++ b/spec/lib/gitlab/import_export/project/relation_saver_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::Project::RelationSaver do + include ImportExport::CommonUtil + + subject(:relation_saver) { described_class.new(project: project, shared: shared, relation: relation) } + + let_it_be(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let_it_be(:project) { setup_project } + + let(:relation) { Projects::ImportExport::RelationExport::ROOT_RELATION } + let(:shared) do + shared = project.import_export_shared + allow(shared).to receive(:export_path).and_return(export_path) + shared + end + + after do + FileUtils.rm_rf(export_path) + end + + describe '#save' do + context 'when relation is the root node' do + let(:relation) { Projects::ImportExport::RelationExport::ROOT_RELATION } + + it 'serializes the root node as a json file in the export path' do + relation_saver.save # rubocop:disable Rails/SaveBang + + json = read_json(File.join(shared.export_path, 'project.json')) + expect(json).to include({ 'description' => 'Project description' }) + end + + it 'serializes only allowed attributes' do + relation_saver.save # rubocop:disable Rails/SaveBang + + json = read_json(File.join(shared.export_path, 'project.json')) + expect(json).to include({ 'description' => 'Project description' }) + expect(json.keys).not_to include('name') + end + + it 'successfuly serializes without errors' do + result = relation_saver.save # rubocop:disable Rails/SaveBang + + expect(result).to eq(true) + expect(shared.errors).to be_empty + end + end + + context 'when relation is a child node' do + let(:relation) { 'labels' } + + it 'serializes the child node as a ndjson file in the export path inside the project folder' do + relation_saver.save # rubocop:disable Rails/SaveBang + + ndjson = read_ndjson(File.join(shared.export_path, 'project', "#{relation}.ndjson")) + expect(ndjson.first).to include({ 'title' => 'Label 1' }) + expect(ndjson.second).to include({ 'title' => 'Label 2' }) + end + + it 'serializes only allowed attributes' do + relation_saver.save # rubocop:disable Rails/SaveBang + + ndjson = read_ndjson(File.join(shared.export_path, 'project', "#{relation}.ndjson")) + expect(ndjson.first.keys).not_to include('description_html') + end + + it 'successfuly serializes without errors' do + result = relation_saver.save # rubocop:disable Rails/SaveBang + + expect(result).to eq(true) + expect(shared.errors).to be_empty + end + end + + context 'when relation name is not supported' do + let(:relation) { 'unknown' } + + it 'returns false and register the error' do + result = relation_saver.save # rubocop:disable Rails/SaveBang + + expect(result).to eq(false) + expect(shared.errors).to be_present + end + end + + context 'when an exception occurs during serialization' do + it 'returns false and register the exception error message' do + allow_next_instance_of(Gitlab::ImportExport::Json::StreamingSerializer) do |serializer| + allow(serializer).to receive(:serialize_root).and_raise('Error!') + end + + result = relation_saver.save # rubocop:disable Rails/SaveBang + + expect(result).to eq(false) + expect(shared.errors).to include('Error!') + end + end + end + + def setup_project + project = create(:project, + description: 'Project description' + ) + + create(:label, project: project, title: 'Label 1') + create(:label, project: project, title: 'Label 2') + + project + end + + def read_json(path) + Gitlab::Json.parse(IO.read(path)) + end + + def read_ndjson(path) + relations = [] + File.foreach(path) do |line| + json = Gitlab::Json.parse(line) + relations << json + end + relations + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index fa03c6920c6..26883fc57b8 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -856,16 +856,6 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do it 'returns the successful deployment jobs for the last deployment pipeline' do expect(subject.pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id) end - - context 'when the feature flag is disabled' do - before do - stub_feature_flags(batch_load_environment_last_deployment_group: false) - end - - it 'returns the successful deployment jobs for the last deployment pipeline' do - expect(subject.pluck(:id)).to contain_exactly(deployment_a.id, deployment_b.id) - end - end end end diff --git a/spec/models/projects/import_export/relation_export_spec.rb b/spec/models/projects/import_export/relation_export_spec.rb index c74ca82e161..8643fbc7b46 100644 --- a/spec/models/projects/import_export/relation_export_spec.rb +++ b/spec/models/projects/import_export/relation_export_spec.rb @@ -20,4 +20,36 @@ RSpec.describe Projects::ImportExport::RelationExport, type: :model do it { is_expected.to validate_length_of(:jid).is_at_most(255) } it { is_expected.to validate_length_of(:export_error).is_at_most(300) } end + + describe '.by_relation' do + it 'returns export relations filtered by relation name' do + project_relation_export_1 = create(:project_relation_export, relation: 'labels') + project_relation_export_2 = create(:project_relation_export, relation: 'labels') + create(:project_relation_export, relation: 'uploads') + + relations = described_class.by_relation('labels').to_a + + expect(relations).to match_array([project_relation_export_1, project_relation_export_2]) + end + end + + describe '.relation_names_list' do + it 'includes extra relations list' do + expect(described_class.relation_names_list).to include( + 'design_repository', 'lfs_objects', 'repository', 'snippets_repository', 'uploads', 'wiki_repository' + ) + end + + it 'includes root tree relation name project' do + expect(described_class.relation_names_list).to include('project') + end + + it 'includes project tree top level relation nodes' do + expect(described_class.relation_names_list).to include('milestones', 'issues', 'snippets', 'releases') + end + + it 'includes project tree nested relation nodes' do + expect(described_class.relation_names_list).not_to include('events', 'notes') + end + end end diff --git a/spec/services/projects/import_export/relation_export_service_spec.rb b/spec/services/projects/import_export/relation_export_service_spec.rb new file mode 100644 index 00000000000..94f5653ee7d --- /dev/null +++ b/spec/services/projects/import_export/relation_export_service_spec.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ImportExport::RelationExportService do + using RSpec::Parameterized::TableSyntax + + subject(:service) { described_class.new(relation_export, 'jid') } + + let_it_be(:project_export_job) { create(:project_export_job) } + let_it_be(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let_it_be(:archive_path) { "#{Dir.tmpdir}/project_archive_spec" } + + let(:relation_export) { create(:project_relation_export, relation: relation, project_export_job: project_export_job) } + + before do + stub_uploads_object_storage(ImportExportUploader, enabled: false) + + allow(project_export_job.project.import_export_shared).to receive(:export_path).and_return(export_path) + allow(project_export_job.project.import_export_shared).to receive(:archive_path).and_return(archive_path) + allow(FileUtils).to receive(:remove_entry).with(any_args).and_call_original + end + + describe '#execute' do + let(:relation) { 'labels' } + + it 'removes temporary paths used to export files' do + expect(FileUtils).to receive(:remove_entry).with(export_path) + expect(FileUtils).to receive(:remove_entry).with(archive_path) + + service.execute + end + + context 'when saver fails to export relation' do + before do + allow_next_instance_of(Gitlab::ImportExport::Project::RelationSaver) do |saver| + allow(saver).to receive(:save).and_return(false) + end + end + + it 'flags export as failed' do + service.execute + + expect(relation_export.failed?).to eq(true) + end + + it 'logs failed message' do + expect_next_instance_of(Gitlab::Export::Logger) do |logger| + expect(logger).to receive(:error).with( + export_error: '', + message: 'Project relation export failed', + project_export_job_id: project_export_job.id, + project_id: project_export_job.project.id, + project_name: project_export_job.project.name + ) + end + + service.execute + end + end + + context 'when an exception is raised' do + before do + allow_next_instance_of(Gitlab::ImportExport::Project::RelationSaver) do |saver| + allow(saver).to receive(:save).and_raise('Error!') + end + end + + it 'flags export as failed' do + service.execute + + expect(relation_export.failed?).to eq(true) + expect(relation_export.export_error).to eq('Error!') + end + + it 'logs exception error message' do + expect_next_instance_of(Gitlab::Export::Logger) do |logger| + expect(logger).to receive(:error).with( + export_error: 'Error!', + message: 'Project relation export failed', + project_export_job_id: project_export_job.id, + project_id: project_export_job.project.id, + project_name: project_export_job.project.name + ) + end + + service.execute + end + end + + describe 'relation name and saver class' do + where(:relation_name, :saver) do + Projects::ImportExport::RelationExport::UPLOADS_RELATION | Gitlab::ImportExport::UploadsSaver + Projects::ImportExport::RelationExport::REPOSITORY_RELATION | Gitlab::ImportExport::RepoSaver + Projects::ImportExport::RelationExport::WIKI_REPOSITORY_RELATION | Gitlab::ImportExport::WikiRepoSaver + Projects::ImportExport::RelationExport::LFS_OBJECTS_RELATION | Gitlab::ImportExport::LfsSaver + Projects::ImportExport::RelationExport::SNIPPETS_REPOSITORY_RELATION | Gitlab::ImportExport::SnippetsRepoSaver + Projects::ImportExport::RelationExport::DESIGN_REPOSITORY_RELATION | Gitlab::ImportExport::DesignRepoSaver + Projects::ImportExport::RelationExport::ROOT_RELATION | Gitlab::ImportExport::Project::RelationSaver + 'labels' | Gitlab::ImportExport::Project::RelationSaver + end + + with_them do + let(:relation) { relation_name } + + it 'exports relation using correct saver' do + expect(saver).to receive(:new).and_call_original + + service.execute + end + + it 'assigns finished status and relation file' do + service.execute + + expect(relation_export.finished?).to eq(true) + expect(relation_export.upload.export_file.filename).to eq("#{relation}.tar.gz") + end + end + end + end +end diff --git a/spec/services/security/ci_configuration/sast_parser_service_spec.rb b/spec/services/security/ci_configuration/sast_parser_service_spec.rb index 4346d0a9e07..b11f31a9086 100644 --- a/spec/services/security/ci_configuration/sast_parser_service_spec.rb +++ b/spec/services/security/ci_configuration/sast_parser_service_spec.rb @@ -16,6 +16,7 @@ RSpec.describe Security::CiConfiguration::SastParserService do let(:bandit) { configuration['analyzers'][0] } let(:brakeman) { configuration['analyzers'][1] } let(:sast_brakeman_level) { brakeman['variables'][0] } + let(:secure_analyzers_prefix) { '$TEMPLATE_REGISTRY_HOST/security-products' } it 'parses the configuration for SAST' do expect(secure_analyzers['default_value']).to eql(secure_analyzers_prefix) diff --git a/spec/services/work_items/create_and_link_service_spec.rb b/spec/services/work_items/create_and_link_service_spec.rb index 831af775101..e259a22d388 100644 --- a/spec/services/work_items/create_and_link_service_spec.rb +++ b/spec/services/work_items/create_and_link_service_spec.rb @@ -62,6 +62,8 @@ RSpec.describe WorkItems::CreateAndLinkService do ) end + it_behaves_like 'title with extra spaces' + context 'when link params are valid' do let(:link_params) { { parent_work_item: related_work_item } } diff --git a/spec/services/work_items/create_from_task_service_spec.rb b/spec/services/work_items/create_from_task_service_spec.rb index 7d2dab228b1..7c5430f038c 100644 --- a/spec/services/work_items/create_from_task_service_spec.rb +++ b/spec/services/work_items/create_from_task_service_spec.rb @@ -64,6 +64,8 @@ RSpec.describe WorkItems::CreateFromTaskService do expect(list_work_item.description).to eq("- [ ] #{created_work_item.to_reference}+") end + + it_behaves_like 'title with extra spaces' end context 'when last operation fails' do diff --git a/spec/support/helpers/ci/template_helpers.rb b/spec/support/helpers/ci/template_helpers.rb index 598a5a0becc..119f8d001a1 100644 --- a/spec/support/helpers/ci/template_helpers.rb +++ b/spec/support/helpers/ci/template_helpers.rb @@ -5,6 +5,10 @@ module Ci def secure_analyzers_prefix 'registry.gitlab.com/security-products' end + + def template_registry_host + 'registry.gitlab.com' + end end end diff --git a/spec/support/shared_examples/services/work_items/create_task_shared_examples.rb b/spec/support/shared_examples/services/work_items/create_task_shared_examples.rb new file mode 100644 index 00000000000..7771e7f0e21 --- /dev/null +++ b/spec/support/shared_examples/services/work_items/create_task_shared_examples.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'title with extra spaces' do + context 'when title has extra spaces' do + before do + params[:title] = " Awesome work item " + end + + it 'removes extra leading and trailing whitespaces from title' do + subject + + created_work_item = WorkItem.last + expect(created_work_item.title).to eq('Awesome work item') + end + end +end diff --git a/spec/workers/projects/import_export/relation_export_worker_spec.rb b/spec/workers/projects/import_export/relation_export_worker_spec.rb new file mode 100644 index 00000000000..236650fe55b --- /dev/null +++ b/spec/workers/projects/import_export/relation_export_worker_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ImportExport::RelationExportWorker, type: :worker do + let(:project_relation_export) { create(:project_relation_export) } + let(:job_args) { [project_relation_export.id] } + + it_behaves_like 'an idempotent worker' + + describe '#perform' do + subject(:worker) { described_class.new } + + context 'when relation export has initial state queued' do + let(:project_relation_export) { create(:project_relation_export) } + + it 'calls RelationExportService' do + expect_next_instance_of(Projects::ImportExport::RelationExportService) do |service| + expect(service).to receive(:execute) + end + + worker.perform(project_relation_export.id) + end + end + + context 'when relation export does not have queued state' do + let(:project_relation_export) { create(:project_relation_export, status_event: :start) } + + it 'does not call RelationExportService' do + expect(Projects::ImportExport::RelationExportService).not_to receive(:new) + + worker.perform(project_relation_export.id) + end + end + end +end |