diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-12 12:10:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-12 12:10:19 +0300 |
commit | 133ec9237af290062aae70e6f115db69b51c88de (patch) | |
tree | 866ddeec2098e6557e4fe93941438ada45713940 /spec | |
parent | f9ad7e0c9fa576bf4d651ffe3e278750bf548400 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
23 files changed, 592 insertions, 132 deletions
diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb index 0c8a06d7e28..b294f1117f5 100644 --- a/spec/finders/deployments_finder_spec.rb +++ b/spec/finders/deployments_finder_spec.rb @@ -201,17 +201,31 @@ RSpec.describe DeploymentsFinder do it 'adds `id` sorting as the second order column' do order_value = subject.order_values[1] - expect(order_value.to_sql).to eq(Deployment.arel_table[:id].desc.to_sql) + expect(order_value.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql) end - it 'uses the `id DESC` as tie-breaker when ordering' do + it 'uses the `id ASC` as tie-breaker when ordering' do updated_at = Time.now deployment_1 = create(:deployment, :success, project: project, updated_at: updated_at) deployment_2 = create(:deployment, :success, project: project, updated_at: updated_at) deployment_3 = create(:deployment, :success, project: project, updated_at: updated_at) - expect(subject).to eq([deployment_3, deployment_2, deployment_1]) + expect(subject).to eq([deployment_1, deployment_2, deployment_3]) + end + + context 'when sort direction is desc' do + let(:params) { { **base_params, updated_after: 1.day.ago, order_by: 'updated_at', sort: 'desc' } } + + it 'uses the `id DESC` as tie-breaker when ordering' do + updated_at = Time.now + + deployment_1 = create(:deployment, :success, project: project, updated_at: updated_at) + deployment_2 = create(:deployment, :success, project: project, updated_at: updated_at) + deployment_3 = create(:deployment, :success, project: project, updated_at: updated_at) + + expect(subject).to eq([deployment_3, deployment_2, deployment_1]) + end end end @@ -228,7 +242,7 @@ RSpec.describe DeploymentsFinder do it 'sorts by `updated_at`' do expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:updated_at].asc.to_sql) - expect(subject.order_values.second.to_sql).to eq(Deployment.arel_table[:id].desc.to_sql) + expect(subject.order_values.second.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql) end context 'when deployments_finder_implicitly_enforce_ordering_for_updated_at_filter feature flag is disabled' do diff --git a/spec/fixtures/api/schemas/public_api/v4/packages/package.json b/spec/fixtures/api/schemas/public_api/v4/packages/package.json index 08909efd10c..607e0df1886 100644 --- a/spec/fixtures/api/schemas/public_api/v4/packages/package.json +++ b/spec/fixtures/api/schemas/public_api/v4/packages/package.json @@ -4,6 +4,7 @@ "name", "version", "package_type", + "status", "_links", "versions" ], @@ -20,6 +21,9 @@ "package_type": { "type": "string" }, + "status": { + "type": "string" + }, "_links": { "type": "object", "required": [ diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap index 03b98478f3e..f4e617ecafe 100644 --- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap +++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap @@ -34,6 +34,8 @@ exports[`packages_list_row renders 1`] = ` </gl-link-stub> <!----> + + <!----> </div> <!----> diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js index fd54cd0f25d..bd15d48c4eb 100644 --- a/spec/frontend/packages/shared/components/package_list_row_spec.js +++ b/spec/frontend/packages/shared/components/package_list_row_spec.js @@ -1,8 +1,11 @@ +import { GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import PackagesListRow from '~/packages/shared/components/package_list_row.vue'; import PackagePath from '~/packages/shared/components/package_path.vue'; import PackageTags from '~/packages/shared/components/package_tags.vue'; +import { PACKAGE_ERROR_STATUS } from '~/packages/shared/constants'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import { packageList } from '../../mock_data'; @@ -20,7 +23,10 @@ describe('packages_list_row', () => { const findPackagePath = () => wrapper.find(PackagePath); const findDeleteButton = () => wrapper.find('[data-testid="action-delete"]'); const findPackageIconAndName = () => wrapper.find(PackageIconAndName); - const findInfrastructureIconAndName = () => wrapper.find(InfrastructureIconAndName); + const findInfrastructureIconAndName = () => wrapper.findComponent(InfrastructureIconAndName); + const findListItem = () => wrapper.findComponent(ListItem); + const findPackageLink = () => wrapper.findComponent(GlLink); + const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]'); const mountComponent = ({ isGroup = false, @@ -44,6 +50,9 @@ describe('packages_list_row', () => { showPackageType, disableDelete, }, + directives: { + GlTooltip: createMockDirective(), + }, }); }; @@ -146,4 +155,31 @@ describe('packages_list_row', () => { expect(findInfrastructureIconAndName().exists()).toBe(true); }); }); + + describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => { + beforeEach(() => { + mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } }); + }); + + it('list item has a disabled prop', () => { + expect(findListItem().props('disabled')).toBe(true); + }); + + it('details link is disabled', () => { + expect(findPackageLink().attributes('disabled')).toBe('true'); + }); + + it('has a warning icon', () => { + const icon = findWarningIcon(); + const tooltip = getBinding(icon.element, 'gl-tooltip'); + expect(icon.props('icon')).toBe('warning'); + expect(tooltip.value).toMatchObject({ + title: 'Invalid Package: failed metadata extraction', + }); + }); + + it('delete button is disabled', () => { + expect(findDeleteButton().props('disabled')).toBe(true); + }); + }); }); diff --git a/spec/frontend/packages/shared/components/package_path_spec.js b/spec/frontend/packages/shared/components/package_path_spec.js index 3c9cd3387ba..edbdd55c1d7 100644 --- a/spec/frontend/packages/shared/components/package_path_spec.js +++ b/spec/frontend/packages/shared/components/package_path_spec.js @@ -39,48 +39,66 @@ describe('PackagePath', () => { const pathPieces = path.split('/').slice(1); const hasTooltip = shouldExist.includes(ELLIPSIS_ICON); - beforeEach(() => { - mountComponent({ path }); - }); + describe('not disabled component', () => { + beforeEach(() => { + mountComponent({ path }); + }); - it('should have a base icon', () => { - expect(findItem(BASE_ICON).exists()).toBe(true); - }); + it('should have a base icon', () => { + expect(findItem(BASE_ICON).exists()).toBe(true); + }); - it('should have a root link', () => { - const root = findItem(ROOT_LINK); - expect(root.exists()).toBe(true); - expect(root.attributes('href')).toBe(rootUrl); - }); + it('should have a root link', () => { + const root = findItem(ROOT_LINK); + expect(root.exists()).toBe(true); + expect(root.attributes('href')).toBe(rootUrl); + }); - if (hasTooltip) { - it('should have a tooltip', () => { - const tooltip = findTooltip(findItem(ELLIPSIS_ICON)); - expect(tooltip).toBeDefined(); - expect(tooltip.value).toMatchObject({ - title: path, + if (hasTooltip) { + it('should have a tooltip', () => { + const tooltip = findTooltip(findItem(ELLIPSIS_ICON)); + expect(tooltip).toBeDefined(); + expect(tooltip.value).toMatchObject({ + title: path, + }); }); - }); - } + } - if (shouldExist.length) { - it.each(shouldExist)(`should have %s`, (element) => { - expect(findItem(element).exists()).toBe(true); - }); - } + if (shouldExist.length) { + it.each(shouldExist)(`should have %s`, (element) => { + expect(findItem(element).exists()).toBe(true); + }); + } - if (shouldNotExist.length) { - it.each(shouldNotExist)(`should not have %s`, (element) => { - expect(findItem(element).exists()).toBe(false); + if (shouldNotExist.length) { + it.each(shouldNotExist)(`should not have %s`, (element) => { + expect(findItem(element).exists()).toBe(false); + }); + } + + if (shouldExist.includes(LEAF_LINK)) { + it('the last link should be the last piece of the path', () => { + const leaf = findItem(LEAF_LINK); + expect(leaf.attributes('href')).toBe(`/${path}`); + expect(leaf.text()).toBe(pathPieces[pathPieces.length - 1]); + }); + } + }); + + describe('disabled component', () => { + beforeEach(() => { + mountComponent({ path, disabled: true }); }); - } - if (shouldExist.includes(LEAF_LINK)) { - it('the last link should be the last piece of the path', () => { - const leaf = findItem(LEAF_LINK); - expect(leaf.attributes('href')).toBe(`/${path}`); - expect(leaf.text()).toBe(pathPieces[pathPieces.length - 1]); + it('root link is disabled', () => { + expect(findItem(ROOT_LINK).attributes('disabled')).toBe('true'); }); - } + + if (shouldExist.includes(LEAF_LINK)) { + it('the last link is disabled', () => { + expect(findItem(LEAF_LINK).attributes('disabled')).toBe('true'); + }); + } + }); }); }); diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js index 8b70f84c1bd..dc9063bde2c 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js @@ -1,5 +1,6 @@ import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import DeleteButton from '~/registry/explorer/components/delete_button.vue'; @@ -72,8 +73,15 @@ describe('tags list row', () => { expect(findCheckbox().exists()).toBe(false); }); - it('is disabled when the digest is missing', () => { - mountComponent({ tag: { ...tag, digest: null } }); + it.each` + digest | disabled + ${'foo'} | ${true} + ${null} | ${false} + ${null} | ${true} + ${'foo'} | ${true} + `('is disabled when the digest $digest and disabled is $disabled', ({ digest, disabled }) => { + mountComponent({ tag: { ...tag, digest }, disabled }); + expect(findCheckbox().attributes('disabled')).toBe('true'); }); @@ -141,6 +149,12 @@ describe('tags list row', () => { title: tag.location, }); }); + + it('is disabled when the component is disabled', () => { + mountComponent({ ...defaultProps, disabled: true }); + + expect(findClipboardButton().attributes('disabled')).toBe('true'); + }); }); describe('warning icon', () => { @@ -266,15 +280,19 @@ describe('tags list row', () => { }); it.each` - canDelete | digest - ${true} | ${null} - ${false} | ${'foo'} - ${false} | ${null} - `('is disabled when canDelete is $canDelete and digest is $digest', ({ canDelete, digest }) => { - mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest } }); - - expect(findDeleteButton().attributes('disabled')).toBe('true'); - }); + canDelete | digest | disabled + ${true} | ${null} | ${true} + ${false} | ${'foo'} | ${true} + ${false} | ${null} | ${true} + ${true} | ${'foo'} | ${true} + `( + 'is disabled when canDelete is $canDelete and digest is $digest and disabled is $disabled', + ({ canDelete, digest, disabled }) => { + mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest }, disabled }); + + expect(findDeleteButton().attributes('disabled')).toBe('true'); + }, + ); it('delete event emits delete', () => { mountComponent(); @@ -287,13 +305,10 @@ describe('tags list row', () => { describe('details rows', () => { describe('when the tag has a digest', () => { - beforeEach(() => { + it('has 3 details rows', async () => { mountComponent(); + await nextTick(); - return wrapper.vm.$nextTick(); - }); - - it('has 3 details rows', () => { expect(findDetailsRows().length).toBe(3); }); @@ -303,17 +318,37 @@ describe('tags list row', () => { ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true} ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true} `('$name details row', ({ finderFunction, text, icon, clipboard }) => { - it(`has ${text} as text`, () => { + it(`has ${text} as text`, async () => { + mountComponent(); + await nextTick(); + expect(finderFunction().text()).toMatchInterpolatedText(text); }); - it(`has the ${icon} icon`, () => { + it(`has the ${icon} icon`, async () => { + mountComponent(); + await nextTick(); + expect(finderFunction().props('icon')).toBe(icon); }); - it(`is ${clipboard} that clipboard button exist`, () => { - expect(finderFunction().find(ClipboardButton).exists()).toBe(clipboard); - }); + if (clipboard) { + it(`clipboard button exist`, async () => { + mountComponent(); + await nextTick(); + + expect(finderFunction().find(ClipboardButton).exists()).toBe(clipboard); + }); + + it('is disabled when the component is disabled', async () => { + mountComponent({ ...defaultProps, disabled: true }); + await nextTick(); + + expect(finderFunction().findComponent(ClipboardButton).attributes('disabled')).toBe( + 'true', + ); + }); + } }); }); @@ -321,7 +356,7 @@ describe('tags list row', () => { it('hides the details rows', async () => { mountComponent({ tag: { ...tag, digest: null } }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDetailsRows().length).toBe(0); }); }); diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js index 6c897b983f7..323d7b177e7 100644 --- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js @@ -25,10 +25,11 @@ describe('Image List Row', () => { const findDetailsLink = () => wrapper.find('[data-testid="details-link"]'); const findTagsCount = () => wrapper.find('[data-testid="tags-count"]'); - const findDeleteBtn = () => wrapper.find(DeleteButton); - const findClipboardButton = () => wrapper.find(ClipboardButton); + const findDeleteBtn = () => wrapper.findComponent(DeleteButton); + const findClipboardButton = () => wrapper.findComponent(ClipboardButton); const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]'); - const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findListItemComponent = () => wrapper.findComponent(ListItem); const mountComponent = (props) => { wrapper = shallowMount(Component, { @@ -52,20 +53,28 @@ describe('Image List Row', () => { wrapper = null; }); - describe('main tooltip', () => { - it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => { - mountComponent(); + describe('list item component', () => { + describe('tooltip', () => { + it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => { + mountComponent(); + + const tooltip = getBinding(wrapper.element, 'gl-tooltip'); + expect(tooltip).toBeDefined(); + expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION); + }); - const tooltip = getBinding(wrapper.element, 'gl-tooltip'); - expect(tooltip).toBeDefined(); - expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION); + it('is disabled when item is being deleted', () => { + mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } }); + + const tooltip = getBinding(wrapper.element, 'gl-tooltip'); + expect(tooltip.value.disabled).toBe(false); + }); }); - it('is disabled when item is being deleted', () => { + it('is disabled when the item is in deleting status', () => { mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } }); - const tooltip = getBinding(wrapper.element, 'gl-tooltip'); - expect(tooltip.value.disabled).toBe(false); + expect(findListItemComponent().props('disabled')).toBe(true); }); }); @@ -118,6 +127,20 @@ describe('Image List Row', () => { }, ); }); + + describe('when the item is deleting', () => { + beforeEach(() => { + mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } }); + }); + + it('the router link is disabled', () => { + // we check the event prop as is the only workaround to disable a router link + expect(findDetailsLink().props('event')).toBe(''); + }); + it('the clipboard button is disabled', () => { + expect(findClipboardButton().attributes('disabled')).toBe('true'); + }); + }); }); describe('delete button', () => { diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js index 33c9c808dc3..ca4bf0b0652 100644 --- a/spec/frontend/vue_shared/components/registry/list_item_spec.js +++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js @@ -101,16 +101,16 @@ describe('list item', () => { }); describe('disabled prop', () => { - it('when true applies disabled-content class', () => { + it('when true applies gl-opacity-5 class', () => { mountComponent({ disabled: true }); - expect(wrapper.classes('disabled-content')).toBe(true); + expect(wrapper.classes('gl-opacity-5')).toBe(true); }); - it('when false does not apply disabled-content class', () => { + it('when false does not apply gl-opacity-5 class', () => { mountComponent({ disabled: false }); - expect(wrapper.classes('disabled-content')).toBe(false); + expect(wrapper.classes('gl-opacity-5')).toBe(false); }); }); diff --git a/spec/frontend/vue_shared/directives/validation_spec.js b/spec/frontend/vue_shared/directives/validation_spec.js index 0c84be9354f..80c283c1a91 100644 --- a/spec/frontend/vue_shared/directives/validation_spec.js +++ b/spec/frontend/vue_shared/directives/validation_spec.js @@ -1,10 +1,10 @@ import { shallowMount } from '@vue/test-utils'; -import validation from '~/vue_shared/directives/validation'; +import validation, { initForm } from '~/vue_shared/directives/validation'; describe('validation directive', () => { let wrapper; - const createComponent = ({ inputAttributes, showValidation, template } = {}) => { + const createComponentFactory = ({ inputAttributes, template, data }) => { const defaultInputAttributes = { type: 'text', required: true, @@ -23,16 +23,7 @@ describe('validation directive', () => { data() { return { attributes: inputAttributes || defaultInputAttributes, - showValidation, - form: { - state: null, - fields: { - exampleField: { - state: null, - feedback: '', - }, - }, - }, + ...data, }; }, template: template || defaultTemplate, @@ -41,6 +32,44 @@ describe('validation directive', () => { wrapper = shallowMount(component, { attachTo: document.body }); }; + const createComponent = ({ inputAttributes, showValidation, template } = {}) => + createComponentFactory({ + inputAttributes, + data: { + showValidation, + form: { + state: null, + fields: { + exampleField: { + state: null, + feedback: '', + }, + }, + }, + }, + template, + }); + + const createComponentWithInitForm = ({ inputAttributes } = {}) => + createComponentFactory({ + inputAttributes, + data: { + form: initForm({ + fields: { + exampleField: { + state: null, + value: 'lorem', + }, + }, + }), + }, + template: ` + <form> + <input v-validation:[form.showValidation] name="exampleField" v-bind="attributes" /> + </form> + `, + }); + afterEach(() => { wrapper.destroy(); wrapper = null; @@ -179,4 +208,86 @@ describe('validation directive', () => { }); }); }); + + describe('component using initForm', () => { + it('sets the form fields correctly', () => { + createComponentWithInitForm(); + + expect(getFormData().state).toBe(false); + expect(getFormData().showValidation).toBe(false); + + expect(getFormData().fields.exampleField).toMatchObject({ + value: 'lorem', + state: null, + required: true, + feedback: expect.any(String), + }); + }); + }); +}); + +describe('initForm', () => { + const MOCK_FORM = { + fields: { + name: { + value: 'lorem', + }, + description: { + value: 'ipsum', + required: false, + skipValidation: true, + }, + }, + }; + + it('returns form object', () => { + expect(initForm(MOCK_FORM)).toMatchObject({ + state: false, + showValidation: false, + fields: { + name: { value: 'lorem', required: true, state: null, feedback: null }, + description: { value: 'ipsum', required: false, state: true, feedback: null }, + }, + }); + }); + + it('returns form object with additional parameters', () => { + const customFormObject = { + foo: { + bar: 'lorem', + }, + }; + + const form = { + ...MOCK_FORM, + ...customFormObject, + }; + + expect(initForm(form)).toMatchObject({ + state: false, + showValidation: false, + fields: { + name: { value: 'lorem', required: true, state: null, feedback: null }, + description: { value: 'ipsum', required: false, state: true, feedback: null }, + }, + ...customFormObject, + }); + }); + + it('can override existing state and showValidation values', () => { + const form = { + ...MOCK_FORM, + state: true, + showValidation: true, + }; + + expect(initForm(form)).toMatchObject({ + state: true, + showValidation: true, + fields: { + name: { value: 'lorem', required: true, state: null, feedback: null }, + description: { value: 'ipsum', required: false, state: true, feedback: null }, + }, + }); + }); }); diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 3ef2fbba002..0f7cadbd4a7 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -14,7 +14,7 @@ RSpec.describe GitlabSchema.types['Project'] do it 'has the expected fields' do expected_fields = %w[ user_permissions id full_path path name_with_namespace - name description description_html tag_list ssh_url_to_repo + name description description_html tag_list topics ssh_url_to_repo http_url_to_repo web_url star_count forks_count created_at last_activity_at archived visibility container_registry_enabled shared_runners_enabled diff --git a/spec/helpers/nav/top_nav_helper_spec.rb b/spec/helpers/nav/top_nav_helper_spec.rb index 42626ff7c23..5c9e1e82b01 100644 --- a/spec/helpers/nav/top_nav_helper_spec.rb +++ b/spec/helpers/nav/top_nav_helper_spec.rb @@ -11,12 +11,17 @@ RSpec.describe Nav::TopNavHelper do let(:current_user) { nil } let(:current_project) { nil } + let(:current_group) { nil } let(:with_current_settings_admin_mode) { false } let(:with_header_link_admin_mode) { false } + let(:with_sherlock_enabled) { false } let(:with_projects) { false } + let(:with_groups) { false } let(:with_milestones) { false } + let(:with_snippets) { false } + let(:with_activity) { false } - let(:subject) { helper.top_nav_view_model(project: current_project) } + let(:subject) { helper.top_nav_view_model(project: current_project, group: current_group) } let(:active_title) { 'Menu' } @@ -24,13 +29,17 @@ RSpec.describe Nav::TopNavHelper do allow(helper).to receive(:current_user) { current_user } allow(Gitlab::CurrentSettings).to receive(:admin_mode) { with_current_settings_admin_mode } allow(helper).to receive(:header_link?).with(:admin_mode) { with_header_link_admin_mode } + allow(Gitlab::Sherlock).to receive(:enabled?) { with_sherlock_enabled } # Defaulting all `dashboard_nav_link?` calls to false ensures the EE-specific behavior # is not enabled in this CE spec allow(helper).to receive(:dashboard_nav_link?).with(anything) { false } allow(helper).to receive(:dashboard_nav_link?).with(:projects) { with_projects } + allow(helper).to receive(:dashboard_nav_link?).with(:groups) { with_groups } allow(helper).to receive(:dashboard_nav_link?).with(:milestones) { with_milestones } + allow(helper).to receive(:dashboard_nav_link?).with(:snippets) { with_snippets } + allow(helper).to receive(:dashboard_nav_link?).with(:activity) { with_activity } end it 'has :activeTitle' do @@ -39,13 +48,30 @@ RSpec.describe Nav::TopNavHelper do context 'when current_user is nil (anonymous)' do it 'has expected :primary' do - expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( + expected_projects_item = ::Gitlab::Nav::TopNavMenuItem.build( href: '/explore', icon: 'project', id: 'project', title: 'Projects' ) - expect(subject[:primary]).to eq([expected_primary]) + expected_groups_item = ::Gitlab::Nav::TopNavMenuItem.build( + href: '/explore/groups', + icon: 'group', + id: 'groups', + title: 'Groups' + ) + expected_snippets_item = ::Gitlab::Nav::TopNavMenuItem.build( + href: '/explore/snippets', + icon: 'snippet', + id: 'snippets', + title: 'Snippets' + ) + expect(subject[:primary]) + .to eq([ + expected_projects_item, + expected_groups_item, + expected_snippets_item + ]) end end @@ -124,7 +150,7 @@ RSpec.describe Nav::TopNavHelper do let_it_be(:project) { build_stubbed(:project) } let(:current_project) { project } - let(:avatar_url) { 'avatar_url' } + let(:avatar_url) { 'project_avatar_url' } before do allow(project).to receive(:persisted?) { true } @@ -146,6 +172,87 @@ RSpec.describe Nav::TopNavHelper do end end + context 'with groups' do + let(:with_groups) { true } + let(:groups_view) { subject[:views][:groups] } + + it 'has expected :primary' do + expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( + css_class: 'qa-groups-dropdown', + data: { + track_event: 'click_dropdown', + track_label: 'groups_dropdown' + }, + icon: 'group', + id: 'groups', + title: 'Groups', + view: 'groups' + ) + expect(subject[:primary]).to eq([expected_primary]) + end + + context 'groups' do + it 'has expected :currentUserName' do + expect(groups_view[:currentUserName]).to eq(current_user.username) + end + + it 'has expected :namespace' do + expect(groups_view[:namespace]).to eq('groups') + end + + it 'has expected :linksPrimary' do + expected_links_primary = [ + ::Gitlab::Nav::TopNavMenuItem.build( + href: '/dashboard/groups', + id: 'your', + title: 'Your groups' + ), + ::Gitlab::Nav::TopNavMenuItem.build( + href: '/explore/groups', + id: 'explore', + title: 'Explore groups' + ) + ] + expect(groups_view[:linksPrimary]).to eq(expected_links_primary) + end + + it 'has expected :linksSecondary' do + expected_links_secondary = [ + ::Gitlab::Nav::TopNavMenuItem.build( + href: '/groups/new#create-group-pane', + id: 'create', + title: 'Create group' + ) + ] + expect(groups_view[:linksSecondary]).to eq(expected_links_secondary) + end + + context 'with persisted group' do + let_it_be(:group) { build_stubbed(:group) } + + let(:current_group) { group } + let(:avatar_url) { 'group_avatar_url' } + + before do + allow(group).to receive(:persisted?) { true } + allow(group).to receive(:avatar_url) { avatar_url } + end + + it 'has expected :container' do + expected_container = { + avatarUrl: avatar_url, + id: group.id, + name: group.name, + namespace: group.full_name, + webUrl: group_path(group) + } + + expect(groups_view[:currentItem]).to eq(expected_container) + end + end + end + end + context 'with milestones' do let(:with_milestones) { true } @@ -162,6 +269,61 @@ RSpec.describe Nav::TopNavHelper do expect(subject[:primary]).to eq([expected_primary]) end end + + context 'with snippets' do + let(:with_snippets) { true } + + it 'has expected :primary' do + expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( + data: { + qa_selector: 'snippets_link' + }, + href: '/dashboard/snippets', + icon: 'snippet', + id: 'snippets', + title: 'Snippets' + ) + expect(subject[:primary]).to eq([expected_primary]) + end + end + + context 'with activity' do + let(:with_activity) { true } + + it 'has expected :primary' do + expected_primary = ::Gitlab::Nav::TopNavMenuItem.build( + data: { + qa_selector: 'activity_link' + }, + href: '/dashboard/activity', + icon: 'history', + id: 'activity', + title: 'Activity' + ) + expect(subject[:primary]).to eq([expected_primary]) + end + end + + context 'when sherlock is enabled' do + let(:with_sherlock_enabled) { true } + + before do + # Note: We have to mock the sherlock route because the route is conditional on + # sherlock being enabled, but it parsed at Rails load time and can't be overridden + # in a spec. + allow(helper).to receive(:sherlock_transactions_path) { '/fake_sherlock_path' } + end + + it 'has sherlock as last :secondary item' do + expected_sherlock_item = ::Gitlab::Nav::TopNavMenuItem.build( + id: 'sherlock', + title: 'Sherlock Transactions', + icon: 'admin', + href: '/fake_sherlock_path' + ) + expect(subject[:secondary].last).to eq(expected_sherlock_item) + end + end end context 'when current_user is admin' do diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 5ec66e7f6a8..a5e4d37d306 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -46,12 +46,20 @@ RSpec.describe Gitlab::Highlight do expect(result).to eq(%[<span id="LC1" class="line" lang="plaintext">plain text contents</span>]) end - it 'returns plain version for long content' do - stub_config(extra: { 'maximum_text_highlight_size_kilobytes' => 0.0001 } ) # 1.024 bytes + context 'when content is too long to be highlighted' do + let(:result) { described_class.highlight(file_name, content) } # content is 44 bytes - result = described_class.highlight(file_name, content) # content is 44 bytes + before do + stub_config(extra: { 'maximum_text_highlight_size_kilobytes' => 0.0001 } ) # 1.024 bytes + end + + it 'increments the metric for oversized files' do + expect { result }.to change { over_highlight_size_limit('text highlighter') }.by(1) + end - expect(result).to eq(%[<span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem")</span>]) + it 'returns plain version for long content' do + expect(result).to eq(%[<span id="LC1" class="line" lang="">(make-pathname :defaults name</span>\n<span id="LC2" class="line" lang="">:type "assem")</span>]) + end end it 'highlights multi-line comments' do @@ -168,4 +176,11 @@ RSpec.describe Gitlab::Highlight do .counter(:highlight_timeout, 'Counts the times highlights have timed out') .get(source: source) end + + def over_highlight_size_limit(source) + Gitlab::Metrics + .counter(:over_highlight_size_limit, + 'Count the times text has been over the highlight size limit') + .get(source: source) + end end diff --git a/spec/models/bulk_imports/export_spec.rb b/spec/models/bulk_imports/export_spec.rb index 26d25e6901e..d85b77d599b 100644 --- a/spec/models/bulk_imports/export_spec.rb +++ b/spec/models/bulk_imports/export_spec.rb @@ -47,12 +47,12 @@ RSpec.describe BulkImports::Export, type: :model do end end - describe '#exportable' do + describe '#portable' do context 'when associated with project' do it 'returns project' do export = create(:bulk_import_export, project: create(:project), group: nil) - expect(export.exportable).to be_instance_of(Project) + expect(export.portable).to be_instance_of(Project) end end @@ -60,7 +60,7 @@ RSpec.describe BulkImports::Export, type: :model do it 'returns group' do export = create(:bulk_import_export) - expect(export.exportable).to be_instance_of(Group) + expect(export.portable).to be_instance_of(Group) end end end @@ -70,7 +70,7 @@ RSpec.describe BulkImports::Export, type: :model do it 'returns project config' do export = create(:bulk_import_export, project: create(:project), group: nil) - expect(export.config).to be_instance_of(BulkImports::Exports::ProjectConfig) + expect(export.config).to be_instance_of(BulkImports::FileTransfer::ProjectConfig) end end @@ -78,7 +78,7 @@ RSpec.describe BulkImports::Export, type: :model do it 'returns group config' do export = create(:bulk_import_export) - expect(export.config).to be_instance_of(BulkImports::Exports::GroupConfig) + expect(export.config).to be_instance_of(BulkImports::FileTransfer::GroupConfig) end end end diff --git a/spec/models/bulk_imports/exports/group_config_spec.rb b/spec/models/bulk_imports/file_transfer/group_config_spec.rb index 856977c9310..21da71de3c7 100644 --- a/spec/models/bulk_imports/exports/group_config_spec.rb +++ b/spec/models/bulk_imports/file_transfer/group_config_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BulkImports::Exports::GroupConfig do +RSpec.describe BulkImports::FileTransfer::GroupConfig do let_it_be(:exportable) { create(:group) } let_it_be(:hex) { '123' } @@ -18,7 +18,7 @@ RSpec.describe BulkImports::Exports::GroupConfig do expect(finder).to receive(:find_root).with(:group).and_call_original end - expect(subject.exportable_tree).not_to be_empty + expect(subject.portable_tree).not_to be_empty end end @@ -32,7 +32,7 @@ RSpec.describe BulkImports::Exports::GroupConfig do describe '#exportable_relations' do it 'returns a list of top level exportable relations' do - expect(subject.exportable_relations).to include('milestones', 'badges', 'boards', 'labels') + expect(subject.portable_relations).to include('milestones', 'badges', 'boards', 'labels') end end end diff --git a/spec/models/bulk_imports/exports/project_config_spec.rb b/spec/models/bulk_imports/file_transfer/project_config_spec.rb index c0b685a091d..021f96ac2a3 100644 --- a/spec/models/bulk_imports/exports/project_config_spec.rb +++ b/spec/models/bulk_imports/file_transfer/project_config_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BulkImports::Exports::ProjectConfig do +RSpec.describe BulkImports::FileTransfer::ProjectConfig do let_it_be(:exportable) { create(:project) } let_it_be(:hex) { '123' } @@ -18,7 +18,7 @@ RSpec.describe BulkImports::Exports::ProjectConfig do expect(finder).to receive(:find_root).with(:project).and_call_original end - expect(subject.exportable_tree).not_to be_empty + expect(subject.portable_tree).not_to be_empty end end @@ -32,7 +32,7 @@ RSpec.describe BulkImports::Exports::ProjectConfig do describe '#exportable_relations' do it 'returns a list of top level exportable relations' do - expect(subject.exportable_relations).to include('issues', 'labels', 'milestones', 'merge_requests') + expect(subject.portable_relations).to include('issues', 'labels', 'milestones', 'merge_requests') end end end diff --git a/spec/models/bulk_imports/file_transfer_spec.rb b/spec/models/bulk_imports/file_transfer_spec.rb new file mode 100644 index 00000000000..5a2b303626c --- /dev/null +++ b/spec/models/bulk_imports/file_transfer_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::FileTransfer do + describe '.config_for' do + context 'when portable is group' do + it 'returns group config' do + expect(described_class.config_for(build(:group))).to be_instance_of(BulkImports::FileTransfer::GroupConfig) + end + end + + context 'when portable is project' do + it 'returns project config' do + expect(described_class.config_for(build(:project))).to be_instance_of(BulkImports::FileTransfer::ProjectConfig) + end + end + + context 'when portable is unsupported' do + it 'raises an error' do + expect { described_class.config_for(nil) }.to raise_error(BulkImports::FileTransfer::UnsupportedObjectType) + end + end + end +end diff --git a/spec/presenters/packages/detail/package_presenter_spec.rb b/spec/presenters/packages/detail/package_presenter_spec.rb index 6f4ae5a5a96..d8f1c98e762 100644 --- a/spec/presenters/packages/detail/package_presenter_spec.rb +++ b/spec/presenters/packages/detail/package_presenter_spec.rb @@ -50,6 +50,7 @@ RSpec.describe ::Packages::Detail::PackagePresenter do name: package.name, package_files: expected_package_files, package_type: package.package_type, + status: package.status, project_id: package.project_id, tags: package.tags.as_json, updated_at: package.updated_at, diff --git a/spec/requests/api/graphql/project_query_spec.rb b/spec/requests/api/graphql/project_query_spec.rb index 2cdd7273b18..b367bbaaf43 100644 --- a/spec/requests/api/graphql/project_query_spec.rb +++ b/spec/requests/api/graphql/project_query_spec.rb @@ -57,6 +57,22 @@ RSpec.describe 'getting project information' do end end + context 'topics' do + it 'includes empty topics array if no topics set' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :topics)).to match([]) + end + + it 'includes topics array' do + project.update!(tag_list: 'topic1, topic2, topic3') + + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(:project, :topics)).to match(%w[topic1 topic2 topic3]) + end + end + it 'includes inherited members in project_members' do group_member = create(:group_member, group: group) project_member = create(:project_member, project: project) diff --git a/spec/services/bulk_imports/export_service_spec.rb b/spec/services/bulk_imports/export_service_spec.rb index 4a31094a22a..2414f7c5ca7 100644 --- a/spec/services/bulk_imports/export_service_spec.rb +++ b/spec/services/bulk_imports/export_service_spec.rb @@ -10,12 +10,12 @@ RSpec.describe BulkImports::ExportService do group.add_owner(user) end - subject { described_class.new(exportable: group, user: user) } + subject { described_class.new(portable: group, user: user) } describe '#execute' do it 'schedules RelationExportWorker for each top level relation' do expect(subject).to receive(:execute).and_return(ServiceResponse.success).and_call_original - top_level_relations = BulkImports::Export.config(group).exportable_relations + top_level_relations = BulkImports::FileTransfer.config_for(group).portable_relations top_level_relations.each do |relation| expect(BulkImports::RelationExportWorker) @@ -28,7 +28,7 @@ RSpec.describe BulkImports::ExportService do context 'when exception occurs' do it 'does not schedule RelationExportWorker' do - service = described_class.new(exportable: nil, user: user) + service = described_class.new(portable: nil, user: user) expect(service) .to receive(:execute) diff --git a/spec/services/bulk_imports/relation_export_service_spec.rb b/spec/services/bulk_imports/relation_export_service_spec.rb index 1116fb1f988..bf286998df2 100644 --- a/spec/services/bulk_imports/relation_export_service_spec.rb +++ b/spec/services/bulk_imports/relation_export_service_spec.rb @@ -77,7 +77,7 @@ RSpec.describe BulkImports::RelationExportService do it 'tracks exception' do expect(Gitlab::ErrorTracking) .to receive(:track_exception) - .with(exception_class, exportable_id: group.id, exportable_type: group.class.name) + .with(exception_class, portable_id: group.id, portable_type: group.class.name) .and_call_original subject.execute diff --git a/spec/services/packages/rubygems/process_gem_service_spec.rb b/spec/services/packages/rubygems/process_gem_service_spec.rb index 83e868d9579..64deb39c6d8 100644 --- a/spec/services/packages/rubygems/process_gem_service_spec.rb +++ b/spec/services/packages/rubygems/process_gem_service_spec.rb @@ -16,12 +16,11 @@ RSpec.describe Packages::Rubygems::ProcessGemService do describe '#execute' do subject { service.execute } - context 'no gem file', :aggregate_failures do + context 'no gem file' do let(:package_file) { nil } it 'returns an error' do - expect(subject.error?).to be(true) - expect(subject.message).to eq('Gem was not processed') + expect { subject }.to raise_error(::Packages::Rubygems::ProcessGemService::ExtractionError, 'Gem was not processed - package_file is not set') end end diff --git a/spec/workers/packages/nuget/extraction_worker_spec.rb b/spec/workers/packages/nuget/extraction_worker_spec.rb index 4703afc9413..3cc2c79176b 100644 --- a/spec/workers/packages/nuget/extraction_worker_spec.rb +++ b/spec/workers/packages/nuget/extraction_worker_spec.rb @@ -14,14 +14,15 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do subject { described_class.new.perform(package_file_id) } shared_examples 'handling the metadata error' do |exception_class: ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError| - it 'removes the package and the package file' do + it 'updates package status to error', :aggregate_failures do expect(Gitlab::ErrorTracking).to receive(:log_exception).with( instance_of(exception_class), project_id: package.project_id ) - expect { subject } - .to change { Packages::Package.count }.by(-1) - .and change { Packages::PackageFile.count }.by(-1) + + subject + + expect(package.reload).to be_error end end diff --git a/spec/workers/packages/rubygems/extraction_worker_spec.rb b/spec/workers/packages/rubygems/extraction_worker_spec.rb index 15c0a3be90c..6f65dceacf7 100644 --- a/spec/workers/packages/rubygems/extraction_worker_spec.rb +++ b/spec/workers/packages/rubygems/extraction_worker_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Packages::Rubygems::ExtractionWorker, type: :worker do describe '#perform' do - let_it_be(:package) { create(:rubygems_package) } + let_it_be(:package) { create(:rubygems_package, :processing) } let(:package_file) { package.package_files.first } let(:package_file_id) { package_file.id } @@ -14,15 +14,13 @@ RSpec.describe Packages::Rubygems::ExtractionWorker, type: :worker do subject { described_class.new.perform(*job_args) } - include_examples 'an idempotent worker' do - it 'processes the gem', :aggregate_failures do - expect { subject } - .to change { Packages::Package.count }.by(0) - .and change { Packages::PackageFile.count }.by(2) + it 'processes the gem', :aggregate_failures do + expect { subject } + .to change { Packages::Package.count }.by(0) + .and change { Packages::PackageFile.count }.by(1) - expect(Packages::Package.last.id).to be(package.id) - expect(package.name).not_to be(package_name) - end + expect(Packages::Package.last.id).to be(package.id) + expect(package.name).not_to be(package_name) end it 'handles a processing failure', :aggregate_failures do @@ -34,9 +32,9 @@ RSpec.describe Packages::Rubygems::ExtractionWorker, type: :worker do project_id: package.project_id ) - expect { subject } - .to change { Packages::Package.count }.by(-1) - .and change { Packages::PackageFile.count }.by(-2) + subject + + expect(package.reload).to be_error end context 'returns when there is no package file' do |