diff options
Diffstat (limited to 'spec')
18 files changed, 812 insertions, 170 deletions
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb index dd995e99b9f..afab4514ce2 100644 --- a/spec/finders/members_finder_spec.rb +++ b/spec/finders/members_finder_spec.rb @@ -161,6 +161,37 @@ RSpec.describe MembersFinder, feature_category: :subgroups do expect(result).to eq([member3, member2, member1]) end + context 'with :shared_into_ancestors' do + let_it_be(:invited_group) do + create(:group).tap do |invited_group| + create(:group_group_link, shared_group: nested_group, shared_with_group: invited_group) + end + end + + let_it_be(:invited_group_member) { create(:group_member, :developer, group: invited_group, user: user1) } + let_it_be(:namespace_parent_member) { create(:group_member, :owner, group: group, user: user2) } + let_it_be(:namespace_member) { create(:group_member, :developer, group: nested_group, user: user3) } + let_it_be(:project_member) { create(:project_member, :developer, project: project, user: user4) } + + subject(:result) { described_class.new(project, user4).execute(include_relations: include_relations) } + + context 'when :shared_into_ancestors is included in the relations' do + let(:include_relations) { [:inherited, :direct, :invited_groups, :shared_into_ancestors] } + + it "includes members of groups invited into ancestors of project's group" do + expect(result).to match_array([namespace_parent_member, namespace_member, invited_group_member, project_member]) + end + end + + context 'when :shared_into_ancestors is not included in the relations' do + let(:include_relations) { [:inherited, :direct, :invited_groups] } + + it "does not include members of groups invited into ancestors of project's group" do + expect(result).to match_array([namespace_parent_member, namespace_member, project_member]) + end + end + end + context 'when :invited_groups is passed' do shared_examples 'with invited_groups param' do subject { described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups]) } diff --git a/spec/frontend/content_editor/components/toolbar_button_spec.js b/spec/frontend/content_editor/components/toolbar_button_spec.js index 1556f761682..ffe1ae20ee9 100644 --- a/spec/frontend/content_editor/components/toolbar_button_spec.js +++ b/spec/frontend/content_editor/components/toolbar_button_spec.js @@ -81,7 +81,7 @@ describe('content_editor/components/toolbar_button', () => { await emitEditorEvent({ event: 'transaction', tiptapEditor }); - expect(findButton().classes().includes('active')).toBe(outcome); + expect(findButton().classes().includes('gl-bg-gray-100!')).toBe(outcome); expect(tiptapEditor.isActive).toHaveBeenCalledWith(CONTENT_TYPE); }, ); diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js index 87208ec7aa8..51fcf26c39a 100644 --- a/spec/frontend/editor/schema/ci/ci_schema_spec.js +++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js @@ -27,6 +27,7 @@ import CacheYaml from './yaml_tests/positive_tests/cache.yml'; import FilterYaml from './yaml_tests/positive_tests/filter.yml'; import IncludeYaml from './yaml_tests/positive_tests/include.yml'; import RulesYaml from './yaml_tests/positive_tests/rules.yml'; +import RulesNeedsYaml from './yaml_tests/positive_tests/rules_needs.yml'; import ProjectPathYaml from './yaml_tests/positive_tests/project_path.yml'; import VariablesYaml from './yaml_tests/positive_tests/variables.yml'; import JobWhenYaml from './yaml_tests/positive_tests/job_when.yml'; @@ -46,6 +47,7 @@ import ProjectPathIncludeLeadSlashYaml from './yaml_tests/negative_tests/project import ProjectPathIncludeNoSlashYaml from './yaml_tests/negative_tests/project_path/include/no_slash.yml'; import ProjectPathIncludeTailSlashYaml from './yaml_tests/negative_tests/project_path/include/tailing_slash.yml'; import RulesNegativeYaml from './yaml_tests/negative_tests/rules.yml'; +import RulesNeedsNegativeYaml from './yaml_tests/negative_tests/rules_needs.yml'; import TriggerNegative from './yaml_tests/negative_tests/trigger.yml'; import VariablesInvalidOptionsYaml from './yaml_tests/negative_tests/variables/invalid_options.yml'; import VariablesInvalidSyntaxDescYaml from './yaml_tests/negative_tests/variables/invalid_syntax_desc.yml'; @@ -88,6 +90,7 @@ describe('positive tests', () => { JobWhenYaml, HooksYaml, RulesYaml, + RulesNeedsYaml, VariablesYaml, ProjectPathYaml, IdTokensYaml, @@ -121,6 +124,7 @@ describe('negative tests', () => { IncludeNegativeYaml, JobWhenNegativeYaml, RulesNegativeYaml, + RulesNeedsNegativeYaml, TriggerNegative, VariablesInvalidOptionsYaml, VariablesInvalidSyntaxDescYaml, diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules_needs.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules_needs.yml new file mode 100644 index 00000000000..f2f1eb118f8 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules_needs.yml @@ -0,0 +1,46 @@ +# invalid rules:needs +lint_job: + script: exit 0 + rules: + - if: $var == null + needs: + +# invalid rules:needs +lint_job_2: + script: exit 0 + rules: + - if: $var == null + needs: [20] + +# invalid rules:needs +lint_job_3: + script: exit 0 + rules: + - if: $var == null + needs: + - job: + +# invalid rules:needs +lint_job_5: + script: exit 0 + rules: + - if: $var == null + needs: + - pipeline: 5 + +# invalid rules:needs +lint_job_6: + script: exit 0 + rules: + - if: $var == null + needs: + - project: namespace/group/project-name + +# invalid rules:needs +lint_job_7: + script: exit 0 + rules: + - if: $var == null + needs: + - pipeline: 5 + job: lint_job_6 diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules_needs.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules_needs.yml new file mode 100644 index 00000000000..a4a5183dcf4 --- /dev/null +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules_needs.yml @@ -0,0 +1,32 @@ +# valid workflow:rules:needs +pre_lint_job: + script: exit 0 + rules: + - if: $var == null + +lint_job: + script: exit 0 + rules: + - if: $var == null + +rspec_job: + script: exit 0 + rules: + - if: $var == null + needs: [lint_job] + +job: + needs: [rspec_job] + script: exit 0 + rules: + - if: $var == null + needs: + - job: lint_job + artifacts: false + optional: true + - job: pre_lint_job + artifacts: true + optional: false + - rspec_job + - if: $var == true + needs: [lint_job, pre_lint_job]
\ No newline at end of file diff --git a/spec/frontend/environments/environment_details/deployments_table_spec.js b/spec/frontend/environments/environment_details/deployments_table_spec.js new file mode 100644 index 00000000000..7dad5617383 --- /dev/null +++ b/spec/frontend/environments/environment_details/deployments_table_spec.js @@ -0,0 +1,58 @@ +import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.json'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import Commit from '~/vue_shared/components/commit.vue'; +import DeploymentStatusLink from '~/environments/environment_details/components/deployment_status_link.vue'; +import DeploymentJob from '~/environments/environment_details/components/deployment_job.vue'; +import DeploymentTriggerer from '~/environments/environment_details/components/deployment_triggerer.vue'; +import DeploymentActions from '~/environments/environment_details/components/deployment_actions.vue'; +import DeploymentsTable from '~/environments/environment_details/deployments_table.vue'; +import { convertToDeploymentTableRow } from '~/environments/helpers/deployment_data_transformation_helper'; + +const { environment } = resolvedEnvironmentDetails.data.project; +const deployments = environment.deployments.nodes.map((d) => + convertToDeploymentTableRow(d, environment), +); + +describe('~/environments/environment_details/index.vue', () => { + let wrapper; + + const createWrapper = (propsData = {}) => { + wrapper = mountExtended(DeploymentsTable, { + propsData: { + deployments, + ...propsData, + }, + }); + }; + + describe('deployment row', () => { + const [, , deployment] = deployments; + + let row; + + beforeEach(() => { + createWrapper(); + + row = wrapper.find('tr:nth-child(3)'); + }); + + it.each` + cell | component | props + ${'status'} | ${DeploymentStatusLink} | ${{ deploymentJob: deployment.job, status: deployment.status }} + ${'triggerer'} | ${DeploymentTriggerer} | ${{ triggerer: deployment.triggerer }} + ${'commit'} | ${Commit} | ${deployment.commit} + ${'job'} | ${DeploymentJob} | ${{ job: deployment.job }} + ${'created date'} | ${'[data-testid="deployment-created-at"]'} | ${{ time: deployment.created }} + ${'deployed date'} | ${'[data-testid="deployment-deployed-at"]'} | ${{ time: deployment.deployed }} + ${'deployment actions'} | ${DeploymentActions} | ${{ actions: deployment.actions, rollback: deployment.rollback, approvalEnvironment: deployment.deploymentApproval }} + `('should show the correct component for $cell', ({ component, props }) => { + expect(row.findComponent(component).props()).toMatchObject(props); + }); + + it('hides the deployed at timestamp for not-finished deployments', () => { + row = wrapper.find('tr'); + + expect(row.find('[data-testid="deployment-deployed-at"]').exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/fixtures/environments.rb b/spec/frontend/fixtures/environments.rb index 77e2a96b328..81f1eb11e3e 100644 --- a/spec/frontend/fixtures/environments.rb +++ b/spec/frontend/fixtures/environments.rb @@ -44,7 +44,7 @@ RSpec.describe 'Environments (JavaScript fixtures)', feature_category: :environm end let_it_be(:deployment_success) do - create(:deployment, :success, environment: environment, deployable: build) + create(:deployment, :success, environment: environment, deployable: build, finished_at: 1.hour.since) end let_it_be(:deployment_failed) do diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js index 4b2ce24a49f..d888abc19ef 100644 --- a/spec/frontend/vue_shared/components/web_ide_link_spec.js +++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js @@ -1,4 +1,4 @@ -import { GlButton, GlLink, GlModal, GlPopover } from '@gitlab/ui'; +import { GlButton, GlModal } from '@gitlab/ui'; import { nextTick } from 'vue'; import ActionsButton from '~/vue_shared/components/actions_button.vue'; @@ -9,7 +9,6 @@ import WebIdeLink, { PREFERRED_EDITOR_KEY, } from '~/vue_shared/components/web_ide_link.vue'; import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue'; -import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; import { KEY_WEB_IDE } from '~/vue_shared/components/constants'; import { stubComponent } from 'helpers/stub_component'; @@ -95,14 +94,7 @@ describe('Web IDE link component', () => { let wrapper; - function createComponent( - props, - { - mountFn = shallowMountExtended, - glFeatures = {}, - userCalloutDismisserSlotProps = { dismiss: jest.fn() }, - } = {}, - ) { + function createComponent(props, { mountFn = shallowMountExtended, glFeatures = {} } = {}) { wrapper = mountFn(WebIdeLink, { propsData: { editUrl: TEST_EDIT_URL, @@ -124,11 +116,6 @@ describe('Web IDE link component', () => { <slot name="modal-footer"></slot> </div>`, }), - UserCalloutDismisser: stubComponent(UserCalloutDismisser, { - render() { - return this.$scopedSlots.default(userCalloutDismisserSlotProps); - }, - }), }, }); } @@ -141,13 +128,6 @@ describe('Web IDE link component', () => { const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); const findModal = () => wrapper.findComponent(GlModal); const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal); - const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser); - const findNewWebIdeCalloutPopover = () => wrapper.findComponent(GlPopover); - const findTryItOutLink = () => - wrapper - .findAllComponents(GlLink) - .filter((link) => link.text().includes('Try it out')) - .at(0); it.each([ { @@ -446,132 +426,6 @@ describe('Web IDE link component', () => { }); }); - describe('Web IDE callout', () => { - describe('vscode_web_ide feature flag is enabled and the edit button is not shown', () => { - let dismiss; - - beforeEach(() => { - dismiss = jest.fn(); - createComponent( - { - showEditButton: false, - }, - { - glFeatures: { vscodeWebIde: true }, - userCalloutDismisserSlotProps: { dismiss }, - }, - ); - }); - it('does not skip the user_callout_dismisser query', () => { - expect(findUserCalloutDismisser().props()).toEqual( - expect.objectContaining({ - skipQuery: false, - featureName: 'vscode_web_ide_callout', - }), - ); - }); - - it('mounts new web ide callout popover', () => { - expect(findNewWebIdeCalloutPopover().props()).toEqual( - expect.objectContaining({ - showCloseButton: '', - target: 'web-ide-link', - triggers: 'manual', - boundaryPadding: 80, - }), - ); - }); - - describe.each` - calloutStatus | shouldShowCallout | popoverVisibility | tooltipVisibility - ${'show'} | ${true} | ${true} | ${false} - ${'hide'} | ${false} | ${false} | ${true} - `( - 'when should $calloutStatus web ide callout', - ({ shouldShowCallout, popoverVisibility, tooltipVisibility }) => { - beforeEach(() => { - createComponent( - { - showEditButton: false, - }, - { - glFeatures: { vscodeWebIde: true }, - userCalloutDismisserSlotProps: { shouldShowCallout, dismiss }, - }, - ); - }); - - it(`popover visibility = ${popoverVisibility}`, () => { - expect(findNewWebIdeCalloutPopover().props().show).toBe(popoverVisibility); - }); - - it(`action button tooltip visibility = ${tooltipVisibility}`, () => { - expect(findActionsButton().props().showActionTooltip).toBe(tooltipVisibility); - }); - }, - ); - - it('dismisses the callout when popover close button is clicked', () => { - findNewWebIdeCalloutPopover().vm.$emit('close-button-clicked'); - - expect(dismiss).toHaveBeenCalled(); - }); - - it('dismisses the callout when try it now link is clicked', () => { - findTryItOutLink().vm.$emit('click'); - - expect(dismiss).toHaveBeenCalled(); - }); - - it('dismisses the callout when action button is clicked', () => { - findActionsButton().vm.$emit('actionClicked'); - - expect(dismiss).toHaveBeenCalled(); - }); - }); - - describe.each` - featureFlag | showEditButton - ${false} | ${true} - ${true} | ${false} - ${false} | ${false} - `( - 'when vscode_web_ide=$featureFlag and showEditButton = $showEditButton', - ({ vscodeWebIde, showEditButton }) => { - let dismiss; - - beforeEach(() => { - dismiss = jest.fn(); - - createComponent( - { - showEditButton, - }, - { glFeatures: { vscodeWebIde }, userCalloutDismisserSlotProps: { dismiss } }, - ); - }); - - it('skips the user_callout_dismisser query', () => { - expect(findUserCalloutDismisser().props().skipQuery).toBe(true); - }); - - it('displays actions button tooltip', () => { - expect(findActionsButton().props().showActionTooltip).toBe(true); - }); - - it('mounts new web ide callout popover', () => { - expect(findNewWebIdeCalloutPopover().exists()).toBe(false); - }); - - it('does not dismiss the callout when action button is clicked', () => { - findActionsButton().vm.$emit('actionClicked'); - - expect(dismiss).not.toHaveBeenCalled(); - }); - }, - ); - }); - describe('when vscode_web_ide feature flag is enabled', () => { describe('when is not showing edit button', () => { describe(`when ${PREFERRED_EDITOR_RESET_KEY} is unset`, () => { diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index b7fdadbd036..dd0d6d1246f 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -102,7 +102,7 @@ RSpec.describe AvatarsHelper, feature_category: :source_code_management do end describe '#avatar_icon_for_email', :clean_gitlab_redis_cache do - let(:user) { create(:user, :public_email, avatar: File.open(uploaded_image_temp_path)) } + let(:user) { create(:user, :public_email, :commit_email, avatar: File.open(uploaded_image_temp_path)) } subject { helper.avatar_icon_for_email(user.email).to_s } @@ -131,13 +131,22 @@ RSpec.describe AvatarsHelper, feature_category: :source_code_management do end context 'without an email passed' do - it 'calls gravatar_icon' do - expect(helper).to receive(:gravatar_icon).with(nil, 20, 2) - expect(User).not_to receive(:find_by_any_email) + it 'returns the default avatar' do + expect(helper).to receive(:default_avatar) + expect(User).not_to receive(:with_public_email) helper.avatar_icon_for_email(nil, 20, 2) end end + + context 'with a blank email address' do + it 'returns the default avatar' do + expect(helper).to receive(:default_avatar) + expect(User).not_to receive(:with_public_email) + + helper.avatar_icon_for_email('', 20, 2) + end + end end end diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb index d1e3c7d2240..17d28b07763 100644 --- a/spec/helpers/nav_helper_spec.rb +++ b/spec/helpers/nav_helper_spec.rb @@ -161,6 +161,21 @@ RSpec.describe NavHelper, feature_category: :navigation do context 'with feature flag on' do let(:new_nav_ff) { true } + context 'when user has not interacted with the new nav toggle yet' do + let(:user_preference) { nil } + + specify { expect(subject).to eq false } + + context 'when the user was enrolled into the new nav via a special feature flag' do + before do + # this ff is disabled in globally to keep tests of the old nav working + stub_feature_flags(super_sidebar_nav_enrolled: true) + end + + specify { expect(subject).to eq true } + end + end + context 'when user has new nav disabled' do let(:user_preference) { false } diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb index e82dcd0254d..1ece0f6b7b9 100644 --- a/spec/lib/gitlab/ci/build/rules_spec.rb +++ b/spec/lib/gitlab/ci/build/rules_spec.rb @@ -181,6 +181,108 @@ RSpec.describe Gitlab::Ci::Build::Rules do end end + context 'with needs' do + context 'when single needs is specified' do + let(:rule_list) do + [{ if: '$VAR == null', needs: [{ name: 'test', artifacts: true, optional: false }] }] + end + + it { + is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, + [{ name: 'test', artifacts: true, optional: false }], nil)) + } + end + + context 'when multiple needs are specified' do + let(:rule_list) do + [{ if: '$VAR == null', + needs: [{ name: 'test', artifacts: true, optional: false }, + { name: 'rspec', artifacts: true, optional: false }] }] + end + + it { + is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, + [{ name: 'test', artifacts: true, optional: false }, + { name: 'rspec', artifacts: true, optional: false }], nil)) + } + end + + context 'when there are no needs specified' do + let(:rule_list) { [{ if: '$VAR == null' }] } + + it { is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, nil, nil)) } + end + + context 'when need is specified with additional attibutes' do + let(:rule_list) do + [{ if: '$VAR == null', needs: [{ + artifacts: true, + name: 'test', + optional: false, + when: 'never' + }] }] + end + + it { + is_expected.to eq( + described_class::Result.new('on_success', nil, nil, nil, + [{ artifacts: true, name: 'test', optional: false, when: 'never' }], nil)) + } + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(introduce_rules_with_needs: false) + end + + context 'with needs' do + context 'when single needs is specified' do + let(:rule_list) do + [{ if: '$VAR == null', needs: [{ name: 'test', artifacts: true, optional: false }] }] + end + + it { + is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, nil, nil)) + } + end + + context 'when multiple needs are specified' do + let(:rule_list) do + [{ if: '$VAR == null', + needs: [{ name: 'test', artifacts: true, optional: false }, + { name: 'rspec', artifacts: true, optional: false }] }] + end + + it { + is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, nil, nil)) + } + end + + context 'when there are no needs specified' do + let(:rule_list) { [{ if: '$VAR == null' }] } + + it { is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, nil, nil)) } + end + + context 'when need is specified with additional attibutes' do + let(:rule_list) do + [{ if: '$VAR == null', needs: [{ + artifacts: true, + name: 'test', + optional: false, + when: 'never' + }] }] + end + + it { + is_expected.to eq( + described_class::Result.new('on_success', nil, nil, nil, nil, nil)) + } + end + end + end + end + context 'with variables' do context 'with matching rule' do let(:rule_list) { [{ if: '$VAR == null', variables: { MY_VAR: 'my var' } }] } @@ -208,9 +310,10 @@ RSpec.describe Gitlab::Ci::Build::Rules do let(:start_in) { nil } let(:allow_failure) { nil } let(:variables) { nil } + let(:needs) { nil } subject(:result) do - Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure, variables) + Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure, variables, needs) end describe '#build_attributes' do @@ -221,6 +324,45 @@ RSpec.describe Gitlab::Ci::Build::Rules do it 'compacts nil values' do is_expected.to eq(options: {}, when: 'on_success') end + + context 'scheduling_type' do + context 'when rules have needs' do + context 'single need' do + let(:needs) do + { job: [{ name: 'test' }] } + end + + it 'saves needs' do + expect(subject[:needs_attributes]).to eq([{ name: "test" }]) + end + + it 'adds schedule type to the build_attributes' do + expect(subject[:scheduling_type]).to eq(:dag) + end + end + + context 'multiple needs' do + let(:needs) do + { job: [{ name: 'test' }, { name: 'test_2', artifacts: true, optional: false }] } + end + + it 'saves needs' do + expect(subject[:needs_attributes]).to match_array([{ name: "test" }, + { name: 'test_2', artifacts: true, optional: false }]) + end + + it 'adds schedule type to the build_attributes' do + expect(subject[:scheduling_type]).to eq(:dag) + end + end + end + + context 'when rules do not have needs' do + it 'does not add schedule type to the build_attributes' do + expect(subject.key?(:scheduling_type)).to be_falsy + end + end + end end describe '#pass?' do diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 86a11111283..9d5a9bc8058 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -109,6 +109,104 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_co end end + context 'with job:rules:[needs:]' do + context 'with a single rule' do + let(:job_needs_attributes) { [{ name: 'rspec' }] } + + context 'when job has needs set' do + context 'when rule evaluates to true' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + needs_attributes: job_needs_attributes, + rules: [{ if: '$VAR == null', needs: { job: [{ name: 'build-job' }] } }] } + end + + it 'overrides the job needs' do + expect(subject).to include(needs_attributes: [{ name: 'build-job' }]) + end + end + + context 'when rule evaluates to false' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + needs_attributes: job_needs_attributes, + rules: [{ if: '$VAR == true', needs: { job: [{ name: 'build-job' }] } }] } + end + + it 'keeps the job needs' do + expect(subject).to include(needs_attributes: job_needs_attributes) + end + end + + context 'with subkeys: artifacts, optional' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + rules: + [ + { if: '$VAR == null', + needs: { + job: [{ + name: 'build-job', + optional: false, + artifacts: true + }] + } } + ] } + end + + context 'when rule evaluates to true' do + it 'sets the job needs as well as the job subkeys' do + expect(subject[:needs_attributes]).to match_array([{ name: 'build-job', optional: false, artifacts: true }]) + end + + it 'sets the scheduling type to dag' do + expect(subject[:scheduling_type]).to eq(:dag) + end + end + end + end + + context 'with multiple rules' do + context 'when a rule evaluates to true' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + needs_attributes: job_needs_attributes, + rules: [ + { if: '$VAR == true', needs: { job: [{ name: 'rspec-1' }] } }, + { if: '$VAR2 == true', needs: { job: [{ name: 'rspec-2' }] } }, + { if: '$VAR3 == null', needs: { job: [{ name: 'rspec' }, { name: 'lint' }] } } + ] } + end + + it 'overrides the job needs' do + expect(subject).to include(needs_attributes: [{ name: 'rspec' }, { name: 'lint' }]) + end + end + + context 'when all rules evaluates to false' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + needs_attributes: job_needs_attributes, + rules: [ + { if: '$VAR == true', needs: { job: [{ name: 'rspec-1' }] } }, + { if: '$VAR2 == true', needs: { job: [{ name: 'rspec-2' }] } }, + { if: '$VAR3 == true', needs: { job: [{ name: 'rspec-3' }] } } + ] } + end + + it 'keeps the job needs' do + expect(subject).to include(needs_attributes: job_needs_attributes) + end + end + end + end + end + context 'with job:tags' do let(:attributes) do { diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index a35dd968cd6..f8c2889798f 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -659,6 +659,191 @@ module Gitlab it_behaves_like 'has warnings and expected error', /build job: need test is not defined in current or prior stages/ end + + describe '#validate_job_needs!' do + context "when all validations pass" do + let(:config) do + <<-EOYML + stages: + - lint + lint_job: + needs: [lint_job_2] + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: + - lint_job_2 + - job: lint_job_3 + optional: true + lint_job_2: + stage: lint + script: 'echo job' + rules: + - if: $var == null + lint_job_3: + stage: lint + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it 'returns a valid response' do + expect(subject).to be_valid + expect(subject).to be_instance_of(Gitlab::Ci::YamlProcessor::Result) + end + end + + context 'needs as array' do + context 'single need in following stage' do + let(:config) do + <<-EOYML + stages: + - lint + - test + lint_job: + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: [test_job] + test_job: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'lint_job job: need test_job is not defined in current or prior stages' + end + + context 'multiple needs in the following stage' do + let(:config) do + <<-EOYML + stages: + - lint + - test + lint_job: + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: [test_job, test_job_2] + test_job: + stage: test + script: 'echo job' + rules: + - if: $var == null + test_job_2: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'lint_job job: need test_job is not defined in current or prior stages' + end + + context 'single need in following state - hyphen need' do + let(:config) do + <<-EOYML + stages: + - lint + - test + lint_job: + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: + - test_job + test_job: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'lint_job job: need test_job is not defined in current or prior stages' + end + + context 'when there are duplicate needs (string and hash)' do + let(:config) do + <<-EOYML + stages: + - test + test_job_1: + stage: test + script: 'echo lint_job' + rules: + - if: $var == null + needs: + - test_job_2 + - job: test_job_2 + test_job_2: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'test_job_1 has the following needs duplicated: test_job_2.' + end + end + + context 'rule needs as hash' do + context 'single hash need in following stage' do + let(:config) do + <<-EOYML + stages: + - lint + - test + lint_job: + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: + - job: test_job + artifacts: false + optional: false + test_job: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'lint_job job: need test_job is not defined in current or prior stages' + end + end + + context 'job rule need does not exist' do + let(:config) do + <<-EOYML + build: + stage: build + script: echo + rules: + - when: always + test: + stage: test + script: echo + rules: + - if: $var == null + needs: [unknown_job] + EOYML + end + + it_behaves_like 'has warnings and expected error', /test job: undefined need: unknown_job/ + end + end end end diff --git a/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb b/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb index 81b00f82803..8e2a53ea76f 100644 --- a/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb +++ b/spec/lib/gitlab/database/partitioning/list/convert_table_spec.rb @@ -286,6 +286,26 @@ RSpec.describe Gitlab::Database::Partitioning::List::ConvertTable, feature_categ expect(migration_context.has_loose_foreign_key?(table_name)).to be_truthy expect(migration_context.has_loose_foreign_key?(parent_table_name)).to be_truthy end + + context 'with locking tables' do + let(:lock_tables) { [table_name] } + + it 'locks the table before dropping the triggers' do + recorder = ActiveRecord::QueryRecorder.new { partition } + + lock_index = recorder.log.find_index do |log| + log.start_with?('LOCK "_test_table_to_partition" IN ACCESS EXCLUSIVE MODE') + end + + trigger_index = recorder.log.find_index do |log| + log.start_with?('DROP TRIGGER IF EXISTS _test_table_to_partition_loose_fk_trigger') + end + + expect(lock_index).to be_present + expect(trigger_index).to be_present + expect(lock_index).to be < trigger_index + end + end end end diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 85ccfa3cf51..353fddcb08d 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -130,6 +130,8 @@ RSpec.describe API::Members, feature_category: :subgroups do let(:project_user) { create(:user) } let(:linked_group_user) { create(:user) } let!(:project_group_link) { create(:project_group_link, project: project, group: linked_group) } + let(:invited_group_developer) { create(:user, username: 'invited_group_developer') } + let(:invited_group) { create(:group) { |group| group.add_developer(invited_group_developer) } } let(:project) do create(:project, :public, group: nested_group) do |project| @@ -146,19 +148,21 @@ RSpec.describe API::Members, feature_category: :subgroups do let(:nested_group) do create(:group, parent: group) do |nested_group| nested_group.add_developer(nested_user) + create(:group_group_link, :guest, shared_with_group: invited_group, shared_group: nested_group) end end - it 'finds all project members including inherited members' do + it 'finds all project members including inherited members and members shared into ancestor groups' do get api("/projects/#{project.id}/members/all", developer) expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id, nested_user.id, project_user.id, linked_group_user.id] + expected_user_ids = [maintainer.id, developer.id, nested_user.id, project_user.id, linked_group_user.id, invited_group_developer.id] + expect(json_response.map { |u| u['id'] }).to match_array expected_user_ids end - it 'returns only one member for each user without returning duplicated members' do + it 'returns only one member for each user without returning duplicated members with correct access levels' do linked_group.add_developer(developer) get api("/projects/#{project.id}/members/all", developer) @@ -172,7 +176,8 @@ RSpec.describe API::Members, feature_category: :subgroups do [maintainer.id, Gitlab::Access::OWNER], [nested_user.id, Gitlab::Access::DEVELOPER], [project_user.id, Gitlab::Access::DEVELOPER], - [linked_group_user.id, Gitlab::Access::DEVELOPER] + [linked_group_user.id, Gitlab::Access::DEVELOPER], + [invited_group_developer.id, Gitlab::Access::GUEST] ] expect(json_response.map { |u| [u['id'], u['access_level']] }).to match_array(expected_users_and_access_levels) end @@ -183,7 +188,8 @@ RSpec.describe API::Members, feature_category: :subgroups do expect(response).to have_gitlab_http_status(:ok) expect(response).to include_pagination_headers expect(json_response).to be_an Array - expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id, nested_user.id] + expected_user_ids = [maintainer.id, developer.id, nested_user.id, invited_group_developer.id] + expect(json_response.map { |u| u['id'] }).to match_array expected_user_ids end context 'with a subgroup' do diff --git a/spec/services/ci/create_pipeline_service/rules_spec.rb b/spec/services/ci/create_pipeline_service/rules_spec.rb index 19f9e7e3e4a..87112137675 100644 --- a/spec/services/ci/create_pipeline_service/rules_spec.rb +++ b/spec/services/ci/create_pipeline_service/rules_spec.rb @@ -386,6 +386,109 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes expect(regular_job.allow_failure).to eq(true) end end + + context 'with needs:' do + let(:config) do + <<-EOY + job1: + script: ls + + job2: + script: ls + rules: + - if: $var == null + needs: [job1] + - when: on_success + + job3: + script: ls + rules: + - if: $var == null + needs: [job1] + - needs: [job2] + + job4: + script: ls + needs: [job1] + rules: + - if: $var == null + needs: [job2] + - when: on_success + needs: [job3] + EOY + end + + let(:job1) { pipeline.builds.find_by(name: 'job1') } + let(:job2) { pipeline.builds.find_by(name: 'job2') } + let(:job3) { pipeline.builds.find_by(name: 'job3') } + let(:job4) { pipeline.builds.find_by(name: 'job4') } + + context 'when the `$var` rule matches' do + it 'creates a pipeline with overridden needs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job1', 'job2', 'job3', 'job4') + + expect(job1.needs).to be_empty + expect(job2.needs).to contain_exactly(an_object_having_attributes(name: 'job1')) + expect(job3.needs).to contain_exactly(an_object_having_attributes(name: 'job1')) + expect(job4.needs).to contain_exactly(an_object_having_attributes(name: 'job2')) + end + end + + context 'when the `$var` rule does not match' do + let(:initialization_params) { base_initialization_params.merge(variables_attributes: variables_attributes) } + + let(:variables_attributes) do + [{ key: 'var', secret_value: 'SOME_VAR' }] + end + + it 'creates a pipeline with overridden needs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job1', 'job2', 'job3', 'job4') + + expect(job1.needs).to be_empty + expect(job2.needs).to be_empty + expect(job3.needs).to contain_exactly(an_object_having_attributes(name: 'job2')) + expect(job4.needs).to contain_exactly(an_object_having_attributes(name: 'job3')) + end + end + + context 'when the FF introduce_rules_with_needs is disabled' do + before do + stub_feature_flags(introduce_rules_with_needs: false) + end + + context 'when the `$var` rule matches' do + it 'creates a pipeline without overridden needs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job1', 'job2', 'job3', 'job4') + + expect(job1.needs).to be_empty + expect(job2.needs).to be_empty + expect(job3.needs).to be_empty + expect(job4.needs).to contain_exactly(an_object_having_attributes(name: 'job1')) + end + end + + context 'when the `$var` rule does not match' do + let(:initialization_params) { base_initialization_params.merge(variables_attributes: variables_attributes) } + + let(:variables_attributes) do + [{ key: 'var', secret_value: 'SOME_VAR' }] + end + + it 'creates a pipeline without overridden needs' do + expect(pipeline).to be_persisted + expect(build_names).to contain_exactly('job1', 'job2', 'job3', 'job4') + + expect(job1.needs).to be_empty + expect(job2.needs).to be_empty + expect(job3.needs).to be_empty + expect(job4.needs).to contain_exactly(an_object_having_attributes(name: 'job1')) + end + end + end + end end context 'changes:' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 470df53051e..f8bbad393e6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -269,6 +269,10 @@ RSpec.configure do |config| stub_feature_flags(ci_queueing_disaster_recovery_disable_fair_scheduling: false) stub_feature_flags(ci_queueing_disaster_recovery_disable_quota: false) + # Only a few percent of users will be "enrolled" into the new nav with this flag. + # Having it enabled globally would make it impossible to test the current nav. + stub_feature_flags(super_sidebar_nav_enrolled: false) + # It's disabled in specs because we don't support certain features which # cause spec failures. stub_feature_flags(use_click_house_database_for_error_tracking: false) diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index 58659775d8c..493a96b8dae 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -79,7 +79,8 @@ RSpec.shared_examples 'an accessible' do let(:access) do [{ 'type' => 'repository', 'name' => project.full_path, - 'actions' => actions }] + 'actions' => actions, + 'meta' => { 'project_path' => project.full_path } }] end it_behaves_like 'a valid token' @@ -244,12 +245,14 @@ RSpec.shared_examples 'a container registry auth service' do { 'type' => 'repository', 'name' => project.full_path, - 'actions' => ['pull'] + 'actions' => ['pull'], + 'meta' => { 'project_path' => project.full_path } }, { 'type' => 'repository', 'name' => "#{project.full_path}/*", - 'actions' => ['pull'] + 'actions' => ['pull'], + 'meta' => { 'project_path' => project.full_path } } ] end @@ -822,16 +825,20 @@ RSpec.shared_examples 'a container registry auth service' do [ { 'type' => 'repository', 'name' => internal_project.full_path, - 'actions' => ['pull'] }, + 'actions' => ['pull'], + 'meta' => { 'project_path' => internal_project.full_path } }, { 'type' => 'repository', 'name' => private_project.full_path, - 'actions' => ['pull'] }, + 'actions' => ['pull'], + 'meta' => { 'project_path' => private_project.full_path } }, { 'type' => 'repository', 'name' => public_project.full_path, - 'actions' => ['pull'] }, + 'actions' => ['pull'], + 'meta' => { 'project_path' => public_project.full_path } }, { 'type' => 'repository', 'name' => public_project_private_container_registry.full_path, - 'actions' => ['pull'] } + 'actions' => ['pull'], + 'meta' => { 'project_path' => public_project_private_container_registry.full_path } } ] end end @@ -845,10 +852,12 @@ RSpec.shared_examples 'a container registry auth service' do [ { 'type' => 'repository', 'name' => internal_project.full_path, - 'actions' => ['pull'] }, + 'actions' => ['pull'], + 'meta' => { 'project_path' => internal_project.full_path } }, { 'type' => 'repository', 'name' => public_project.full_path, - 'actions' => ['pull'] } + 'actions' => ['pull'], + 'meta' => { 'project_path' => public_project.full_path } } ] end end @@ -862,7 +871,8 @@ RSpec.shared_examples 'a container registry auth service' do [ { 'type' => 'repository', 'name' => public_project.full_path, - 'actions' => ['pull'] } + 'actions' => ['pull'], + 'meta' => { 'project_path' => public_project.full_path } } ] end end @@ -1258,4 +1268,29 @@ RSpec.shared_examples 'a container registry auth service' do end end end + + context 'with a project with a path containing special characters' do + let_it_be(:bad_project) { create(:project) } + + before do + bad_project.update_attribute(:path, "#{bad_project.path}_") + end + + describe '#access_token' do + let(:token) { described_class.access_token(['pull'], [bad_project.full_path]) } + let(:access) do + [{ 'type' => 'repository', + 'name' => bad_project.full_path, + 'actions' => ['pull'] }] + end + + subject { { token: token } } + + it_behaves_like 'a valid token' + + it 'has the correct scope' do + expect(payload).to include('access' => access) + end + end + end end |