diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-08 21:11:24 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-08 21:11:24 +0300 |
commit | 70b375c29f67bdc8bd7e8ade1d5355444106482d (patch) | |
tree | 7e4fdec178464a016953a9aecfd349441edb9f44 /spec | |
parent | 3de2ce7c6b536d63ea2f93239022eb51fa9241c1 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
27 files changed, 1116 insertions, 137 deletions
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index e03a8b7d321..14b198dbefe 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -12,47 +12,6 @@ RSpec.describe SearchController do sign_in(user) end - shared_examples_for 'when the user cannot read cross project' do |action, params| - before do - allow(Ability).to receive(:allowed?).and_call_original - allow(Ability).to receive(:allowed?) - .with(user, :read_cross_project, :global) { false } - end - - it 'blocks access without a project_id' do - get action, params: params - - expect(response).to have_gitlab_http_status(:forbidden) - end - - it 'allows access with a project_id' do - get action, params: params.merge(project_id: create(:project, :public).id) - - expect(response).to have_gitlab_http_status(:ok) - end - end - - shared_examples_for 'with external authorization service enabled' do |action, params| - let(:project) { create(:project, namespace: user.namespace) } - let(:note) { create(:note_on_issue, project: project) } - - before do - enable_external_authorization_service_check - end - - it 'renders a 403 when no project is given' do - get action, params: params - - expect(response).to have_gitlab_http_status(:forbidden) - end - - it 'renders a 200 when a project was set' do - get action, params: params.merge(project_id: project.id) - - expect(response).to have_gitlab_http_status(:ok) - end - end - shared_examples_for 'support for active record query timeouts' do |action, params, method_to_stub, format| before do allow_next_instance_of(SearchService) do |service| @@ -133,10 +92,11 @@ RSpec.describe SearchController do { chars_under_limit: (('a' * (term_char_limit - 1) + ' ') * (term_limit - 1))[0, char_limit], chars_over_limit: (('a' * (term_char_limit - 1) + ' ') * (term_limit - 1))[0, char_limit + 1], - terms_under_limit: ('abc ' * (term_limit - 1)), + terms_under_limit: ('abc ' * (term_limit - 1)), terms_over_limit: ('abc ' * (term_limit + 1)), term_length_over_limit: ('a' * (term_char_limit + 1)), - term_length_under_limit: ('a' * (term_char_limit - 1)) + term_length_under_limit: ('a' * (term_char_limit - 1)), + blank: '' } end @@ -147,6 +107,7 @@ RSpec.describe SearchController do :terms_over_limit | :set_terms_flash :term_length_under_limit | :not_to_set_flash :term_length_over_limit | :not_to_set_flash # abuse, so do nothing. + :blank | :not_to_set_flash end with_them do @@ -417,7 +378,7 @@ RSpec.describe SearchController do expect(payload[:metadata]['meta.search.project_ids']).to eq(%w(456 789)) expect(payload[:metadata]['meta.search.type']).to eq('basic') expect(payload[:metadata]['meta.search.level']).to eq('global') - expect(payload[:metadata]['meta.search.language']).to eq('ruby') + expect(payload[:metadata]['meta.search.filters.language']).to eq('ruby') end get :show, params: { diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 9d2d1454d77..f45025d079a 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -3,16 +3,44 @@ require 'spec_helper' RSpec.describe 'New project', :js do - include Select2Helper include Spec::Support::Helpers::Features::TopNavSpecHelpers context 'as a user' do - let(:user) { create(:user) } + let_it_be(:user) { create(:user) } before do sign_in(user) end + it 'shows the project description field when it should' do + description_label = 'Project description (optional)' + + visit new_project_path + click_link 'Create blank project' + + page.within('#blank-project-pane') do + expect(page).not_to have_content(description_label) + end + + visit new_project_path + click_link 'Import project' + + page.within('#import-project-pane') do + click_button 'Repository by URL' + + expect(page).to have_content(description_label) + end + + visit new_project_path + click_link 'Create from template' + + page.within('#create-from-template-pane') do + find("[data-testid='use_template_#{Gitlab::ProjectTemplate.localized_templates_table.first.name}']").click + + expect(page).to have_content(description_label) + end + end + it 'shows a message if multiple levels are restricted' do Gitlab::CurrentSettings.update!( restricted_visibility_levels: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js new file mode 100644 index 00000000000..e9966576cab --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js @@ -0,0 +1,139 @@ +import { GlDropdown, GlDropdownItem, GlIcon, GlSearchBoxByType } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import { allEnvironments } from '~/ci_variable_list/constants'; +import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; + +describe('Ci environments dropdown', () => { + let wrapper; + + const envs = ['dev', 'prod', 'staging']; + const defaultProps = { environments: envs, selectedEnvironmentScope: '' }; + + const findDropdownText = () => wrapper.findComponent(GlDropdown).text(); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); + const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + + const createComponent = ({ props = {}, searchTerm = '' } = {}) => { + wrapper = mount(CiEnvironmentsDropdown, { + propsData: { + ...defaultProps, + ...props, + }, + }); + + findSearchBox().vm.$emit('input', searchTerm); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('No environments found', () => { + beforeEach(() => { + createComponent({ searchTerm: 'stable' }); + }); + + it('renders create button with search term if environments do not contain search term', () => { + expect(findAllDropdownItems()).toHaveLength(2); + expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable'); + }); + + it('renders empty results message', () => { + expect(findDropdownItemByIndex(0).text()).toBe('No matching results'); + }); + }); + + describe('Search term is empty', () => { + beforeEach(() => { + createComponent({ props: { environments: envs } }); + }); + + it('renders all environments when search term is empty', () => { + expect(findAllDropdownItems()).toHaveLength(3); + expect(findDropdownItemByIndex(0).text()).toBe(envs[0]); + expect(findDropdownItemByIndex(1).text()).toBe(envs[1]); + expect(findDropdownItemByIndex(2).text()).toBe(envs[2]); + }); + + it('should not display active checkmark on the inactive stage', () => { + expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true); + }); + }); + + describe('when `*` is the value of selectedEnvironmentScope props', () => { + const wildcardScope = '*'; + + beforeEach(() => { + createComponent({ props: { selectedEnvironmentScope: wildcardScope } }); + }); + + it('shows the `All environments` text and not the wildcard', () => { + expect(findDropdownText()).toContain(allEnvironments.text); + expect(findDropdownText()).not.toContain(wildcardScope); + }); + }); + + describe('Environments found', () => { + const currentEnv = envs[2]; + + beforeEach(async () => { + createComponent({ searchTerm: currentEnv }); + await nextTick(); + }); + + it('renders only the environment searched for', () => { + expect(findAllDropdownItems()).toHaveLength(1); + expect(findDropdownItemByIndex(0).text()).toBe(currentEnv); + }); + + it('should not display create button', () => { + const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create')); + expect(environments).toHaveLength(0); + expect(findAllDropdownItems()).toHaveLength(1); + }); + + it('should not display empty results message', () => { + expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false); + }); + + it('should clear the search term when showing the dropdown', () => { + wrapper.findComponent(GlDropdown).trigger('click'); + + expect(findSearchBox().text()).toBe(''); + }); + + describe('Custom events', () => { + describe('when clicking on an environment', () => { + const itemIndex = 0; + + beforeEach(() => { + createComponent(); + }); + + it('should emit `select-environment` if an environment is clicked', async () => { + await nextTick(); + + await findDropdownItemByIndex(itemIndex).vm.$emit('click'); + + expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]); + }); + }); + + describe('when creating a new environment from a search term', () => { + const search = 'new-env'; + beforeEach(() => { + createComponent({ searchTerm: search }); + }); + + it('should emit createClicked if an environment is clicked', async () => { + await nextTick(); + findDropdownItemByIndex(1).vm.$emit('click'); + expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]); + }); + }); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js new file mode 100644 index 00000000000..51b902d97dc --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -0,0 +1,380 @@ +import { GlButton, GlFormInput } from '@gitlab/ui'; +import { mockTracking } from 'helpers/tracking_helper'; +import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue'; +import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; +import { + ADD_VARIABLE_ACTION, + AWS_ACCESS_KEY_ID, + EDIT_VARIABLE_ACTION, + EVENT_LABEL, + EVENT_ACTION, + ENVIRONMENT_SCOPE_LINK_TITLE, +} from '~/ci_variable_list/constants'; +import { mockVariablesWithScopes } from '../mocks'; +import ModalStub from '../stubs'; + +describe('Ci variable modal', () => { + let wrapper; + let trackingSpy; + + const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$'; + + const defaultProvide = { + awsLogoSvgPath: '/logo', + awsTipCommandsLink: '/tips', + awsTipDeployLink: '/deploy', + awsTipLearnLink: '/learn-link', + containsVariableReferenceLink: '/reference', + environmentScopeLink: '/help/environments', + isProtectedByDefault: false, + maskedEnvironmentVariablesLink: '/variables-link', + maskableRegex, + protectedEnvironmentVariablesLink: '/protected-link', + }; + + const defaultProps = { + areScopedVariablesAvailable: true, + environments: [], + mode: ADD_VARIABLE_ACTION, + selectedVariable: {}, + }; + + const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => { + wrapper = mountFn(CiVariableModal, { + attachTo: document.body, + provide: { ...defaultProvide, ...provide }, + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlModal: ModalStub, + }, + }); + }; + + const findCiEnvironmentsDropdown = () => wrapper.find(CiEnvironmentsDropdown); + const findReferenceWarning = () => wrapper.findByTestId('contains-variable-reference'); + const findModal = () => wrapper.find(ModalStub); + const findAWSTip = () => wrapper.findByTestId('aws-guidance-tip'); + const findAddorUpdateButton = () => wrapper.findByTestId('ciUpdateOrAddVariableBtn'); + const deleteVariableButton = () => + findModal() + .findAll(GlButton) + .wrappers.find((button) => button.props('variant') === 'danger'); + const findProtectedVariableCheckbox = () => + wrapper.findByTestId('ci-variable-protected-checkbox'); + const findMaskedVariableCheckbox = () => wrapper.findByTestId('ci-variable-masked-checkbox'); + const findValueField = () => wrapper.find('#ci-variable-value'); + const findEnvScopeLink = () => wrapper.findByTestId('environment-scope-link'); + const findEnvScopeInput = () => wrapper.findByTestId('environment-scope').find(GlFormInput); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Adding a variable', () => { + describe('when no key/value pair are present', () => { + beforeEach(() => { + createComponent(); + }); + + it('shows the submit button as disabled ', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + }); + }); + + describe('when a key/value pair is present', () => { + beforeEach(() => { + createComponent({ props: { selectedVariable: mockVariablesWithScopes[0] } }); + }); + + it('shows the submit button as enabled ', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + }); + }); + + describe('events', () => { + const [currentVariable] = mockVariablesWithScopes; + + beforeEach(() => { + createComponent({ props: { selectedVariable: currentVariable } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('Dispatches `add-variable` action on submit', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]); + }); + + it('Dispatches the `hideModal` event when dismissing', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + }); + }); + + describe('when protected by default', () => { + describe('when adding a new variable', () => { + beforeEach(() => { + createComponent({ provide: { isProtectedByDefault: true } }); + findModal().vm.$emit('shown'); + }); + + it('updates the protected value to true', () => { + expect( + findProtectedVariableCheckbox().attributes('data-is-protected-checked'), + ).toBeTruthy(); + }); + }); + + describe('when editing a variable', () => { + beforeEach(() => { + createComponent({ + provide: { isProtectedByDefault: false }, + props: { + selectedVariable: {}, + mode: EDIT_VARIABLE_ACTION, + }, + }); + findModal().vm.$emit('shown'); + }); + + it('keeps the value as false', async () => { + expect( + findProtectedVariableCheckbox().attributes('data-is-protected-checked'), + ).toBeUndefined(); + }); + }); + }); + + describe('Adding a new non-AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } }); + }); + + it('does not show AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(false); + }); + }); + + describe('Adding a new AWS variable', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const AWSKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhy', + }; + createComponent({ mountFn: mountExtended, props: { selectedVariable: AWSKeyVariable } }); + }); + + it('shows AWS guidance tip', () => { + const tip = findAWSTip(); + expect(tip.exists()).toBe(true); + expect(tip.isVisible()).toBe(true); + }); + }); + + describe('Reference warning when adding a variable', () => { + describe('with a $ character', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const variableWithDollarSign = { + ...variable, + value: 'valueWith$', + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variableWithDollarSign }, + }); + }); + + it(`renders the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(true); + }); + }); + + describe('without a $ character', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: variable }, + }); + }); + + it(`does not render the variable reference warning`, () => { + expect(findReferenceWarning().exists()).toBe(false); + }); + }); + }); + + describe('Editing a variable', () => { + const [variable] = mockVariablesWithScopes; + + beforeEach(() => { + createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } }); + jest.spyOn(wrapper.vm, '$emit'); + }); + + it('button text is Update variable when updating', () => { + expect(findAddorUpdateButton().text()).toBe('Update variable'); + }); + + it('Update variable button dispatches updateVariable with correct variable', () => { + findAddorUpdateButton().vm.$emit('click'); + expect(wrapper.emitted('update-variable')).toEqual([[variable]]); + }); + + it('Propagates the `hideModal` event', () => { + findModal().vm.$emit('hidden'); + expect(wrapper.emitted('hideModal')).toEqual([[]]); + }); + + it('dispatches `delete-variable` with correct variable to delete', () => { + deleteVariableButton().vm.$emit('click'); + expect(wrapper.emitted('delete-variable')).toEqual([[variable]]); + }); + }); + + describe('Environment scope', () => { + describe('when feature is available', () => { + it('renders the environment dropdown', () => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: true, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(true); + expect(findCiEnvironmentsDropdown().isVisible()).toBe(true); + }); + + it('renders a link to documentation on scopes', () => { + createComponent({ mountFn: mountExtended }); + + const link = findEnvScopeLink(); + + expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE); + expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink); + }); + }); + + describe('when feature is not available', () => { + it('disables the dropdown', () => { + createComponent({ + mountFn: mountExtended, + props: { + areScopedVariablesAvailable: false, + }, + }); + + expect(findCiEnvironmentsDropdown().exists()).toBe(false); + expect(findEnvScopeInput().attributes('readonly')).toBe('readonly'); + }); + }); + }); + + describe('Validations', () => { + const maskError = 'This variable can not be masked.'; + + describe('when the mask state is invalid', () => { + beforeEach(async () => { + const [variable] = mockVariablesWithScopes; + const invalidMaskVariable = { + ...variable, + value: 'd:;', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidMaskVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findMaskedVariableCheckbox().trigger('click'); + }); + + it('disables the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + expect(findModal().text()).toContain(maskError); + }); + + it('sends the correct tracking event', () => { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: ';', + }); + }); + }); + + describe.each` + value | masked | eventSent | trackingErrorProperty + ${'secretValue'} | ${false} | ${0} | ${null} + ${'short'} | ${true} | ${0} | ${null} + ${'dollar$ign'} | ${false} | ${1} | ${'$'} + ${'dollar$ign'} | ${true} | ${1} | ${'$'} + ${'unsupported|char'} | ${true} | ${1} | ${'|'} + ${'unsupported|char'} | ${false} | ${0} | ${null} + `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => { + beforeEach(async () => { + const [variable] = mockVariablesWithScopes; + const invalidKeyVariable = { + ...variable, + value: '', + masked: false, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: invalidKeyVariable }, + }); + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + await findValueField().vm.$emit('input', value); + if (masked) { + await findMaskedVariableCheckbox().trigger('click'); + } + }); + + it(`${ + eventSent > 0 ? 'sends the correct' : 'does not send the' + } variable validation tracking event with ${value}`, () => { + expect(trackingSpy).toHaveBeenCalledTimes(eventSent); + + if (eventSent > 0) { + expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, { + label: EVENT_LABEL, + property: trackingErrorProperty, + }); + } + }); + }); + + describe('when masked variable has acceptable value', () => { + beforeEach(() => { + const [variable] = mockVariablesWithScopes; + const validMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: '12345678', + masked: true, + }; + createComponent({ + mountFn: mountExtended, + props: { selectedVariable: validMaskandKeyVariable }, + }); + }); + + it('does not disable the submit button', () => { + expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); + }); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js new file mode 100644 index 00000000000..1ba50c74152 --- /dev/null +++ b/spec/frontend/ci_variable_list/mocks.js @@ -0,0 +1,116 @@ +import { variableTypes } from '~/ci_variable_list/constants'; + +export const devName = 'dev'; +export const prodName = 'prod'; + +export const mockVariables = [ + { + __typename: 'CiVariable', + id: 1, + key: 'my-var', + masked: false, + protected: true, + value: 'env_val', + variableType: variableTypes.variableType, + }, + { + __typename: 'CiVariable', + id: 2, + key: 'secret', + masked: true, + protected: false, + value: 'the_secret_value', + variableType: variableTypes.fileType, + }, +]; + +export const mockVariablesWithScopes = mockVariables.map((variable) => { + return { ...variable, environmentScope: '*' }; +}); + +const createDefaultVars = ({ withScope = true } = {}) => { + let base = mockVariables; + + if (withScope) { + base = mockVariablesWithScopes; + } + + return { + __typename: 'CiVariableConnection', + nodes: base, + }; +}; + +const defaultEnvs = { + __typename: 'EnvironmentConnection', + nodes: [ + { + __typename: 'Environment', + id: 1, + name: prodName, + }, + { + __typename: 'Environment', + id: 2, + name: devName, + }, + ], +}; + +export const mockEnvs = defaultEnvs.nodes; + +export const mockProjectEnvironments = { + data: { + project: { + __typename: 'Project', + id: 1, + environments: defaultEnvs, + }, + }, +}; + +export const mockProjectVariables = { + data: { + project: { + __typename: 'Project', + id: 1, + ciVariables: createDefaultVars(), + }, + }, +}; + +export const mockGroupEnvironments = { + data: { + group: { + __typename: 'Group', + id: 1, + environments: defaultEnvs, + }, + }, +}; + +export const mockGroupVariables = { + data: { + group: { + __typename: 'Group', + id: 1, + ciVariables: createDefaultVars(), + }, + }, +}; + +export const mockAdminVariables = { + data: { + ciVariables: createDefaultVars({ withScope: false }), + }, +}; + +export const newVariable = { + id: 3, + environmentScope: 'new', + key: 'AWS_RANDOM_THING', + masked: true, + protected: false, + value: 'devops', + variableType: variableTypes.variableType, +}; diff --git a/spec/frontend/ci_variable_list/utils_spec.js b/spec/frontend/ci_variable_list/utils_spec.js new file mode 100644 index 00000000000..1676e786515 --- /dev/null +++ b/spec/frontend/ci_variable_list/utils_spec.js @@ -0,0 +1,68 @@ +import { + createJoinedEnvironments, + convertEnvironmentScope, + mapEnvironmentNames, +} from '~/ci_variable_list/utils'; +import { allEnvironments } from '~/ci_variable_list/constants'; + +describe('utils', () => { + const environments = ['dev', 'prod']; + + describe('createJoinedEnvironments', () => { + it('returns only `environments` if `variables` argument is undefined', () => { + const variables = undefined; + + expect(createJoinedEnvironments(variables, environments)).toEqual(environments); + }); + + it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => { + const envScope1 = 'new1'; + const envScope2 = 'new2'; + + const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }]; + + expect(createJoinedEnvironments(variables, environments)).toEqual([ + environments[0], + envScope1, + envScope2, + environments[1], + ]); + }); + + it('removes duplicate environments', () => { + const envScope1 = environments[0]; + const envScope2 = 'new2'; + + const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }]; + + expect(createJoinedEnvironments(variables, environments)).toEqual([ + environments[0], + envScope2, + environments[1], + ]); + }); + }); + + describe('convertEnvironmentScope', () => { + it('converts the * to the `All environments` text', () => { + expect(convertEnvironmentScope('*')).toBe(allEnvironments.text); + }); + + it('returns the environment as is if not the *', () => { + expect(convertEnvironmentScope('prod')).toBe('prod'); + }); + }); + + describe('mapEnvironmentNames', () => { + const envName = 'dev'; + const envName2 = 'prod'; + + const nodes = [ + { name: envName, otherProp: {} }, + { name: envName2, otherProp: {} }, + ]; + it('flatten a nodes array with only their names', () => { + expect(mapEnvironmentNames(nodes)).toEqual([envName, envName2]); + }); + }); +}); diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js index e1de2823726..ca552644258 100644 --- a/spec/frontend/content_editor/remark_markdown_processing_spec.js +++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js @@ -1170,7 +1170,7 @@ _world_. const trimmed = markdown.trim(); const document = await deserialize(trimmed); - expect(expectedDoc).not.toBeFalsy(); + expect(expectedDoc).not.toBe(false); expect(document.toJSON()).toEqual(expectedDoc.toJSON()); expect(serialize(document)).toEqual(expectedMarkdown ?? trimmed); }, diff --git a/spec/frontend/labels/labels_select_spec.js b/spec/frontend/labels/labels_select_spec.js index f6e280564cc..63f7c725bc7 100644 --- a/spec/frontend/labels/labels_select_spec.js +++ b/spec/frontend/labels/labels_select_spec.js @@ -101,6 +101,12 @@ describe('LabelsSelect', () => { expect($labelEl.find('a').attr('data-html')).toBe('true'); }); + it('generated label item template has correct title for tooltip', () => { + expect($labelEl.find('a').attr('title')).toBe( + "<span class='font-weight-bold scoped-label-tooltip-title'>Scoped label</span><br>Foobar", + ); + }); + it('generated label item template has correct label styles and classes', () => { expect($labelEl.find('span.gl-label-text').attr('style')).toBe( `background-color: ${label.color};`, diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 312e4f636c3..2c6b603197d 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -348,15 +348,13 @@ describe('URL utility', () => { describe('urlContainsSha', () => { it('returns true when there is a valid 40-character SHA1 hash in the URL', () => { shas.valid.forEach((sha) => { - expect( - urlUtils.urlContainsSha({ url: `http://urlstuff/${sha}/moreurlstuff` }), - ).toBeTruthy(); + expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${sha}/moreurlstuff` })).toBe(true); }); }); it('returns false when there is not a valid 40-character SHA1 hash in the URL', () => { shas.invalid.forEach((str) => { - expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${str}/moreurlstuff` })).toBeFalsy(); + expect(urlUtils.urlContainsSha({ url: `http://urlstuff/${str}/moreurlstuff` })).toBe(false); }); }); }); @@ -813,13 +811,13 @@ describe('URL utility', () => { }); it('should compare against the window location if no compare value is provided', () => { - expect(urlUtils.urlIsDifferent('different')).toBeTruthy(); - expect(urlUtils.urlIsDifferent(current)).toBeFalsy(); + expect(urlUtils.urlIsDifferent('different')).toBe(true); + expect(urlUtils.urlIsDifferent(current)).toBe(false); }); it('should use the provided compare value', () => { - expect(urlUtils.urlIsDifferent('different', current)).toBeTruthy(); - expect(urlUtils.urlIsDifferent(current, current)).toBeFalsy(); + expect(urlUtils.urlIsDifferent('different', current)).toBe(true); + expect(urlUtils.urlIsDifferent(current, current)).toBe(false); }); }); diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js index 3034037fb1d..4fcecc3a307 100644 --- a/spec/frontend/projects/project_new_spec.js +++ b/spec/frontend/projects/project_new_spec.js @@ -1,6 +1,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import { TEST_HOST } from 'helpers/test_constants'; import projectNew from '~/projects/project_new'; +import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper'; describe('New Project', () => { let $projectImportUrl; @@ -12,21 +13,27 @@ describe('New Project', () => { beforeEach(() => { setHTMLFixture(` - <div class='toggle-import-form'> - <div class='import-url-data'> - <div class="form-group"> - <input id="project_import_url" /> - </div> - <div id="import-url-auth-method"> - <div class="form-group"> - <input id="project-import-url-user" /> + <div class="tab-pane active"> + <div class='toggle-import-form'> + <form id="new_project"> + <div class='import-url-data'> + <div class="form-group"> + <input id="project_import_url" /> + </div> + <div id="import-url-auth-method"> + <div class="form-group"> + <input id="project-import-url-user" /> + </div> + <div class="form-group"> + <input id="project_import_url_password" /> + </div> + </div> + <input id="project_name" /> + <input id="project_path" /> </div> - <div class="form-group"> - <input id="project_import_url_password" /> - </div> - </div> - <input id="project_name" /> - <input id="project_path" /> + <div class="js-user-readme-repo"></div> + <button class="js-create-project-button"/> + </form> </div> </div> `); @@ -45,6 +52,38 @@ describe('New Project', () => { el.value = value; }; + describe('tracks manual path input', () => { + let trackingSpy; + + beforeEach(() => { + trackingSpy = mockTracking('_category_', undefined, jest.spyOn); + projectNew.bindEvents(); + $projectPath.oldInputValue = '_old_value_'; + }); + + afterEach(() => { + unmockTracking(); + }); + + it('tracks the event', () => { + $projectPath.value = '_new_value_'; + + triggerEvent($projectPath, 'blur'); + + expect(trackingSpy).toHaveBeenCalledWith(undefined, 'user_input_path_slug', { + label: 'new_project_form', + }); + }); + + it('does not track the event when there has been no change', () => { + $projectPath.value = '_old_value_'; + + triggerEvent($projectPath, 'blur'); + + expect(trackingSpy).not.toHaveBeenCalled(); + }); + }); + describe('deriveProjectPathFromUrl', () => { const dummyImportUrl = `${TEST_HOST}/dummy/import/url.git`; diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js index f0106914674..193a16bae8d 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js @@ -80,20 +80,20 @@ describe('MemoryUsage', () => { it('should have default data', () => { const data = MemoryUsage.data(); - expect(Array.isArray(data.memoryMetrics)).toBeTruthy(); + expect(Array.isArray(data.memoryMetrics)).toBe(true); expect(data.memoryMetrics.length).toBe(0); expect(typeof data.deploymentTime).toBe('number'); expect(data.deploymentTime).toBe(0); expect(typeof data.hasMetrics).toBe('boolean'); - expect(data.hasMetrics).toBeFalsy(); + expect(data.hasMetrics).toBe(false); expect(typeof data.loadFailed).toBe('boolean'); - expect(data.loadFailed).toBeFalsy(); + expect(data.loadFailed).toBe(false); expect(typeof data.loadingMetrics).toBe('boolean'); - expect(data.loadingMetrics).toBeTruthy(); + expect(data.loadingMetrics).toBe(true); expect(typeof data.backOffRequestCounter).toBe('number'); expect(data.backOffRequestCounter).toBe(0); @@ -144,7 +144,7 @@ describe('MemoryUsage', () => { vm.computeGraphData(metrics, deployment_time); const { hasMetrics, memoryMetrics, deploymentTime, memoryFrom, memoryTo } = vm; - expect(hasMetrics).toBeTruthy(); + expect(hasMetrics).toBe(true); expect(memoryMetrics.length).toBeGreaterThan(0); expect(deploymentTime).toEqual(deployment_time); expect(memoryFrom).toEqual('9.13'); @@ -171,7 +171,7 @@ describe('MemoryUsage', () => { describe('template', () => { it('should render template elements correctly', () => { - expect(el.classList.contains('mr-memory-usage')).toBeTruthy(); + expect(el.classList.contains('mr-memory-usage')).toBe(true); expect(el.querySelector('.js-usage-info')).toBeDefined(); }); 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 index f113ffcd0a7..41fe5fbdbbd 100644 --- a/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/base_importer_spec.rb @@ -4,10 +4,10 @@ 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(:client) { instance_double('Gitlab::GithubImport::Client') } 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) } + let(:importer_instance) { importer_class.new(project, client) } describe '#execute' do it { expect { importer_instance.execute(issue_event) }.to raise_error(NotImplementedError) } 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 index a1918dd0da8..2f6f727dc38 100644 --- a/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/changed_assignee_spec.rb @@ -3,14 +3,13 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } 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 @@ -22,7 +21,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do '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 + 'issue' => { 'number' => issue.iid } ) end @@ -70,8 +69,13 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do 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) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(assignee.id, assignee.username).and_return(assignee.id) + allow(finder).to receive(:find).with(assigner.id, assigner.username).and_return(assigner.id) + end end context 'when importing an assigned event' do 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 98a8daf1653..e21672aa430 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,13 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } 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) } @@ -21,7 +20,8 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do 'commit_id' => nil, 'label_title' => label.title, 'issue_db_id' => issue.id, - 'created_at' => '2022-04-26 18:30:53 UTC' + 'created_at' => '2022-04-26 18:30:53 UTC', + 'issue' => { 'number' => issue.iid } ) end @@ -45,7 +45,12 @@ 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) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end 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 a5852c967df..2687627fc23 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,13 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } 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) } @@ -21,7 +20,8 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do 'commit_id' => nil, 'milestone_title' => milestone.title, 'issue_db_id' => issue.id, - 'created_at' => '2022-04-26 18:30:53 UTC' + 'created_at' => '2022-04-26 18:30:53 UTC', + 'issue' => { 'number' => issue.iid } ) end @@ -47,7 +47,12 @@ 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) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end 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 749c52a215e..9a49d80a8bb 100644 --- a/spec/lib/gitlab/github_import/importer/events/closed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/closed_spec.rb @@ -3,13 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } 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 } @@ -22,7 +21,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do 'event' => 'closed', 'created_at' => '2022-04-26 18:30:53 UTC', 'commit_id' => commit_id, - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -48,7 +47,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Closed do end before do - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected event and state event' do 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 fd2e564d1b1..68e001c7364 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,14 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_gitlab_redis_cache do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } 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_iid) { 999 } let(:issue) { create(:issue, project: project, iid: issue_iid) } @@ -32,7 +30,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g } }, 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -41,7 +39,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g { system: true, noteable_type: Issue.name, - noteable_id: issue_event.issue_db_id, + noteable_id: issue.id, project_id: project.id, author_id: user.id, note: expected_note_body, @@ -53,10 +51,13 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g let(:expected_note_body) { "mentioned in issue ##{referenced_in.iid}" } before do - 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) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(referenced_in.iid) + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected note' do @@ -75,11 +76,13 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::CrossReferenced, :clean_g let(:expected_note_body) { "mentioned in merge request !#{referenced_in.iid}" } before do - other_issue_resource = - 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) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(referenced_in.iid) + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected note' do 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 8acf82af40c..316ea798965 100644 --- a/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/renamed_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -11,7 +11,6 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do 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, @@ -21,7 +20,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do 'created_at' => '2022-04-26 18:30:53 UTC', 'old_title' => 'old title', 'new_title' => 'new title', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -48,7 +47,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Renamed do describe '#execute' do before do - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected note' do 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 39b8809dfa4..2461dbb9701 100644 --- a/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb +++ b/spec/lib/gitlab/github_import/importer/events/reopened_spec.rb @@ -3,13 +3,12 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_failures do - subject(:importer) { described_class.new(project, user_finder) } + subject(:importer) { described_class.new(project, client) } 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 @@ -20,7 +19,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail 'actor' => { 'id' => user.id, 'login' => user.username }, 'event' => 'reopened', 'created_at' => '2022-04-26 18:30:53 UTC', - 'issue_db_id' => issue.id + 'issue' => { 'number' => issue.iid } ) end @@ -45,7 +44,12 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::Reopened, :aggregate_fail end before do - allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id) + allow_next_instance_of(Gitlab::GithubImport::IssuableFinder) do |finder| + allow(finder).to receive(:database_id).and_return(issue.id) + end + allow_next_instance_of(Gitlab::GithubImport::UserFinder) do |finder| + allow(finder).to receive(:find).with(user.id, user.username).and_return(user.id) + end end it 'creates expected event and state event' do 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 fee7c2708a4..33d5fbf13a0 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, anything) + .to receive(:new).with(project, client) .and_return(specific_importer) expect(specific_importer).to receive(:execute).with(issue_event) diff --git a/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb new file mode 100644 index 00000000000..8d4c1b01e50 --- /dev/null +++ b/spec/lib/gitlab/github_import/importer/issue_events_importer_spec.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GithubImport::Importer::IssueEventsImporter do + subject(:importer) { described_class.new(project, client, parallel: parallel) } + + let(:project) { instance_double(Project, id: 4, import_source: 'foo/bar') } + let(:client) { instance_double(Gitlab::GithubImport::Client) } + + let(:parallel) { true } + let(:issue_event) do + struct = Struct.new( + :id, :node_id, :url, :actor, :event, :commit_id, :commit_url, :label, :rename, :milestone, + :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app, + keyword_init: true + ) + struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC') + end + + describe '#parallel?' do + context 'when running in parallel mode' do + it { expect(importer).to be_parallel } + end + + context 'when running in sequential mode' do + let(:parallel) { false } + + it { expect(importer).not_to be_parallel } + end + end + + describe '#execute' do + context 'when running in parallel mode' do + it 'imports events in parallel' do + expect(importer).to receive(:parallel_import) + + importer.execute + end + end + + context 'when running in sequential mode' do + let(:parallel) { false } + + it 'imports notes in sequence' do + expect(importer).to receive(:sequential_import) + + importer.execute + end + end + end + + describe '#sequential_import' do + let(:parallel) { false } + + it 'imports each event in sequence' do + event_importer = instance_double(Gitlab::GithubImport::Importer::IssueEventImporter) + + allow(importer).to receive(:each_object_to_import).and_yield(issue_event) + + expect(Gitlab::GithubImport::Importer::IssueEventImporter) + .to receive(:new) + .with( + an_instance_of(Gitlab::GithubImport::Representation::IssueEvent), + project, + client + ) + .and_return(event_importer) + + expect(event_importer).to receive(:execute) + + importer.sequential_import + end + end + + describe '#parallel_import' do + it 'imports each note in parallel' do + allow(importer).to receive(:each_object_to_import).and_yield(issue_event) + + expect(Gitlab::GithubImport::ImportIssueEventWorker).to receive(:bulk_perform_in).with( + 1.second, [ + [project.id, an_instance_of(Hash), an_instance_of(String)] + ], batch_size: 1000, batch_delay: 1.minute + ) + + waiter = importer.parallel_import + + expect(waiter).to be_an_instance_of(Gitlab::JobWaiter) + expect(waiter.jobs_remaining).to eq(1) + end + end + + describe '#importer_class' do + it { expect(importer.importer_class).to eq Gitlab::GithubImport::Importer::IssueEventImporter } + end + + describe '#representation_class' do + it { expect(importer.representation_class).to eq Gitlab::GithubImport::Representation::IssueEvent } + end + + describe '#sidekiq_worker_class' do + it { expect(importer.sidekiq_worker_class).to eq Gitlab::GithubImport::ImportIssueEventWorker } + end + + describe '#object_type' do + it { expect(importer.object_type).to eq :issue_event } + end + + describe '#collection_method' do + it { expect(importer.collection_method).to eq :repository_issue_events } + end + + describe '#id_for_already_imported_cache' do + it 'returns the ID of the given note' do + expect(importer.id_for_already_imported_cache(issue_event)).to eq(issue_event.id) + end + end + + describe '#collection_options' do + it { expect(importer.collection_options).to eq({}) } + end +end diff --git a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb index 087faeffe02..bb1ee79ad93 100644 --- a/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/single_endpoint_issue_events_importer_spec.rb @@ -53,7 +53,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter describe '#each_object_to_import', :clean_gitlab_redis_cache do let(:issue_event) do - struct = Struct.new(:id, :event, :created_at, :issue_db_id, keyword_init: true) + struct = Struct.new(:id, :event, :created_at, :issue, keyword_init: true) struct.new(id: rand(10), event: 'closed', created_at: '2022-04-26 18:30:53 UTC') end @@ -81,7 +81,7 @@ RSpec.describe Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter counter = 0 subject.each_object_to_import do |object| expect(object).to eq issue_event - expect(issue_event.issue_db_id).to eq issue.id + expect(issue_event.issue['number']).to eq issue.iid counter += 1 end expect(counter).to eq 1 diff --git a/spec/lib/gitlab/github_import/issuable_finder_spec.rb b/spec/lib/gitlab/github_import/issuable_finder_spec.rb index 3afd006109b..d550f15e8c5 100644 --- a/spec/lib/gitlab/github_import/issuable_finder_spec.rb +++ b/spec/lib/gitlab/github_import/issuable_finder_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::GithubImport::IssuableFinder, :clean_gitlab_redis_cache do let(:project) { double(:project, id: 4, group: nil) } let(:issue) do - double(:issue, issuable_type: MergeRequest, iid: 1) + double(:issue, issuable_type: MergeRequest, issuable_id: 1) end let(:finder) { described_class.new(project, issue) } 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 cf796b55b14..d3a98035e73 100644 --- a/spec/lib/gitlab/github_import/representation/issue_event_spec.rb +++ b/spec/lib/gitlab/github_import/representation/issue_event_spec.rb @@ -25,8 +25,8 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do expect(issue_event.source).to eq({ type: 'issue', id: 123456 }) end - it 'includes the issue_db_id' do - expect(issue_event.issue_db_id).to eq(100500) + it 'includes the issue data' do + expect(issue_event.issue).to eq({ number: 2, pull_request: pull_request }) end context 'when actor data present' do @@ -119,6 +119,24 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do end end + describe '#issuable_id' do + it 'returns issuable_id' do + expect(issue_event.issuable_id).to eq(2) + end + end + + describe '#issuable_type' do + context 'when event related to issue' do + it { expect(issue_event.issuable_type).to eq('Issue') } + end + + context 'when event related to pull request' do + let(:pull_request) { { url: FFaker::Internet.http_url } } + + it { expect(issue_event.issuable_type).to eq('MergeRequest') } + end + end + describe '#github_identifiers' do it 'returns a hash with needed identifiers' do expect(issue_event.github_identifiers).to eq({ id: 6501124486 }) @@ -130,7 +148,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:response) do event_resource = Struct.new( :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, + :source, :assignee, :assigner, :issue, :created_at, :performed_via_github_app, keyword_init: true ) user_resource = Struct.new(:id, :login, keyword_init: true) @@ -149,7 +167,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do 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, + issue: { 'number' => 2, 'pull_request' => pull_request }, created_at: '2022-04-26 18:30:53 UTC', performed_via_github_app: nil ) @@ -160,6 +178,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_rename) { true } let(:with_milestone) { true } let(:with_assignee) { true } + let(:pull_request) { nil } it_behaves_like 'an IssueEvent' do let(:issue_event) { described_class.from_api_response(response) } @@ -185,7 +204,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do '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, + 'issue' => { 'number' => 2, 'pull_request' => pull_request }, 'created_at' => '2022-04-26 18:30:53 UTC', 'performed_via_github_app' => nil } @@ -196,6 +215,7 @@ RSpec.describe Gitlab::GithubImport::Representation::IssueEvent do let(:with_rename) { true } let(:with_milestone) { true } let(:with_assignee) { true } + let(:pull_request) { nil } let(:issue_event) { described_class.from_json_hash(hash) } end diff --git a/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb b/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb new file mode 100644 index 00000000000..9421561aea4 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_cross_project_authorization_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'when the user cannot read cross project' do |action, params| + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(false) + end + + it 'blocks access without a project_id' do + get action, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'allows access with a project_id' do + get action, params: params.merge(project_id: create(:project, :public).id) + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb new file mode 100644 index 00000000000..6b72988b3e6 --- /dev/null +++ b/spec/support/shared_examples/controllers/search_external_authorization_service_shared_examples.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'with external authorization service enabled' do |action, params| + include ExternalAuthorizationServiceHelpers + + let(:project) { create(:project, namespace: user.namespace) } + let(:note) { create(:note_on_issue, project: project) } + + before do + enable_external_authorization_service_check + end + + it 'renders a 403 when no project is given' do + get action, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + + it 'renders a 200 when a project was set' do + get action, params: params.merge(project_id: project.id) + + expect(response).to have_gitlab_http_status(:ok) + end +end diff --git a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb index b3c6a48767c..932152c0764 100644 --- a/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb +++ b/spec/workers/gitlab/github_import/stage/import_issue_events_worker_spec.rb @@ -8,37 +8,66 @@ RSpec.describe Gitlab::GithubImport::Stage::ImportIssueEventsWorker do let(:project) { create(:project) } let!(:group) { create(:group, projects: [project]) } let(:feature_flag_state) { [group] } + let(:single_endpoint_feature_flag_state) { [group] } describe '#import' do let(:importer) { instance_double('Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter') } let(:client) { instance_double('Gitlab::GithubImport::Client') } before do + stub_feature_flags(github_importer_single_endpoint_issue_events_import: single_endpoint_feature_flag_state) stub_feature_flags(github_importer_issue_events_import: feature_flag_state) end - it 'imports all the issue events' do - waiter = Gitlab::JobWaiter.new(2, '123') + context 'when single endpoint feature flag enabled' do + it 'imports all the issue events' do + waiter = Gitlab::JobWaiter.new(2, '123') - expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter) - .to receive(:new) - .with(project, client) - .and_return(importer) + expect(Gitlab::GithubImport::Importer::IssueEventsImporter).not_to receive(:new) + expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter) + .to receive(:new) + .with(project, client) + .and_return(importer) - expect(importer).to receive(:execute).and_return(waiter) + expect(importer).to receive(:execute).and_return(waiter) - expect(Gitlab::GithubImport::AdvanceStageWorker) - .to receive(:perform_async) - .with(project.id, { '123' => 2 }, :notes) + expect(Gitlab::GithubImport::AdvanceStageWorker) + .to receive(:perform_async) + .with(project.id, { '123' => 2 }, :notes) - worker.import(client, project) + worker.import(client, project) + end + end + + context 'when import issue events feature flag enabled' do + let(:single_endpoint_feature_flag_state) { false } + + it 'imports the issue events partly' do + waiter = Gitlab::JobWaiter.new(2, '123') + + expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter).not_to receive(:new) + expect(Gitlab::GithubImport::Importer::IssueEventsImporter) + .to receive(:new) + .with(project, client) + .and_return(importer) + + expect(importer).to receive(:execute).and_return(waiter) + + expect(Gitlab::GithubImport::AdvanceStageWorker) + .to receive(:perform_async) + .with(project.id, { '123' => 2 }, :notes) + + worker.import(client, project) + end end - context 'when feature flag is disabled' do + context 'when feature flags are disabled' do let(:feature_flag_state) { false } + let(:single_endpoint_feature_flag_state) { false } it 'skips issue events import and calls next stage' do expect(Gitlab::GithubImport::Importer::SingleEndpointIssueEventsImporter).not_to receive(:new) + expect(Gitlab::GithubImport::Importer::IssueEventsImporter).not_to receive(:new) expect(Gitlab::GithubImport::AdvanceStageWorker).to receive(:perform_async).with(project.id, {}, :notes) worker.import(client, project) |