diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-11 18:10:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-11 18:10:20 +0300 |
commit | e3042fc5ced749e693ccef81b3f5838c55d5480c (patch) | |
tree | e004dca26da0ec413d5c9ebf174962a008fde0bb /spec | |
parent | c33a9adb709ffb40f816e66eb0c98cc750d6cd43 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
40 files changed, 908 insertions, 365 deletions
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb index 54b190a220a..b666f73110a 100644 --- a/spec/controllers/groups/group_members_controller_spec.rb +++ b/spec/controllers/groups/group_members_controller_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Groups::GroupMembersController do expect(response).to render_template(:index) end - context 'user with owner access' do + context 'when user can manage members' do let_it_be(:invited) { create_list(:group_member, 3, :invited, group: group) } before do @@ -71,6 +71,19 @@ RSpec.describe Groups::GroupMembersController do end end + context 'when user cannot manage members' do + before do + sign_in(user) + end + + it 'does not assign invited members or skip_groups', :aggregate_failures do + get :index, params: { group_id: group } + + expect(assigns(:invited_members)).to be_nil + expect(assigns(:skip_groups)).to be_nil + end + end + context 'when user has owner access to subgroup' do let_it_be(:nested_group) { create(:group, parent: group) } let_it_be(:nested_group_user) { create(:user) } diff --git a/spec/finders/concerns/packages/finder_helper_spec.rb b/spec/finders/concerns/packages/finder_helper_spec.rb index c1740ee1796..bad4c482bc6 100644 --- a/spec/finders/concerns/packages/finder_helper_spec.rb +++ b/spec/finders/concerns/packages/finder_helper_spec.rb @@ -3,6 +3,30 @@ require 'spec_helper' RSpec.describe ::Packages::FinderHelper do + describe '#packages_for_project' do + let_it_be_with_reload(:project1) { create(:project) } + let_it_be(:package1) { create(:package, project: project1) } + let_it_be(:package2) { create(:package, :error, project: project1) } + let_it_be(:project2) { create(:project) } + let_it_be(:package3) { create(:package, project: project2) } + + let(:finder_class) do + Class.new do + include ::Packages::FinderHelper + + def execute(project1) + packages_for_project(project1) + end + end + end + + let(:finder) { finder_class.new } + + subject { finder.execute(project1) } + + it { is_expected.to eq [package1]} + end + describe '#packages_visible_to_user' do using RSpec::Parameterized::TableSyntax @@ -12,6 +36,7 @@ RSpec.describe ::Packages::FinderHelper do let_it_be_with_reload(:subgroup) { create(:group, parent: group) } let_it_be_with_reload(:project2) { create(:project, namespace: subgroup) } let_it_be(:package2) { create(:package, project: project2) } + let_it_be(:package3) { create(:package, :error, project: project2) } let(:finder_class) do Class.new do diff --git a/spec/finders/packages/composer/packages_finder_spec.rb b/spec/finders/packages/composer/packages_finder_spec.rb new file mode 100644 index 00000000000..d4328827de3 --- /dev/null +++ b/spec/finders/packages/composer/packages_finder_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe ::Packages::Composer::PackagesFinder do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + let(:params) { {} } + + describe '#execute' do + let_it_be(:composer_package) { create(:composer_package, project: project) } + let_it_be(:composer_package2) { create(:composer_package, project: project) } + let_it_be(:error_package) { create(:composer_package, :error, project: project) } + let_it_be(:composer_package3) { create(:composer_package) } + + subject { described_class.new(user, group, params).execute } + + before do + project.add_developer(user) + end + + it { is_expected.to match_array([composer_package, composer_package2]) } + end +end diff --git a/spec/finders/packages/conan/package_finder_spec.rb b/spec/finders/packages/conan/package_finder_spec.rb index 936a0e5ff4b..b26f8900090 100644 --- a/spec/finders/packages/conan/package_finder_spec.rb +++ b/spec/finders/packages/conan/package_finder_spec.rb @@ -11,7 +11,8 @@ RSpec.describe ::Packages::Conan::PackageFinder do subject { described_class.new(user, query: query).execute } - context 'packages that are not visible to user' do + context 'packages that are not installable' do + let!(:conan_package3) { create(:conan_package, :error, project: project) } let!(:non_visible_project) { create(:project, :private) } let!(:non_visible_conan_package) { create(:conan_package, project: non_visible_project) } let(:query) { "#{conan_package.name.split('/').first[0, 3]}%" } diff --git a/spec/finders/packages/generic/package_finder_spec.rb b/spec/finders/packages/generic/package_finder_spec.rb index ed34268e7a9..707f943b285 100644 --- a/spec/finders/packages/generic/package_finder_spec.rb +++ b/spec/finders/packages/generic/package_finder_spec.rb @@ -23,6 +23,13 @@ RSpec.describe ::Packages::Generic::PackageFinder do expect(found_package).to eq(package) end + it 'does not find uninstallable packages' do + error_package = create(:generic_package, :error, project: project) + + expect { finder.execute!(error_package.name, error_package.version) } + .to raise_error(ActiveRecord::RecordNotFound) + end + it 'raises ActiveRecord::RecordNotFound if package is not found' do expect { finder.execute!(package.name, '3.1.4') } .to raise_error(ActiveRecord::RecordNotFound) diff --git a/spec/finders/packages/go/package_finder_spec.rb b/spec/finders/packages/go/package_finder_spec.rb index b6fad1e7061..dbcb8255d47 100644 --- a/spec/finders/packages/go/package_finder_spec.rb +++ b/spec/finders/packages/go/package_finder_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Packages::Go::PackageFinder do let_it_be(:mod) { create :go_module, project: project } let_it_be(:version) { create :go_module_version, :tagged, mod: mod, name: 'v1.0.1' } - let_it_be(:package) { create :golang_package, project: project, name: mod.name, version: 'v1.0.1' } + let_it_be_with_refind(:package) { create :golang_package, project: project, name: mod.name, version: 'v1.0.1' } let(:finder) { described_class.new(project, mod_name, version_name) } @@ -54,6 +54,17 @@ RSpec.describe Packages::Go::PackageFinder do it { is_expected.to eq(package) } end + context 'with an uninstallable package' do + let(:mod_name) { mod.name } + let(:version_name) { version.name } + + before do + package.update_column(:status, 1) + end + + it { is_expected.to eq(nil) } + end + context 'with an invalid name' do let(:mod_name) { 'foo/bar' } let(:version_name) { 'baz' } diff --git a/spec/finders/packages/maven/package_finder_spec.rb b/spec/finders/packages/maven/package_finder_spec.rb index 9a6bb675248..d5f521ff895 100644 --- a/spec/finders/packages/maven/package_finder_spec.rb +++ b/spec/finders/packages/maven/package_finder_spec.rb @@ -6,7 +6,7 @@ RSpec.describe ::Packages::Maven::PackageFinder do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, namespace: group) } - let_it_be(:package) { create(:maven_package, project: project) } + let_it_be_with_refind(:package) { create(:maven_package, project: project) } let(:param_path) { nil } let(:param_project) { nil } @@ -36,6 +36,16 @@ RSpec.describe ::Packages::Maven::PackageFinder do expect { subject }.to raise_error(ActiveRecord::RecordNotFound) end end + + context 'with an uninstallable package' do + let(:param_path) { package.maven_metadatum.path } + + before do + package.update_column(:status, 1) + end + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end end context 'within the project' do diff --git a/spec/finders/packages/npm/package_finder_spec.rb b/spec/finders/packages/npm/package_finder_spec.rb index f021d800f31..a995f3b96c4 100644 --- a/spec/finders/packages/npm/package_finder_spec.rb +++ b/spec/finders/packages/npm/package_finder_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ::Packages::Npm::PackageFinder do let_it_be_with_reload(:project) { create(:project)} - let_it_be(:package) { create(:npm_package, project: project) } + let_it_be_with_refind(:package) { create(:npm_package, project: project) } let(:project) { package.project } let(:package_name) { package.name } @@ -46,6 +46,14 @@ RSpec.describe ::Packages::Npm::PackageFinder do it { is_expected.to be_empty } end + + context 'with an uninstallable package' do + before do + package.update_column(:status, 1) + end + + it { is_expected.to be_empty } + end end subject { finder.execute } diff --git a/spec/finders/packages/nuget/package_finder_spec.rb b/spec/finders/packages/nuget/package_finder_spec.rb index 10b5f6c8ec2..59cca2d06dc 100644 --- a/spec/finders/packages/nuget/package_finder_spec.rb +++ b/spec/finders/packages/nuget/package_finder_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Packages::Nuget::PackageFinder do let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:project) { create(:project, namespace: subgroup) } - let_it_be(:package1) { create(:nuget_package, project: project) } + let_it_be_with_refind(:package1) { create(:nuget_package, project: project) } let_it_be(:package2) { create(:nuget_package, name: package1.name, version: '2.0.0', project: project) } let_it_be(:package3) { create(:nuget_package, name: 'Another.Dummy.Package', project: project) } let_it_be(:other_package_1) { create(:nuget_package, name: package1.name, version: package1.version) } @@ -33,6 +33,14 @@ RSpec.describe Packages::Nuget::PackageFinder do it { is_expected.to be_empty } end + context 'with an uninstallable package' do + before do + package1.update_column(:status, 1) + end + + it { is_expected.to contain_exactly(package2) } + end + context 'with valid version' do let(:package_version) { '2.0.0' } diff --git a/spec/finders/packages/package_finder_spec.rb b/spec/finders/packages/package_finder_spec.rb index e8c7404a612..6a1d857dad4 100644 --- a/spec/finders/packages/package_finder_spec.rb +++ b/spec/finders/packages/package_finder_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe ::Packages::PackageFinder do let_it_be(:project) { create(:project) } - let_it_be(:maven_package) { create(:maven_package, project: project) } + let_it_be_with_refind(:maven_package) { create(:maven_package, project: project) } describe '#execute' do let(:package_id) { maven_package.id } @@ -13,6 +13,16 @@ RSpec.describe ::Packages::PackageFinder do it { is_expected.to eq(maven_package) } + context 'with non-displayable package' do + before do + maven_package.update_column(:status, 1) + end + + it 'raises an exception' do + expect { subject }.to raise_exception(ActiveRecord::RecordNotFound) + end + end + context 'processing packages' do let_it_be(:nuget_package) { create(:nuget_package, project: project, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } let(:package_id) { nuget_package.id } diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js new file mode 100644 index 00000000000..8a4f07c4d88 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/cards/first_pipeline_card_spec.js @@ -0,0 +1,47 @@ +import { getByRole } from '@testing-library/dom'; +import { mount } from '@vue/test-utils'; +import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue'; +import PipelineVisualReference from '~/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue'; + +describe('First pipeline card', () => { + let wrapper; + + const defaultProvide = { + ciExamplesHelpPagePath: '/pipelines/examples', + runnerHelpPagePath: '/help/runners', + }; + + const createComponent = () => { + wrapper = mount(FirstPipelineCard, { + provide: { + ...defaultProvide, + }, + }); + }; + + const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href; + const findPipelinesLink = () => getLinkByName(/examples and templates/i); + const findRunnersLink = () => getLinkByName(/make sure your instance has runners available/i); + const findVisualReference = () => wrapper.findComponent(PipelineVisualReference); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the title', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); + }); + + it('renders the content', () => { + expect(findVisualReference().exists()).toBe(true); + }); + + it('renders the links', () => { + expect(findRunnersLink()).toContain(defaultProvide.runnerHelpPagePath); + expect(findPipelinesLink()).toContain(defaultProvide.ciExamplesHelpPagePath); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js new file mode 100644 index 00000000000..c592e959068 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/cards/getting_started_card_spec.js @@ -0,0 +1,26 @@ +import { shallowMount } from '@vue/test-utils'; +import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue'; + +describe('Getting started card', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(GettingStartedCard); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the title', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); + }); + + it('renders the content', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js new file mode 100644 index 00000000000..3c8821d05a7 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/cards/pipeline_config_reference_card_spec.js @@ -0,0 +1,51 @@ +import { getByRole } from '@testing-library/dom'; +import { mount } from '@vue/test-utils'; +import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue'; + +describe('Pipeline config reference card', () => { + let wrapper; + + const defaultProvide = { + ciExamplesHelpPagePath: 'help/ci/examples/', + ciHelpPagePath: 'help/ci/introduction', + needsHelpPagePath: 'help/ci/yaml#needs', + ymlHelpPagePath: 'help/ci/yaml', + }; + + const createComponent = () => { + wrapper = mount(PipelineConfigReferenceCard, { + provide: { + ...defaultProvide, + }, + }); + }; + + const getLinkByName = (name) => getByRole(wrapper.element, 'link', { name }).href; + const findCiExamplesLink = () => getLinkByName(/CI\/CD examples and templates/i); + const findCiIntroLink = () => getLinkByName(/GitLab CI\/CD concepts/i); + const findNeedsLink = () => getLinkByName(/Needs keyword/i); + const findYmlSyntaxLink = () => getLinkByName(/.gitlab-ci.yml syntax reference/i); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the title', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); + }); + + it('renders the content', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph); + }); + + it('renders the links', () => { + expect(findCiExamplesLink()).toContain(defaultProvide.ciExamplesHelpPagePath); + expect(findCiIntroLink()).toContain(defaultProvide.ciHelpPagePath); + expect(findNeedsLink()).toContain(defaultProvide.needsHelpPagePath); + expect(findYmlSyntaxLink()).toContain(defaultProvide.ymlHelpPagePath); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js b/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js new file mode 100644 index 00000000000..bebd2484c1d --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/cards/visualize_and_lint_card_spec.js @@ -0,0 +1,26 @@ +import { shallowMount } from '@vue/test-utils'; +import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue'; + +describe('Visual and Lint card', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(VisualizeAndLintCard); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the title', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.title); + }); + + it('renders the content', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.firstParagraph); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js index 587373c99b4..fea7d90de52 100644 --- a/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js +++ b/spec/frontend/pipeline_editor/components/drawer/pipeline_editor_drawer_spec.js @@ -1,4 +1,9 @@ +import { GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import FirstPipelineCard from '~/pipeline_editor/components/drawer/cards/first_pipeline_card.vue'; +import GettingStartedCard from '~/pipeline_editor/components/drawer/cards/getting_started_card.vue'; +import PipelineConfigReferenceCard from '~/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue'; +import VisualizeAndLintCard from '~/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue'; import PipelineEditorDrawer from '~/pipeline_editor/components/drawer/pipeline_editor_drawer.vue'; describe('Pipeline editor drawer', () => { @@ -8,7 +13,12 @@ describe('Pipeline editor drawer', () => { wrapper = shallowMount(PipelineEditorDrawer); }; - const findToggleBtn = () => wrapper.find('[data-testid="toggleBtn"]'); + const findFirstPipelineCard = () => wrapper.findComponent(FirstPipelineCard); + const findGettingStartedCard = () => wrapper.findComponent(GettingStartedCard); + const findPipelineConfigReferenceCard = () => wrapper.findComponent(PipelineConfigReferenceCard); + const findToggleBtn = () => wrapper.findComponent(GlButton); + const findVisualizeAndLintCard = () => wrapper.findComponent(VisualizeAndLintCard); + const findArrowIcon = () => wrapper.find('[data-testid="toggle-icon"]'); const findCollapseText = () => wrapper.find('[data-testid="collapse-text"]'); const findDrawerContent = () => wrapper.find('[data-testid="drawer-content"]'); @@ -24,7 +34,7 @@ describe('Pipeline editor drawer', () => { createComponent(); }); - it('show the left facing arrow icon', () => { + it('shows the left facing arrow icon', () => { expect(findArrowIcon().props('name')).toBe('chevron-double-lg-left'); }); @@ -51,7 +61,7 @@ describe('Pipeline editor drawer', () => { await clickToggleBtn(); }); - it('show the right facing arrow icon', () => { + it('shows the right facing arrow icon', () => { expect(findArrowIcon().props('name')).toBe('chevron-double-lg-right'); }); @@ -59,10 +69,17 @@ describe('Pipeline editor drawer', () => { expect(findCollapseText().exists()).toBe(true); }); - it('show the drawer content', () => { + it('shows the drawer content', () => { expect(findDrawerContent().exists()).toBe(true); }); + it('shows all the introduction cards', () => { + expect(findFirstPipelineCard().exists()).toBe(true); + expect(findGettingStartedCard().exists()).toBe(true); + expect(findPipelineConfigReferenceCard().exists()).toBe(true); + expect(findVisualizeAndLintCard().exists()).toBe(true); + }); + it('can close the drawer by clicking on the toggle button', async () => { expect(findDrawerContent().exists()).toBe(true); diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js b/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js new file mode 100644 index 00000000000..edd2b45569a --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/ui/demo_job_pill_spec.js @@ -0,0 +1,27 @@ +import { shallowMount } from '@vue/test-utils'; +import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue'; + +describe('Demo job pill', () => { + let wrapper; + const jobName = 'my-build-job'; + + const createComponent = () => { + wrapper = shallowMount(DemoJobPill, { + propsData: { + jobName, + }, + }); + }; + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the jobName', () => { + expect(wrapper.text()).toContain(jobName); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js b/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js new file mode 100644 index 00000000000..e4834544484 --- /dev/null +++ b/spec/frontend/pipeline_editor/components/drawer/ui/pipeline_visual_reference_spec.js @@ -0,0 +1,31 @@ +import { shallowMount } from '@vue/test-utils'; +import DemoJobPill from '~/pipeline_editor/components/drawer/ui/demo_job_pill.vue'; +import PipelineVisualReference from '~/pipeline_editor/components/drawer/ui/pipeline_visual_reference.vue'; + +describe('Demo job pill', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(PipelineVisualReference); + }; + + const findAllDemoJobPills = () => wrapper.findAllComponents(DemoJobPill); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders all stage names', () => { + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.build); + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.test); + expect(wrapper.text()).toContain(wrapper.vm.$options.i18n.stageNames.deploy); + }); + + it('renders all job pills', () => { + expect(findAllDemoJobPills()).toHaveLength(4); + }); +}); diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js index 4fe44a3307a..632f506f4ae 100644 --- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js @@ -1,7 +1,10 @@ import { GlButton, GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; import { useFakeDate } from 'helpers/fake_date'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import waitForPromises from 'helpers/wait_for_promises'; import component from '~/registry/explorer/components/details_page/details_header.vue'; import { UNSCHEDULED_STATUS, @@ -16,15 +19,18 @@ import { ROOT_IMAGE_TEXT, ROOT_IMAGE_TOOLTIP, } from '~/registry/explorer/constants'; +import getContainerRepositoryTagCountQuery from '~/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { imageTagsCountMock } from '../../mock_data'; describe('Details Header', () => { let wrapper; + let apolloProvider; + let localVue; const defaultImage = { name: 'foo', updatedAt: '2020-11-03T13:29:21Z', - tagsCount: 10, canDelete: true, project: { visibility: 'public', @@ -51,12 +57,31 @@ describe('Details Header', () => { await wrapper.vm.$nextTick(); }; - const mountComponent = (propsData = { image: defaultImage }) => { + const mountComponent = ({ + propsData = { image: defaultImage }, + resolver = jest.fn().mockResolvedValue(imageTagsCountMock()), + $apollo = undefined, + } = {}) => { + const mocks = {}; + + if ($apollo) { + mocks.$apollo = $apollo; + } else { + localVue = createLocalVue(); + localVue.use(VueApollo); + + const requestHandlers = [[getContainerRepositoryTagCountQuery, resolver]]; + apolloProvider = createMockApollo(requestHandlers); + } + wrapper = shallowMount(component, { + localVue, + apolloProvider, propsData, directives: { GlTooltip: createMockDirective(), }, + mocks, stubs: { TitleArea, }, @@ -64,41 +89,48 @@ describe('Details Header', () => { }; afterEach(() => { + // if we want to mix createMockApollo and manual mocks we need to reset everything wrapper.destroy(); + apolloProvider = undefined; + localVue = undefined; wrapper = null; }); + describe('image name', () => { describe('missing image name', () => { - it('root image ', () => { - mountComponent({ image: { ...defaultImage, name: '' } }); + beforeEach(() => { + mountComponent({ propsData: { image: { ...defaultImage, name: '' } } }); + + return waitForPromises(); + }); + it('root image ', () => { expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT); }); it('has an icon', () => { - mountComponent({ image: { ...defaultImage, name: '' } }); - expect(findInfoIcon().exists()).toBe(true); expect(findInfoIcon().props('name')).toBe('information-o'); }); it('has a tooltip', () => { - mountComponent({ image: { ...defaultImage, name: '' } }); - const tooltip = getBinding(findInfoIcon().element, 'gl-tooltip'); expect(tooltip.value).toBe(ROOT_IMAGE_TOOLTIP); }); }); describe('with image name present', () => { - it('shows image.name ', () => { + beforeEach(() => { mountComponent(); + + return waitForPromises(); + }); + + it('shows image.name ', () => { expect(findTitle().text()).toContain('foo'); }); it('has no icon', () => { - mountComponent(); - expect(findInfoIcon().exists()).toBe(false); }); }); @@ -111,12 +143,6 @@ describe('Details Header', () => { expect(findDeleteButton().exists()).toBe(true); }); - it('is hidden while loading', () => { - mountComponent({ image: defaultImage, metadataLoading: true }); - - expect(findDeleteButton().exists()).toBe(false); - }); - it('has the correct text', () => { mountComponent(); @@ -149,7 +175,7 @@ describe('Details Header', () => { `( 'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled', ({ canDelete, disabled, isDisabled }) => { - mountComponent({ image: { ...defaultImage, canDelete }, disabled }); + mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } }); expect(findDeleteButton().props('disabled')).toBe(isDisabled); }, @@ -158,15 +184,32 @@ describe('Details Header', () => { describe('metadata items', () => { describe('tags count', () => { + it('displays "-- tags" while loading', async () => { + // here we are forced to mock apollo because `waitForMetadataItems` waits + // for two ticks, de facto allowing the promise to resolve, so there is + // no way to catch the component as both rendered and in loading state + mountComponent({ $apollo: { queries: { containerRepository: { loading: true } } } }); + + await waitForMetadataItems(); + + expect(findTagsCount().props('text')).toBe('-- tags'); + }); + it('when there is more than one tag has the correct text', async () => { mountComponent(); + + await waitForPromises(); await waitForMetadataItems(); - expect(findTagsCount().props('text')).toBe('10 tags'); + expect(findTagsCount().props('text')).toBe('13 tags'); }); it('when there is one tag has the correct text', async () => { - mountComponent({ image: { ...defaultImage, tagsCount: 1 } }); + mountComponent({ + resolver: jest.fn().mockResolvedValue(imageTagsCountMock({ tagsCount: 1 })), + }); + + await waitForPromises(); await waitForMetadataItems(); expect(findTagsCount().props('text')).toBe('1 tag'); @@ -208,11 +251,13 @@ describe('Details Header', () => { 'when the status is $status the text is $text and the tooltip is $tooltip', async ({ status, text, tooltip }) => { mountComponent({ - image: { - ...defaultImage, - expirationPolicyCleanupStatus: status, - project: { - containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' }, + propsData: { + image: { + ...defaultImage, + expirationPolicyCleanupStatus: status, + project: { + containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' }, + }, }, }, }); @@ -242,7 +287,9 @@ describe('Details Header', () => { expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye'); }); it('shows an eye slashed when the project is not public', async () => { - mountComponent({ image: { ...defaultImage, project: { visibility: 'private' } } }); + mountComponent({ + propsData: { image: { ...defaultImage, project: { visibility: 'private' } } }, + }); await waitForMetadataItems(); expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash'); diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js index 7d544b71466..fe258dcd4e8 100644 --- a/spec/frontend/registry/explorer/mock_data.js +++ b/spec/frontend/registry/explorer/mock_data.js @@ -113,7 +113,6 @@ export const containerRepositoryMock = { canDelete: true, createdAt: '2020-11-03T13:29:21Z', updatedAt: '2020-11-03T13:29:21Z', - tagsCount: 13, expirationPolicyStartedAt: null, expirationPolicyCleanupStatus: 'UNSCHEDULED', project: { @@ -175,6 +174,16 @@ export const imageTagsMock = (nodes = tagsMock) => ({ }, }); +export const imageTagsCountMock = (override) => ({ + data: { + containerRepository: { + id: containerRepositoryMock.id, + tagsCount: 13, + ...override, + }, + }, +}); + export const graphQLImageDetailsMock = (override) => ({ data: { containerRepository: { diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index eb01fb1a7e6..022f6e71fe6 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -292,7 +292,6 @@ describe('Details Page', () => { await waitForApolloRequestRender(); expect(findDetailsHeader().props()).toMatchObject({ - metadataLoading: false, image: { name: containerRepositoryMock.name, project: { diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js index a5d91468ef2..eb6e3711e2e 100644 --- a/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js +++ b/spec/frontend/vue_mr_widget/deployment/deployment_view_button_spec.js @@ -1,4 +1,5 @@ -import { mount } from '@vue/test-utils'; +import { GlDropdown, GlLink } from '@gitlab/ui'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import DeploymentViewButton from '~/vue_merge_request_widget/components/deployment/deployment_view_button.vue'; import ReviewAppLink from '~/vue_merge_request_widget/components/review_app_link.vue'; import { deploymentMockData } from './deployment_mock_data'; @@ -11,14 +12,14 @@ const appButtonText = { describe('Deployment View App button', () => { let wrapper; - const factory = (options = {}) => { - wrapper = mount(DeploymentViewButton, { + const createComponent = (options = {}) => { + wrapper = mountExtended(DeploymentViewButton, { ...options, }); }; beforeEach(() => { - factory({ + createComponent({ propsData: { deployment: deploymentMockData, appButtonText, @@ -30,15 +31,21 @@ describe('Deployment View App button', () => { wrapper.destroy(); }); + const findReviewAppLink = () => wrapper.findComponent(ReviewAppLink); + const findMrWigdetDeploymentDropdown = () => wrapper.findComponent(GlDropdown); + const findMrWigdetDeploymentDropdownIcon = () => + wrapper.findByTestId('mr-wigdet-deployment-dropdown-icon'); + const findDeployUrlMenuItems = () => wrapper.findAllComponents(GlLink); + describe('text', () => { it('renders text as passed', () => { - expect(wrapper.find(ReviewAppLink).text()).toContain(appButtonText.text); + expect(findReviewAppLink().props().display.text).toBe(appButtonText.text); }); }); describe('without changes', () => { beforeEach(() => { - factory({ + createComponent({ propsData: { deployment: { ...deploymentMockData, changes: null }, appButtonText, @@ -47,13 +54,13 @@ describe('Deployment View App button', () => { }); it('renders the link to the review app without dropdown', () => { - expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false); + expect(findMrWigdetDeploymentDropdown().exists()).toBe(false); }); }); describe('with a single change', () => { beforeEach(() => { - factory({ + createComponent({ propsData: { deployment: { ...deploymentMockData, changes: [deploymentMockData.changes[0]] }, appButtonText, @@ -62,21 +69,20 @@ describe('Deployment View App button', () => { }); it('renders the link to the review app without dropdown', () => { - expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(false); + expect(findMrWigdetDeploymentDropdown().exists()).toBe(false); + expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(false); }); it('renders the link to the review app linked to to the first change', () => { const expectedUrl = deploymentMockData.changes[0].external_url; - const deployUrl = wrapper.find('.js-deploy-url'); - expect(deployUrl.attributes().href).not.toBeNull(); - expect(deployUrl.attributes().href).toEqual(expectedUrl); + expect(findReviewAppLink().attributes('href')).toBe(expectedUrl); }); }); describe('with multiple changes', () => { beforeEach(() => { - factory({ + createComponent({ propsData: { deployment: deploymentMockData, appButtonText, @@ -85,18 +91,18 @@ describe('Deployment View App button', () => { }); it('renders the link to the review app with dropdown', () => { - expect(wrapper.find('.js-mr-wigdet-deployment-dropdown').exists()).toBe(true); + expect(findMrWigdetDeploymentDropdown().exists()).toBe(true); + expect(findMrWigdetDeploymentDropdownIcon().exists()).toBe(true); }); it('renders all the links to the review apps', () => { - const allUrls = wrapper.findAll('.js-deploy-url-menu-item').wrappers; + const allUrls = findDeployUrlMenuItems().wrappers; const expectedUrls = deploymentMockData.changes.map((change) => change.external_url); expectedUrls.forEach((expectedUrl, idx) => { const deployUrl = allUrls[idx]; - expect(deployUrl.attributes().href).not.toBeNull(); - expect(deployUrl.attributes().href).toEqual(expectedUrl); + expect(deployUrl.attributes('href')).toBe(expectedUrl); }); }); }); diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb index e276796f3ec..aacfc3b91c6 100644 --- a/spec/helpers/ci/pipeline_editor_helper_spec.rb +++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb @@ -40,16 +40,21 @@ RSpec.describe Ci::PipelineEditorHelper do it 'returns pipeline editor data' do expect(pipeline_editor_data).to eq({ "ci-config-path": project.ci_config_path_or_default, + "ci-examples-help-page-path" => help_page_path('ci/examples/README'), + "ci-help-page-path" => help_page_path('ci/README'), "commit-sha" => project.commit.sha, "default-branch" => project.default_branch, "empty-state-illustration-path" => 'foo', "initial-branch-name": nil, "lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'), + "needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'), "new-merge-request-path" => '/mock/project/-/merge_requests/new', "pipeline_etag" => graphql_etag_pipeline_sha_path(project.commit.sha), + "pipeline-page-path" => project_pipelines_path(project), "project-path" => project.path, "project-full-path" => project.full_path, "project-namespace" => project.namespace.full_path, + "runner-help-page-path" => help_page_path('ci/runners/README'), "yml-help-page-path" => help_page_path('ci/yaml/README') }) end @@ -61,16 +66,21 @@ RSpec.describe Ci::PipelineEditorHelper do it 'returns pipeline editor data' do expect(pipeline_editor_data).to eq({ "ci-config-path": project.ci_config_path_or_default, + "ci-examples-help-page-path" => help_page_path('ci/examples/README'), + "ci-help-page-path" => help_page_path('ci/README'), "commit-sha" => '', "default-branch" => project.default_branch, "empty-state-illustration-path" => 'foo', "initial-branch-name": nil, "lint-help-page-path" => help_page_path('ci/lint', anchor: 'validate-basic-logic-and-syntax'), + "needs-help-page-path" => help_page_path('ci/yaml/README', anchor: 'needs'), "new-merge-request-path" => '/mock/project/-/merge_requests/new', "pipeline_etag" => '', + "pipeline-page-path" => project_pipelines_path(project), "project-path" => project.path, "project-full-path" => project.full_path, "project-namespace" => project.namespace.full_path, + "runner-help-page-path" => help_page_path('ci/runners/README'), "yml-help-page-path" => help_page_path('ci/yaml/README') }) end diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb index 95b78ceb5d5..60ff15a88e0 100644 --- a/spec/lib/banzai/cross_project_reference_spec.rb +++ b/spec/lib/banzai/cross_project_reference_spec.rb @@ -4,10 +4,12 @@ require 'spec_helper' RSpec.describe Banzai::CrossProjectReference do let(:including_class) { Class.new.include(described_class).new } + let(:reference_cache) { Banzai::Filter::References::ReferenceCache.new(including_class, {})} before do allow(including_class).to receive(:context).and_return({}) allow(including_class).to receive(:parent_from_ref).and_call_original + allow(including_class).to receive(:reference_cache).and_return(reference_cache) end describe '#parent_from_ref' do @@ -47,5 +49,18 @@ RSpec.describe Banzai::CrossProjectReference do expect(including_class.parent_from_ref('cross/reference')).to eq project2 end end + + context 'when reference cache is loaded' do + let(:project2) { double('referenced project') } + + before do + allow(reference_cache).to receive(:cache_loaded?).and_return(true) + allow(reference_cache).to receive(:parent_per_reference).and_return({ 'cross/reference' => project2 }) + end + + it 'pulls from the reference cache' do + expect(including_class.parent_from_ref('cross/reference')).to eq project2 + end + end end end diff --git a/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb index d10b52bf7d0..3cb3ebc42a6 100644 --- a/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/abstract_reference_filter_spec.rb @@ -8,18 +8,6 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do let(:doc) { Nokogiri::HTML.fragment('') } let(:filter) { described_class.new(doc, project: project) } - describe '#references_per_parent' do - let(:doc) { Nokogiri::HTML.fragment("#1 #{project.full_path}#2 #2") } - - it 'returns a Hash containing references grouped per parent paths' do - expect(described_class).to receive(:object_class).exactly(6).times.and_return(Issue) - - refs = filter.references_per_parent - - expect(refs).to match(a_hash_including(project.full_path => contain_exactly(1, 2))) - end - end - describe '#data_attributes_for' do let_it_be(:issue) { create(:issue, project: project) } @@ -32,74 +20,6 @@ RSpec.describe Banzai::Filter::References::AbstractReferenceFilter do end end - describe '#parent_per_reference' do - it 'returns a Hash containing projects grouped per parent paths' do - expect(filter).to receive(:references_per_parent) - .and_return({ project.full_path => Set.new([1]) }) - - expect(filter.parent_per_reference) - .to eq({ project.full_path => project }) - end - end - - describe '#find_for_paths' do - context 'with RequestStore disabled' do - it 'returns a list of Projects for a list of paths' do - expect(filter.find_for_paths([project.full_path])) - .to eq([project]) - end - - it "return an empty array for paths that don't exist" do - expect(filter.find_for_paths(['nonexistent/project'])) - .to eq([]) - end - end - - context 'with RequestStore enabled', :request_store do - it 'returns a list of Projects for a list of paths' do - expect(filter.find_for_paths([project.full_path])) - .to eq([project]) - end - - context 'when no project with that path exists' do - it 'returns no value' do - expect(filter.find_for_paths(['nonexistent/project'])) - .to eq([]) - end - - it 'adds the ref to the project refs cache' do - project_refs_cache = {} - allow(filter).to receive(:refs_cache).and_return(project_refs_cache) - - filter.find_for_paths(['nonexistent/project']) - - expect(project_refs_cache).to eq({ 'nonexistent/project' => nil }) - end - - context 'when the project refs cache includes nil values' do - before do - # adds { 'nonexistent/project' => nil } to cache - filter.from_ref_cached('nonexistent/project') - end - - it "return an empty array for paths that don't exist" do - expect(filter.find_for_paths(['nonexistent/project'])) - .to eq([]) - end - end - end - end - end - - describe '#current_parent_path' do - it 'returns the path of the current parent' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter.current_parent_path).to eq(project.full_path) - end - end - context 'abstract methods' do describe '#find_object' do it 'raises NotImplementedError' do diff --git a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb index 0e1cb1ade74..88c2494b243 100644 --- a/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/issue_reference_filter_spec.rb @@ -470,24 +470,6 @@ RSpec.describe Banzai::Filter::References::IssueReferenceFilter do end end - describe '#records_per_parent' do - context 'using an internal issue tracker' do - it 'returns a Hash containing the issues per project' do - doc = Nokogiri::HTML.fragment('') - filter = described_class.new(doc, project: project) - - expect(filter).to receive(:parent_per_reference) - .and_return({ project.full_path => project }) - - expect(filter).to receive(:references_per_parent) - .and_return({ project.full_path => Set.new([issue.iid]) }) - - expect(filter.records_per_parent) - .to eq({ project => { issue.iid => issue } }) - end - end - end - describe '.references_in' do let(:merge_request) { create(:merge_request) } diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb new file mode 100644 index 00000000000..2e37e34bba5 --- /dev/null +++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::References::ReferenceCache do + let_it_be(:project) { create(:project) } + let_it_be(:project2) { create(:project) } + let_it_be(:issue1) { create(:issue, project: project) } + let_it_be(:issue2) { create(:issue, project: project) } + let_it_be(:issue3) { create(:issue, project: project2) } + let_it_be(:doc) { Nokogiri::HTML.fragment("#{issue1.to_reference} #{issue2.to_reference} #{issue3.to_reference(full: true)}") } + + let(:filter_class) { Banzai::Filter::References::IssueReferenceFilter } + let(:filter) { filter_class.new(doc, project: project) } + let(:cache) { described_class.new(filter, { project: project }) } + + describe '#load_references_per_parent' do + it 'loads references grouped per parent paths' do + cache.load_references_per_parent(filter.nodes) + + expect(cache.references_per_parent).to eq({ project.full_path => [issue1.iid, issue2.iid].to_set, + project2.full_path => [issue3.iid].to_set }) + end + end + + describe '#load_parent_per_reference' do + it 'returns a Hash containing projects grouped per parent paths' do + cache.load_references_per_parent(filter.nodes) + cache.load_parent_per_reference + + expect(cache.parent_per_reference).to match({ project.full_path => project, project2.full_path => project2 }) + end + end + + describe '#load_records_per_parent' do + it 'returns a Hash containing projects grouped per parent paths' do + cache.load_references_per_parent(filter.nodes) + cache.load_parent_per_reference + cache.load_records_per_parent + + expect(cache.records_per_parent).to match({ project => { issue1.iid => issue1, issue2.iid => issue2 }, + project2 => { issue3.iid => issue3 } }) + end + end + + describe '#initialize_reference_cache' do + it 'does not have an N+1 query problem with cross projects' do + doc_single = Nokogiri::HTML.fragment("#1") + filter_single = filter_class.new(doc_single, project: project) + cache_single = described_class.new(filter_single, { project: project }) + + control_count = ActiveRecord::QueryRecorder.new do + cache_single.load_references_per_parent(filter_single.nodes) + cache_single.load_parent_per_reference + cache_single.load_records_per_parent + end.count + + # Since this is an issue filter that is not batching issue queries + # across projects, we have to account for that. + # 1 for both projects, 1 for issues in each project == 3 + max_count = control_count + 1 + + expect do + cache.load_references_per_parent(filter.nodes) + cache.load_parent_per_reference + cache.load_records_per_parent + end.not_to exceed_query_limit(max_count) + end + end + + describe '#find_for_paths' do + context 'with RequestStore disabled' do + it 'returns a list of Projects for a list of paths' do + expect(cache.find_for_paths([project.full_path])) + .to eq([project]) + end + + it 'return an empty array for paths that do not exist' do + expect(cache.find_for_paths(['nonexistent/project'])) + .to eq([]) + end + end + + context 'with RequestStore enabled', :request_store do + it 'returns a list of Projects for a list of paths' do + expect(cache.find_for_paths([project.full_path])) + .to eq([project]) + end + + context 'when no project with that path exists' do + it 'returns no value' do + expect(cache.find_for_paths(['nonexistent/project'])) + .to eq([]) + end + + it 'adds the ref to the project refs cache' do + project_refs_cache = {} + allow(cache).to receive(:refs_cache).and_return(project_refs_cache) + + cache.find_for_paths(['nonexistent/project']) + + expect(project_refs_cache).to eq({ 'nonexistent/project' => nil }) + end + end + end + end + + describe '#current_parent_path' do + it 'returns the path of the current parent' do + expect(cache.current_parent_path).to eq project.full_path + end + end + + describe '#current_project_namespace_path' do + it 'returns the path of the current project namespace' do + expect(cache.current_project_namespace_path).to eq project.namespace.full_path + end + end + + describe '#full_project_path' do + it 'returns current parent path when no ref specified' do + expect(cache.full_project_path('something', nil)).to eq cache.current_parent_path + end + + it 'returns combined namespace and project ref' do + expect(cache.full_project_path('something', 'cool')).to eq 'something/cool' + end + + it 'returns uses default namespace and project ref when namespace nil' do + expect(cache.full_project_path(nil, 'cool')).to eq "#{project.namespace.full_path}/cool" + end + end + + describe '#full_group_path' do + it 'returns current parent path when no group ref specified' do + expect(cache.full_group_path(nil)).to eq cache.current_parent_path + end + + it 'returns group ref' do + expect(cache.full_group_path('cool_group')).to eq 'cool_group' + end + end +end diff --git a/spec/lib/bulk_imports/clients/http_spec.rb b/spec/lib/bulk_imports/clients/http_spec.rb index 2d841b7fac2..213fa23675e 100644 --- a/spec/lib/bulk_imports/clients/http_spec.rb +++ b/spec/lib/bulk_imports/clients/http_spec.rb @@ -8,66 +8,23 @@ RSpec.describe BulkImports::Clients::Http do let(:uri) { 'http://gitlab.example' } let(:token) { 'token' } let(:resource) { 'resource' } + let(:response_double) { double(code: 200, success?: true, parsed_response: {}) } subject { described_class.new(uri: uri, token: token) } - describe '#get' do - let(:response_double) { double(code: 200, success?: true, parsed_response: {}) } - - shared_examples 'performs network request' do - it 'performs network request' do - expect(Gitlab::HTTP).to receive(:get).with(*expected_args).and_return(response_double) - - subject.get(resource) - end - end - - describe 'request query' do - include_examples 'performs network request' do - let(:expected_args) do - [ - anything, - hash_including( - query: { - page: described_class::DEFAULT_PAGE, - per_page: described_class::DEFAULT_PER_PAGE - } - ) - ] - end - end - end - - describe 'request headers' do - include_examples 'performs network request' do - let(:expected_args) do - [ - anything, - hash_including( - headers: { - 'Content-Type' => 'application/json', - 'Authorization' => "Bearer #{token}" - } - ) - ] - end - end - end + shared_examples 'performs network request' do + it 'performs network request' do + expect(Gitlab::HTTP).to receive(method).with(*expected_args).and_return(response_double) - describe 'request uri' do - include_examples 'performs network request' do - let(:expected_args) do - ['http://gitlab.example:80/api/v4/resource', anything] - end - end + subject.public_send(method, resource) end context 'error handling' do context 'when error occurred' do it 'raises ConnectionError' do - allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED) + allow(Gitlab::HTTP).to receive(method).and_raise(Errno::ECONNREFUSED) - expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError) + expect { subject.public_send(method, resource) }.to raise_exception(described_class::ConnectionError) end end @@ -75,12 +32,34 @@ RSpec.describe BulkImports::Clients::Http do it 'raises ConnectionError' do response_double = double(code: 503, success?: false) - allow(Gitlab::HTTP).to receive(:get).and_return(response_double) + allow(Gitlab::HTTP).to receive(method).and_return(response_double) - expect { subject.get(resource) }.to raise_exception(described_class::ConnectionError) + expect { subject.public_send(method, resource) }.to raise_exception(described_class::ConnectionError) end end end + end + + describe '#get' do + let(:method) { :get } + + include_examples 'performs network request' do + let(:expected_args) do + [ + 'http://gitlab.example:80/api/v4/resource', + hash_including( + query: { + page: described_class::DEFAULT_PAGE, + per_page: described_class::DEFAULT_PER_PAGE + }, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{token}" + } + ) + ] + end + end describe '#each_page' do let(:objects1) { [{ object: 1 }, { object: 2 }] } @@ -129,4 +108,23 @@ RSpec.describe BulkImports::Clients::Http do end end end + + describe '#post' do + let(:method) { :post } + + include_examples 'performs network request' do + let(:expected_args) do + [ + 'http://gitlab.example:80/api/v4/resource', + hash_including( + body: {}, + headers: { + 'Content-Type' => 'application/json', + 'Authorization' => "Bearer #{token}" + } + ) + ] + end + end + end end diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb new file mode 100644 index 00000000000..35928deff82 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsChildren, :migration, schema: 20210506065000 do + let(:namespaces_table) { table(:namespaces) } + + let!(:user_namespace) { namespaces_table.create!(id: 1, name: 'user', path: 'user', type: nil) } + let!(:root_group) { namespaces_table.create!(id: 2, name: 'group', path: 'group', type: 'Group', parent_id: nil) } + let!(:sub_group) { namespaces_table.create!(id: 3, name: 'subgroup', path: 'subgroup', type: 'Group', parent_id: 2) } + + describe '#perform' do + it 'backfills traversal_ids for child namespaces' do + described_class.new.perform(1, 3, 5) + + expect(user_namespace.reload.traversal_ids).to eq([]) + expect(root_group.reload.traversal_ids).to eq([]) + expect(sub_group.reload.traversal_ids).to eq([root_group.id, sub_group.id]) + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb new file mode 100644 index 00000000000..96e43275972 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceTraversalIdsRoots, :migration, schema: 20210506065000 do + let(:namespaces_table) { table(:namespaces) } + + let!(:user_namespace) { namespaces_table.create!(id: 1, name: 'user', path: 'user', type: nil) } + let!(:root_group) { namespaces_table.create!(id: 2, name: 'group', path: 'group', type: 'Group', parent_id: nil) } + let!(:sub_group) { namespaces_table.create!(id: 3, name: 'subgroup', path: 'subgroup', type: 'Group', parent_id: 2) } + + describe '#perform' do + it 'backfills traversal_ids for root namespaces' do + described_class.new.perform(1, 3, 5) + + expect(user_namespace.reload.traversal_ids).to eq([user_namespace.id]) + expect(root_group.reload.traversal_ids).to eq([root_group.id]) + expect(sub_group.reload.traversal_ids).to eq([]) + end + end +end diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb index c6fbd263072..d2d287d8e24 100644 --- a/spec/models/board_group_recent_visit_spec.rb +++ b/spec/models/board_group_recent_visit_spec.rb @@ -3,9 +3,8 @@ require 'spec_helper' RSpec.describe BoardGroupRecentVisit do - let(:user) { create(:user) } - let(:group) { create(:group) } - let(:board) { create(:board, group: group) } + let_it_be(:board_parent) { create(:group) } + let_it_be(:board) { create(:board, group: board_parent) } describe 'relationships' do it { is_expected.to belong_to(:user) } @@ -19,56 +18,9 @@ RSpec.describe BoardGroupRecentVisit do it { is_expected.to validate_presence_of(:board) } end - describe '#visited' do - it 'creates a visit if one does not exists' do - expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) - end - - shared_examples 'was visited previously' do - let!(:visit) { create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago } - - it 'updates the timestamp' do - freeze_time do - described_class.visited!(user, board) - - expect(described_class.count).to eq 1 - expect(described_class.first.updated_at).to be_like_time(Time.zone.now) - end - end - end - - it_behaves_like 'was visited previously' - - context 'when we try to create a visit that is not unique' do - before do - expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') - expect(described_class).to receive(:find_or_create_by).and_return(visit) - end - - it_behaves_like 'was visited previously' - end - end - - describe '#latest' do - def create_visit(time) - create :board_group_recent_visit, group: group, user: user, updated_at: time - end - - it 'returns the most recent visited' do - create_visit(7.days.ago) - create_visit(5.days.ago) - recent = create_visit(1.day.ago) - - expect(described_class.latest(user, group)).to eq recent - end - - it 'returns last 3 visited boards' do - create_visit(7.days.ago) - visit1 = create_visit(3.days.ago) - visit2 = create_visit(2.days.ago) - visit3 = create_visit(5.days.ago) - - expect(described_class.latest(user, group, count: 3)).to eq([visit2, visit1, visit3]) - end + it_behaves_like 'boards recent visit' do + let_it_be(:board_relation) { :board } + let_it_be(:board_parent_relation) { :group } + let_it_be(:visit_relation) { :board_group_recent_visit } end end diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb index 145a4f5b1a7..262c3a8faaa 100644 --- a/spec/models/board_project_recent_visit_spec.rb +++ b/spec/models/board_project_recent_visit_spec.rb @@ -3,9 +3,8 @@ require 'spec_helper' RSpec.describe BoardProjectRecentVisit do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:board) { create(:board, project: project) } + let_it_be(:board_parent) { create(:project) } + let_it_be(:board) { create(:board, project: board_parent) } describe 'relationships' do it { is_expected.to belong_to(:user) } @@ -19,56 +18,9 @@ RSpec.describe BoardProjectRecentVisit do it { is_expected.to validate_presence_of(:board) } end - describe '#visited' do - it 'creates a visit if one does not exists' do - expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) - end - - shared_examples 'was visited previously' do - let!(:visit) { create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago } - - it 'updates the timestamp' do - freeze_time do - described_class.visited!(user, board) - - expect(described_class.count).to eq 1 - expect(described_class.first.updated_at).to be_like_time(Time.zone.now) - end - end - end - - it_behaves_like 'was visited previously' - - context 'when we try to create a visit that is not unique' do - before do - expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') - expect(described_class).to receive(:find_or_create_by).and_return(visit) - end - - it_behaves_like 'was visited previously' - end - end - - describe '#latest' do - def create_visit(time) - create :board_project_recent_visit, project: project, user: user, updated_at: time - end - - it 'returns the most recent visited' do - create_visit(7.days.ago) - create_visit(5.days.ago) - recent = create_visit(1.day.ago) - - expect(described_class.latest(user, project)).to eq recent - end - - it 'returns last 3 visited boards' do - create_visit(7.days.ago) - visit1 = create_visit(3.days.ago) - visit2 = create_visit(2.days.ago) - visit3 = create_visit(5.days.ago) - - expect(described_class.latest(user, project, count: 3)).to eq([visit2, visit1, visit3]) - end + it_behaves_like 'boards recent visit' do + let_it_be(:board_relation) { :board } + let_it_be(:board_parent_relation) { :project } + let_it_be(:visit_relation) { :board_project_recent_visit } end end diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb index 652ea431696..d1b7125a6e6 100644 --- a/spec/models/bulk_imports/entity_spec.rb +++ b/spec/models/bulk_imports/entity_spec.rb @@ -125,4 +125,13 @@ RSpec.describe BulkImports::Entity, type: :model do end end end + + describe '#encoded_source_full_path' do + it 'encodes entity source full path' do + expected = 'foo%2Fbar' + entity = build(:bulk_import_entity, source_full_path: 'foo/bar') + + expect(entity.encoded_source_full_path).to eq(expected) + end + end end diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 5d5351eb9fe..ffdb9fc988c 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -660,27 +660,37 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.to match_array([pypi_package]) } end - describe '.displayable' do + context 'status scopes' do let_it_be(:hidden_package) { create(:maven_package, :hidden) } let_it_be(:processing_package) { create(:maven_package, :processing) } let_it_be(:error_package) { create(:maven_package, :error) } - subject { described_class.displayable } + describe '.displayable' do + subject { described_class.displayable } - it 'does not include non-displayable packages', :aggregate_failures do - is_expected.to include(error_package) - is_expected.not_to include(hidden_package) - is_expected.not_to include(processing_package) + it 'does not include non-displayable packages', :aggregate_failures do + is_expected.to include(error_package) + is_expected.not_to include(hidden_package) + is_expected.not_to include(processing_package) + end end - end - describe '.with_status' do - let_it_be(:hidden_package) { create(:maven_package, :hidden) } + describe '.installable' do + subject { described_class.installable } - subject { described_class.with_status(:hidden) } + it 'does not include non-displayable packages', :aggregate_failures do + is_expected.not_to include(error_package) + is_expected.not_to include(hidden_package) + is_expected.not_to include(processing_package) + end + end + + describe '.with_status' do + subject { described_class.with_status(:hidden) } - it 'returns packages with specified status' do - is_expected.to match_array([hidden_package]) + it 'returns packages with specified status' do + is_expected.to match_array([hidden_package]) + end end end end diff --git a/spec/services/boards/visits/create_service_spec.rb b/spec/services/boards/visits/create_service_spec.rb index 64faa2cf07b..8910345d170 100644 --- a/spec/services/boards/visits/create_service_spec.rb +++ b/spec/services/boards/visits/create_service_spec.rb @@ -7,47 +7,20 @@ RSpec.describe Boards::Visits::CreateService do let(:user) { create(:user) } context 'when a project board' do - let(:project) { create(:project) } - let(:project_board) { create(:board, project: project) } + let_it_be(:project) { create(:project) } + let_it_be(:board) { create(:board, project: project) } - subject(:service) { described_class.new(project_board.resource_parent, user) } + let_it_be(:model) { BoardProjectRecentVisit } - it 'returns nil when there is no user' do - service.current_user = nil - - expect(service.execute(project_board)).to eq nil - end - - it 'returns nil when database is read-only' do - allow(Gitlab::Database).to receive(:read_only?) { true } - - expect(service.execute(project_board)).to eq nil - end - - it 'records the visit' do - expect(BoardProjectRecentVisit).to receive(:visited!).once - - service.execute(project_board) - end + it_behaves_like 'boards recent visit create service' end context 'when a group board' do - let(:group) { create(:group) } - let(:group_board) { create(:board, group: group) } - - subject(:service) { described_class.new(group_board.resource_parent, user) } - - it 'returns nil when there is no user' do - service.current_user = nil - - expect(service.execute(group_board)).to eq nil - end - - it 'records the visit' do - expect(BoardGroupRecentVisit).to receive(:visited!).once + let_it_be(:group) { create(:group) } + let_it_be(:board) { create(:board, group: group) } + let_it_be(:model) { BoardGroupRecentVisit } - service.execute(group_board) - end + it_behaves_like 'boards recent visit create service' end end end diff --git a/spec/services/packages/nuget/search_service_spec.rb b/spec/services/packages/nuget/search_service_spec.rb index db758dc6672..1838065c5be 100644 --- a/spec/services/packages/nuget/search_service_spec.rb +++ b/spec/services/packages/nuget/search_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Packages::Nuget::SearchService do let_it_be(:group) { create(:group) } let_it_be(:subgroup) { create(:group, parent: group) } let_it_be(:project) { create(:project, namespace: subgroup) } - let_it_be(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') } + let_it_be_with_refind(:package_a) { create(:nuget_package, project: project, name: 'DummyPackageA') } let_it_be(:packages_b) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageB') } let_it_be(:packages_c) { create_list(:nuget_package, 5, project: project, name: 'DummyPackageC') } let_it_be(:package_d) { create(:nuget_package, project: project, name: 'FooBarD') } @@ -79,6 +79,16 @@ RSpec.describe Packages::Nuget::SearchService do it { expect_search_results 4, package_a, packages_b, packages_c, package_d } end + context 'with non-displayable packages' do + let(:search_term) { '' } + + before do + package_a.update_column(:status, 1) + end + + it { expect_search_results 3, packages_b, packages_c, package_d } + end + context 'with prefix search term' do let(:search_term) { 'dummy' } diff --git a/spec/support/shared_examples/finders/packages_shared_examples.rb b/spec/support/shared_examples/finders/packages_shared_examples.rb index 2d4e8d0df1f..b3ec2336cca 100644 --- a/spec/support/shared_examples/finders/packages_shared_examples.rb +++ b/spec/support/shared_examples/finders/packages_shared_examples.rb @@ -20,9 +20,11 @@ end RSpec.shared_examples 'concerning package statuses' do let_it_be(:hidden_package) { create(:maven_package, :hidden, project: project) } + let_it_be(:error_package) { create(:maven_package, :error, project: project) } - context 'hidden packages' do + context 'displayable packages' do it { is_expected.not_to include(hidden_package) } + it { is_expected.to include(error_package) } end context 'with status param' do diff --git a/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb new file mode 100644 index 00000000000..68ea460dabc --- /dev/null +++ b/spec/support/shared_examples/services/boards/boards_recent_visit_shared_examples.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards recent visit' do + let_it_be(:user) { create(:user) } + + describe '#visited' do + it 'creates a visit if one does not exists' do + expect { described_class.visited!(user, board) }.to change(described_class, :count).by(1) + end + + shared_examples 'was visited previously' do + let_it_be(:visit) do + create(visit_relation, + board_parent_relation => board_parent, + board_relation => board, + user: user, + updated_at: 7.days.ago + ) + end + + it 'updates the timestamp' do + freeze_time do + described_class.visited!(user, board) + + expect(described_class.count).to eq 1 + expect(described_class.first.updated_at).to be_like_time(Time.zone.now) + end + end + end + + it_behaves_like 'was visited previously' + + context 'when we try to create a visit that is not unique' do + before do + expect(described_class).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique, 'record not unique') + expect(described_class).to receive(:find_or_create_by).and_return(visit) + end + + it_behaves_like 'was visited previously' + end + end + + describe '#latest' do + def create_visit(time) + create(visit_relation, board_parent_relation => board_parent, user: user, updated_at: time) + end + + it 'returns the most recent visited' do + create_visit(7.days.ago) + create_visit(5.days.ago) + recent = create_visit(1.day.ago) + + expect(described_class.latest(user, board_parent)).to eq recent + end + + it 'returns last 3 visited boards' do + create_visit(7.days.ago) + visit1 = create_visit(3.days.ago) + visit2 = create_visit(2.days.ago) + visit3 = create_visit(5.days.ago) + + expect(described_class.latest(user, board_parent, count: 3)).to eq([visit2, visit1, visit3]) + end + end +end diff --git a/spec/support/shared_examples/services/boards/create_service_shared_examples.rb b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb new file mode 100644 index 00000000000..63b5e3a5a84 --- /dev/null +++ b/spec/support/shared_examples/services/boards/create_service_shared_examples.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'boards recent visit create service' do + let_it_be(:user) { create(:user) } + + subject(:service) { described_class.new(board.resource_parent, user) } + + it 'returns nil when there is no user' do + service.current_user = nil + + expect(service.execute(board)).to be_nil + end + + it 'returns nil when database is read only' do + allow(Gitlab::Database).to receive(:read_only?) { true } + + expect(service.execute(board)).to be_nil + end + + it 'records the visit' do + expect(model).to receive(:visited!).once + + service.execute(board) + end +end diff --git a/spec/workers/bulk_import_worker_spec.rb b/spec/workers/bulk_import_worker_spec.rb index 5964ec45563..9119394f250 100644 --- a/spec/workers/bulk_import_worker_spec.rb +++ b/spec/workers/bulk_import_worker_spec.rb @@ -69,7 +69,7 @@ RSpec.describe BulkImportWorker do end context 'when there are created entities to process' do - it 'marks a batch of entities as started, enqueues BulkImports::EntityWorker and reenqueues' do + it 'marks a batch of entities as started, enqueues EntityWorker, ExportRequestWorker and reenqueues' do stub_const("#{described_class}::DEFAULT_BATCH_SIZE", 1) bulk_import = create(:bulk_import, :created) @@ -78,6 +78,7 @@ RSpec.describe BulkImportWorker do expect(described_class).to receive(:perform_in).with(described_class::PERFORM_DELAY, bulk_import.id) expect(BulkImports::EntityWorker).to receive(:perform_async) + expect(BulkImports::ExportRequestWorker).to receive(:perform_async) subject.perform(bulk_import.id) diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb new file mode 100644 index 00000000000..f7838279212 --- /dev/null +++ b/spec/workers/bulk_imports/export_request_worker_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::ExportRequestWorker do + let_it_be(:bulk_import) { create(:bulk_import) } + let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) } + let_it_be(:entity) { create(:bulk_import_entity, source_full_path: 'foo/bar', bulk_import: bulk_import) } + + let(:response_double) { double(code: 200, success?: true, parsed_response: {}) } + let(:job_args) { [entity.id] } + + describe '#perform' do + before do + allow(Gitlab::HTTP).to receive(:post).and_return(response_double) + end + + include_examples 'an idempotent worker' do + it 'requests relations export' do + expected = "/groups/foo%2Fbar/export_relations" + + expect_next_instance_of(BulkImports::Clients::Http) do |client| + expect(client).to receive(:post).with(expected).twice + end + + perform_multiple(job_args) + end + end + end +end |