diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-27 21:07:34 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-27 21:07:34 +0300 |
commit | 7dd130e2cae40514f02b02922251b62302f2fdd5 (patch) | |
tree | fd2f91802a2dd2659b72d3767ea9a00786d7900b /spec | |
parent | 9979d2afd66e12d938ea55372dcf2d8105b5eaca (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
16 files changed, 453 insertions, 400 deletions
diff --git a/spec/components/projects/ml/models_index_component_spec.rb b/spec/components/projects/ml/models_index_component_spec.rb new file mode 100644 index 00000000000..e4599cc5eec --- /dev/null +++ b/spec/components/projects/ml/models_index_component_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Projects::Ml::ModelsIndexComponent, type: :component, feature_category: :mlops do + let_it_be(:project) { build_stubbed(:project) } + let_it_be(:model1) { build_stubbed(:ml_models, :with_latest_version_and_package, project: project) } + let_it_be(:model2) { build_stubbed(:ml_models, project: project) } + let_it_be(:models) { [model1, model2] } + + subject(:component) do + described_class.new(models: models) + end + + describe 'rendered' do + let(:element) { page.find("#js-index-ml-models") } + + before do + render_inline component + end + + it 'renders element with view_model' do + element = page.find("#js-index-ml-models") + + expect(Gitlab::Json.parse(element['data-view-model'])).to eq({ + 'models' => [ + { + 'name' => model1.name, + 'version' => model1.latest_version.version, + 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}" + }, + { + 'name' => model2.name, + 'version' => nil, + 'path' => nil + } + ] + }) + end + end +end diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js index 576263d5418..ca5f80f331c 100644 --- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js @@ -1,19 +1,19 @@ import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { GlButton, GlDrawer, GlModal } from '@gitlab/ui'; +import { GlButton, GlModal } from '@gitlab/ui'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; -import CiEditorHeader from '~/ci/pipeline_editor/components/editor/ci_editor_header.vue'; import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue'; import PipelineEditorDrawer from '~/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue'; import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue'; import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue'; import PipelineEditorFileTree from '~/ci/pipeline_editor/components/file_tree/container.vue'; -import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue'; import PipelineEditorHeader from '~/ci/pipeline_editor/components/header/pipeline_editor_header.vue'; import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue'; import { CREATE_TAB, + EDITOR_APP_DRAWER_HELP, + EDITOR_APP_DRAWER_JOB_ASSISTANT, + EDITOR_APP_DRAWER_NONE, FILE_TREE_DISPLAY_KEY, VALIDATE_TAB, MERGED_TAB, @@ -29,10 +29,9 @@ jest.mock('~/lib/utils/common_utils'); describe('Pipeline editor home wrapper', () => { let wrapper; - const createComponent = ({ props = {}, glFeatures = {}, data = {}, stubs = {} } = {}) => { + const createComponent = ({ props = {}, glFeatures = {}, stubs = {} } = {}) => { wrapper = extendedWrapper( shallowMount(PipelineEditorHome, { - data: () => data, propsData: { ciConfigData: mockLintResponse, ciFileContent: mockCiYml, @@ -53,7 +52,6 @@ describe('Pipeline editor home wrapper', () => { ); }; - const findBranchSwitcher = () => wrapper.findComponent(BranchSwitcher); const findCommitSection = () => wrapper.findComponent(CommitSection); const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav); const findModal = () => wrapper.findComponent(GlModal); @@ -63,8 +61,16 @@ describe('Pipeline editor home wrapper', () => { const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader); const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs); const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle'); - const findHelpBtn = () => wrapper.findByTestId('drawer-toggle'); - const findJobAssistantBtn = () => wrapper.findByTestId('job-assistant-drawer-toggle'); + + const clickHelpBtn = async () => { + await findPipelineEditorDrawer().vm.$emit('switch-drawer', EDITOR_APP_DRAWER_HELP); + }; + const clickJobAssistantBtn = async () => { + await findJobAssistantDrawer().vm.$emit('switch-drawer', EDITOR_APP_DRAWER_JOB_ASSISTANT); + }; + const closeDrawer = async (finder) => { + await finder().vm.$emit('switch-drawer', EDITOR_APP_DRAWER_NONE); + }; afterEach(() => { localStorage.clear(); @@ -103,11 +109,9 @@ describe('Pipeline editor home wrapper', () => { }); }); describe('when `showSwitchBranchModal` value is true', () => { - beforeEach(() => { - createComponent({ - data: { showSwitchBranchModal: true }, - stubs: { PipelineEditorFileNav }, - }); + beforeEach(async () => { + createComponent(); + await findFileNav().vm.$emit('select-branch'); }); it('is visible', () => { @@ -115,11 +119,11 @@ describe('Pipeline editor home wrapper', () => { }); it('pass down `shouldLoadNewBranch` to the branch switcher when primary is selected', async () => { - expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(false); + expect(findFileNav().props('shouldLoadNewBranch')).toBe(false); await findModal().vm.$emit('primary'); - expect(findBranchSwitcher().props('shouldLoadNewBranch')).toBe(true); + expect(findFileNav().props('shouldLoadNewBranch')).toBe(true); }); it('closes the modal when secondary action is selected', async () => { @@ -148,9 +152,7 @@ describe('Pipeline editor home wrapper', () => { async ({ tab, shouldShow }) => { expect(findCommitSection().exists()).toBe(true); - findPipelineEditorTabs().vm.$emit('set-current-tab', tab); - - await nextTick(); + await findPipelineEditorTabs().vm.$emit('set-current-tab', tab); expect(findCommitSection().isVisible()).toBe(shouldShow); }, @@ -159,12 +161,10 @@ describe('Pipeline editor home wrapper', () => { it('shows the commit form again when coming back to the create tab', async () => { expect(findCommitSection().isVisible()).toBe(true); - findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB); - await nextTick(); + await findPipelineEditorTabs().vm.$emit('set-current-tab', MERGED_TAB); expect(findCommitSection().isVisible()).toBe(false); - findPipelineEditorTabs().vm.$emit('set-current-tab', CREATE_TAB); - await nextTick(); + await findPipelineEditorTabs().vm.$emit('set-current-tab', CREATE_TAB); expect(findCommitSection().isVisible()).toBe(true); }); @@ -195,7 +195,9 @@ describe('Pipeline editor home wrapper', () => { describe('when "walkthrough-popover-cta-clicked" is emitted from pipeline editor tabs', () => { it('passes down `scrollToCommitForm=true` to commit section', async () => { expect(findCommitSection().props('scrollToCommitForm')).toBe(false); + await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked'); + expect(findCommitSection().props('scrollToCommitForm')).toBe(true); }); }); @@ -204,6 +206,7 @@ describe('Pipeline editor home wrapper', () => { it('passes down `scrollToCommitForm=false` to commit section', async () => { await findPipelineEditorTabs().vm.$emit('walkthrough-popover-cta-clicked'); expect(findCommitSection().props('scrollToCommitForm')).toBe(true); + await findCommitSection().vm.$emit('scrolled-to-commit-form'); expect(findCommitSection().props('scrollToCommitForm')).toBe(false); }); @@ -211,133 +214,49 @@ describe('Pipeline editor home wrapper', () => { }); describe('help drawer', () => { - const clickHelpBtn = async () => { - findHelpBtn().vm.$emit('click'); - await nextTick(); - }; - - it('hides the drawer by default', () => { + beforeEach(() => { createComponent(); + }); + it('hides the drawer by default', () => { expect(findPipelineEditorDrawer().props('isVisible')).toBe(false); }); it('toggles the drawer on button click', async () => { - createComponent({ - stubs: { - CiEditorHeader, - GlButton, - GlDrawer, - PipelineEditorTabs, - PipelineEditorDrawer, - }, - }); - - await clickHelpBtn(); - - expect(findPipelineEditorDrawer().props('isVisible')).toBe(true); - - await clickHelpBtn(); - expect(findPipelineEditorDrawer().props('isVisible')).toBe(false); - }); - - it("closes the drawer through the drawer's close button", async () => { - createComponent({ - stubs: { - CiEditorHeader, - GlButton, - GlDrawer, - PipelineEditorTabs, - PipelineEditorDrawer, - }, - }); await clickHelpBtn(); - expect(findPipelineEditorDrawer().props('isVisible')).toBe(true); - findPipelineEditorDrawer().findComponent(GlDrawer).vm.$emit('close'); - await nextTick(); - + await closeDrawer(findPipelineEditorDrawer); expect(findPipelineEditorDrawer().props('isVisible')).toBe(false); }); }); describe('job assistant drawer', () => { - const clickHelpBtn = async () => { - findHelpBtn().vm.$emit('click'); - await nextTick(); - }; - const clickJobAssistantBtn = async () => { - findJobAssistantBtn().vm.$emit('click'); - await nextTick(); - }; - - const stubs = { - CiEditorHeader, - GlButton, - GlDrawer, - PipelineEditorTabs, - JobAssistantDrawer, - }; - - it('hides the job assistant drawer by default', () => { + beforeEach(() => { createComponent({ glFeatures: { ciJobAssistantDrawer: true, }, }); + }); + it('hides the job assistant drawer by default', () => { expect(findJobAssistantDrawer().props('isVisible')).toBe(false); }); it('toggles the job assistant drawer on button click', async () => { - createComponent({ - stubs, - glFeatures: { - ciJobAssistantDrawer: true, - }, - }); - - await clickJobAssistantBtn(); - - expect(findJobAssistantDrawer().props('isVisible')).toBe(true); - - await clickJobAssistantBtn(); - expect(findJobAssistantDrawer().props('isVisible')).toBe(false); - }); - - it("closes the job assistant drawer through the drawer's close button", async () => { - createComponent({ - stubs, - glFeatures: { - ciJobAssistantDrawer: true, - }, - }); await clickJobAssistantBtn(); - expect(findJobAssistantDrawer().props('isVisible')).toBe(true); - findJobAssistantDrawer().findComponent(GlDrawer).vm.$emit('close'); - await nextTick(); - + await closeDrawer(findJobAssistantDrawer); expect(findJobAssistantDrawer().props('isVisible')).toBe(false); }); it('covers helper drawer when opened last', async () => { - createComponent({ - stubs: { - ...stubs, - PipelineEditorDrawer, - }, - glFeatures: { - ciJobAssistantDrawer: true, - }, - }); - await clickHelpBtn(); await clickJobAssistantBtn(); @@ -348,16 +267,6 @@ describe('Pipeline editor home wrapper', () => { }); it('covered by helper drawer when opened first', async () => { - createComponent({ - stubs: { - ...stubs, - PipelineEditorDrawer, - }, - glFeatures: { - ciJobAssistantDrawer: true, - }, - }); - await clickJobAssistantBtn(); await clickHelpBtn(); @@ -370,8 +279,7 @@ describe('Pipeline editor home wrapper', () => { describe('file tree', () => { const toggleFileTree = async () => { - findFileTreeBtn().vm.$emit('click'); - await nextTick(); + await findFileTreeBtn().vm.$emit('click'); }; describe('button toggle', () => { @@ -412,9 +320,7 @@ describe('Pipeline editor home wrapper', () => { describe('when file tree display state is saved in local storage', () => { beforeEach(() => { localStorage.setItem(FILE_TREE_DISPLAY_KEY, 'true'); - createComponent({ - stubs: { PipelineEditorFileNav }, - }); + createComponent(); }); it('shows the file tree by default', () => { @@ -424,9 +330,7 @@ describe('Pipeline editor home wrapper', () => { describe('when file tree display state is not saved in local storage', () => { beforeEach(() => { - createComponent({ - stubs: { PipelineEditorFileNav }, - }); + createComponent(); }); it('hides the file tree by default', () => { diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js index 1a200090805..88ab51cf135 100644 --- a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js +++ b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js @@ -1,16 +1,24 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import ProjectStorageApp from '~/usage_quotas/storage/components/project_storage_app.vue'; import UsageGraph from '~/usage_quotas/storage/components/usage_graph.vue'; -import { TOTAL_USAGE_DEFAULT_TEXT } from '~/usage_quotas/storage/constants'; +import { + descendingStorageUsageSort, + getStorageTypesFromProjectStatistics, +} from '~/usage_quotas/storage/utils'; +import { + storageTypeHelpPaths, + PROJECT_STORAGE_TYPES, + NAMESPACE_STORAGE_TYPES, + TOTAL_USAGE_DEFAULT_TEXT, +} from '~/usage_quotas/storage/constants'; import getProjectStorageStatistics from '~/usage_quotas/storage/queries/project_storage.query.graphql'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; import { - projectData, mockGetProjectStorageStatisticsGraphQLResponse, mockEmptyResponse, defaultProjectProvideValues, @@ -36,25 +44,26 @@ describe('ProjectStorageApp', () => { }; const createComponent = ({ provide = {}, mockApollo } = {}) => { - wrapper = extendedWrapper( - shallowMount(ProjectStorageApp, { - apolloProvider: mockApollo, - provide: { - ...defaultProjectProvideValues, - ...provide, - }, - }), - ); + wrapper = shallowMountExtended(ProjectStorageApp, { + apolloProvider: mockApollo, + provide: { + ...defaultProjectProvideValues, + ...provide, + }, + }); }; const findAlert = () => wrapper.findComponent(GlAlert); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findUsagePercentage = () => wrapper.findByTestId('total-usage'); - const findUsageQuotasHelpLink = () => wrapper.findByTestId('usage-quotas-help-link'); const findUsageGraph = () => wrapper.findComponent(UsageGraph); + const findProjectDetailsTable = () => wrapper.findByTestId('usage-quotas-project-usage-details'); + const findNamespaceDetailsTable = () => + wrapper.findByTestId('usage-quotas-namespace-usage-details'); describe('with apollo fetching successful', () => { let mockApollo; + const mockProjectData = mockGetProjectStorageStatisticsGraphQLResponse.data.project; beforeEach(async () => { mockApollo = createMockApolloProvider({ @@ -65,13 +74,33 @@ describe('ProjectStorageApp', () => { }); it('renders correct total usage', () => { - expect(findUsagePercentage().text()).toBe(projectData.storage.totalUsage); + const expectedValue = numberToHumanSize( + mockGetProjectStorageStatisticsGraphQLResponse.data.project.statistics.storageSize, + 1, + ); + expect(findUsagePercentage().text()).toBe(expectedValue); + }); + + it('passes project storage entities to project details table', () => { + const expectedValue = getStorageTypesFromProjectStatistics( + PROJECT_STORAGE_TYPES, + mockProjectData.statistics, + mockProjectData.statisticsDetailsPaths, + storageTypeHelpPaths, + ).sort(descendingStorageUsageSort('value')); + + expect(findProjectDetailsTable().props('storageTypes')).toStrictEqual(expectedValue); }); - it('renders correct usage quotas help link', () => { - expect(findUsageQuotasHelpLink().attributes('href')).toBe( - defaultProjectProvideValues.helpLinks.usageQuotas, + it('passes namespace storage entities to namespace details table', () => { + const expectedValue = getStorageTypesFromProjectStatistics( + NAMESPACE_STORAGE_TYPES, + mockProjectData.statistics, + mockProjectData.statisticsDetailsPaths, + storageTypeHelpPaths, ); + + expect(findNamespaceDetailsTable().props('storageTypes')).toStrictEqual(expectedValue); }); }); @@ -104,6 +133,14 @@ describe('ProjectStorageApp', () => { it('shows default text for total usage', () => { expect(findUsagePercentage().text()).toBe(TOTAL_USAGE_DEFAULT_TEXT); }); + + it('passes empty array to project details table', () => { + expect(findProjectDetailsTable().props('storageTypes')).toStrictEqual([]); + }); + + it('passes empty array to namespace details table', () => { + expect(findNamespaceDetailsTable().props('storageTypes')).toStrictEqual([]); + }); }); describe('with apollo fetching error', () => { diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js index 37fc9602315..364517a474f 100644 --- a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js +++ b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js @@ -1,15 +1,36 @@ -import { GlTableLite, GlPopover } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlTableLite } from '@gitlab/ui'; +import { mount, Wrapper } from '@vue/test-utils'; // eslint-disable-line no-unused-vars import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import ProjectStorageDetail from '~/usage_quotas/storage/components/project_storage_detail.vue'; -import { containerRegistryPopoverId, containerRegistryId } from '~/usage_quotas/storage/constants'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { projectData, projectHelpLinks } from '../mock_data'; describe('ProjectStorageDetail', () => { + /** @type { Wrapper } */ let wrapper; - const { storageTypes } = projectData.storage; + const generateStorageType = (props) => { + return { + id: 'id', + name: 'name', + description: 'description', + helpPath: '/help-path', + detailsPath: '/details-link', + value: 42, + ...props, + }; + }; + + const storageTypes = [ + generateStorageType({ id: 'one' }), + generateStorageType({ id: 'two' }), + generateStorageType({ + id: 'three', + warning: { + content: 'warning message', + }, + }), + ]; + const defaultProps = { storageTypes }; const createComponent = (props = {}) => { @@ -26,23 +47,7 @@ describe('ProjectStorageDetail', () => { ); }; - const generateStorageType = (id = 'buildArtifacts') => { - return { - storageType: { - id, - name: 'Test Name', - description: 'Test Description', - helpPath: '/test-type', - }, - value: 400000, - }; - }; - const findTable = () => wrapper.findComponent(GlTableLite); - const findPopoverById = (id) => - wrapper.findAllComponents(GlPopover).filter((p) => p.attributes('data-testid') === id); - const findContainerRegistryPopover = () => findPopoverById(containerRegistryPopoverId); - const findContainerRegistryWarningIcon = () => wrapper.find(`#${containerRegistryPopoverId}`); beforeEach(() => { createComponent(); @@ -51,33 +56,23 @@ describe('ProjectStorageDetail', () => { describe('with storage types', () => { it.each(storageTypes)( 'renders table row correctly %o', - ({ storageType: { id, name, description } }) => { + ({ id, name, value, description, helpPath, warning }) => { expect(wrapper.findByTestId(`${id}-name`).text()).toBe(name); expect(wrapper.findByTestId(`${id}-description`).text()).toBe(description); expect(wrapper.findByTestId(`${id}-icon`).props('name')).toBe(id); - expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe( - projectHelpLinks[id], - ); + expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe(helpPath); + expect(wrapper.findByTestId(`${id}-value`).text()).toContain(numberToHumanSize(value, 1)); + + expect(wrapper.findByTestId(`${id}-warning-icon`).exists()).toBe(Boolean(warning)); + expect(wrapper.findByTestId(`${id}-popover`).exists()).toBe(Boolean(warning)); }, ); - - it('should render items in order from the biggest usage size to the smallest', () => { - const rows = findTable().find('tbody').findAll('tr'); - // Cloning array not to mutate the source - const sortedStorageTypes = [...storageTypes].sort((a, b) => b.value - a.value); - - sortedStorageTypes.forEach((storageType, i) => { - const rowUsageAmount = rows.wrappers[i].find('td:last-child').text(); - const expectedUsageAmount = numberToHumanSize(storageType.value, 1); - expect(rowUsageAmount).toBe(expectedUsageAmount); - }); - }); }); describe('with details links', () => { it.each(storageTypes)('each $storageType.id', (item) => { - const shouldExist = Boolean(item.storageType.detailsPath && item.value); - const detailsLink = wrapper.findByTestId(`${item.storageType.id}-details-link`); + const shouldExist = Boolean(item.detailsPath && item.value); + const detailsLink = wrapper.findByTestId(`${item.id}-details-link`); expect(detailsLink.exists()).toBe(shouldExist); }); }); @@ -95,21 +90,4 @@ describe('ProjectStorageDetail', () => { expect(findTable().find('td').exists()).toBe(false); }); }); - - describe.each` - description | mockStorageTypes | rendersContainerRegistryPopover - ${'without any storage type that has popover'} | ${[generateStorageType()]} | ${false} - ${'with container registry storage type'} | ${[generateStorageType(containerRegistryId)]} | ${true} - `('$description', ({ mockStorageTypes, rendersContainerRegistryPopover }) => { - beforeEach(() => { - createComponent({ storageTypes: mockStorageTypes }); - }); - - it(`does ${ - rendersContainerRegistryPopover ? '' : ' not' - } render container registry warning icon and popover`, () => { - expect(findContainerRegistryWarningIcon().exists()).toBe(rendersContainerRegistryPopover); - expect(findContainerRegistryPopover().exists()).toBe(rendersContainerRegistryPopover); - }); - }); }); diff --git a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js index 7fef20c900e..fc116211bf0 100644 --- a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js +++ b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js @@ -44,7 +44,6 @@ describe('UsageGraph', () => { buildArtifactsSize, lfsObjectsSize, packagesSize, - containerRegistrySize, repositorySize, wikiSize, snippetsSize, @@ -57,14 +56,11 @@ describe('UsageGraph', () => { expect(types.at(2).text()).toMatchInterpolatedText( `Packages ${numberToHumanSize(packagesSize)}`, ); - expect(types.at(3).text()).toMatchInterpolatedText( - `Container Registry ${numberToHumanSize(containerRegistrySize)}`, - ); - expect(types.at(4).text()).toMatchInterpolatedText(`LFS ${numberToHumanSize(lfsObjectsSize)}`); - expect(types.at(5).text()).toMatchInterpolatedText( + expect(types.at(3).text()).toMatchInterpolatedText(`LFS ${numberToHumanSize(lfsObjectsSize)}`); + expect(types.at(4).text()).toMatchInterpolatedText( `Snippets ${numberToHumanSize(snippetsSize)}`, ); - expect(types.at(6).text()).toMatchInterpolatedText( + expect(types.at(5).text()).toMatchInterpolatedText( `Job artifacts ${numberToHumanSize(buildArtifactsSize)}`, ); }); @@ -102,7 +98,6 @@ describe('UsageGraph', () => { '0.29411764705882354', '0.23529411764705882', '0.17647058823529413', - '0.14705882352941177', '0.11764705882352941', '0.11764705882352941', '0.041176470588235294', @@ -121,7 +116,6 @@ describe('UsageGraph', () => { '0.29411764705882354', '0.23529411764705882', '0.17647058823529413', - '0.14705882352941177', '0.11764705882352941', '0.11764705882352941', '0.041176470588235294', diff --git a/spec/frontend/usage_quotas/storage/mock_data.js b/spec/frontend/usage_quotas/storage/mock_data.js index 452fa83b9a7..16c03a13028 100644 --- a/spec/frontend/usage_quotas/storage/mock_data.js +++ b/spec/frontend/usage_quotas/storage/mock_data.js @@ -3,95 +3,6 @@ import mockGetProjectStorageStatisticsGraphQLResponse from 'test_fixtures/graphq export { mockGetProjectStorageStatisticsGraphQLResponse }; export const mockEmptyResponse = { data: { project: null } }; -export const projectData = { - storage: { - totalUsage: '13.4 MiB', - storageTypes: [ - { - storageType: { - id: 'containerRegistry', - name: 'Container Registry', - description: 'Gitlab-integrated Docker Container Registry for storing Docker Images.', - helpPath: '/container_registry', - detailsPath: 'http://localhost/frontend-fixtures/builds-project/container_registry', - }, - value: 3900000, - }, - { - storageType: { - id: 'buildArtifacts', - name: 'Job artifacts', - description: 'Job artifacts created by CI/CD.', - helpPath: '/build-artifacts', - detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/artifacts', - }, - value: 400000, - }, - { - storageType: { - id: 'lfsObjects', - name: 'LFS', - description: 'Audio samples, videos, datasets, and graphics.', - helpPath: '/lsf-objects', - }, - value: 4800000, - }, - { - storageType: { - id: 'packages', - name: 'Packages', - description: 'Code packages and container images.', - helpPath: '/packages', - detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/packages', - }, - value: 3800000, - }, - { - storageType: { - id: 'repository', - name: 'Repository', - description: 'Git repository.', - helpPath: '/repository', - detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/tree/master', - }, - value: 3900000, - }, - { - storageType: { - id: 'snippets', - name: 'Snippets', - description: 'Shared bits of code and text.', - helpPath: '/snippets', - detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/snippets', - }, - value: 0, - }, - { - storageType: { - id: 'wiki', - name: 'Wiki', - description: 'Wiki content.', - helpPath: '/wiki', - detailsPath: 'http://localhost/frontend-fixtures/builds-project/-/wikis/pages', - }, - value: 300000, - }, - ], - }, -}; - -export const projectHelpLinks = { - containerRegistry: '/container_registry', - usageQuotas: '/usage-quotas', - buildArtifacts: '/build-artifacts', - lfsObjects: '/lsf-objects', - packages: '/packages', - repository: '/repository', - snippets: '/snippets', - wiki: '/wiki', -}; - export const defaultProjectProvideValues = { projectPath: '/project-path', - helpLinks: projectHelpLinks, }; diff --git a/spec/frontend/usage_quotas/storage/utils_spec.js b/spec/frontend/usage_quotas/storage/utils_spec.js index e3a271adc57..dd05e105c26 100644 --- a/spec/frontend/usage_quotas/storage/utils_spec.js +++ b/spec/frontend/usage_quotas/storage/utils_spec.js @@ -1,15 +1,9 @@ -import cloneDeep from 'lodash/cloneDeep'; import { PROJECT_STORAGE_TYPES } from '~/usage_quotas/storage/constants'; import { - parseGetProjectStorageResults, getStorageTypesFromProjectStatistics, descendingStorageUsageSort, } from '~/usage_quotas/storage/utils'; -import { - mockGetProjectStorageStatisticsGraphQLResponse, - defaultProjectProvideValues, - projectData, -} from './mock_data'; +import { mockGetProjectStorageStatisticsGraphQLResponse } from './mock_data'; describe('getStorageTypesFromProjectStatistics', () => { const { @@ -18,15 +12,18 @@ describe('getStorageTypesFromProjectStatistics', () => { } = mockGetProjectStorageStatisticsGraphQLResponse.data.project; describe('matches project statistics value with matching storage type', () => { - const typesWithStats = getStorageTypesFromProjectStatistics(projectStatistics); + const typesWithStats = getStorageTypesFromProjectStatistics( + PROJECT_STORAGE_TYPES, + projectStatistics, + ); it.each(PROJECT_STORAGE_TYPES)('storage type: $id', ({ id }) => { - expect(typesWithStats).toContainEqual({ - storageType: expect.objectContaining({ + expect(typesWithStats).toContainEqual( + expect.objectContaining({ id, + value: projectStatistics[`${id}Size`], }), - value: projectStatistics[`${id}Size`], - }); + ); }); }); @@ -38,57 +35,31 @@ describe('getStorageTypesFromProjectStatistics', () => { }; }, {}); - const typesWithStats = getStorageTypesFromProjectStatistics(projectStatistics, helpLinks); + const typesWithStats = getStorageTypesFromProjectStatistics( + PROJECT_STORAGE_TYPES, + projectStatistics, + {}, + helpLinks, + ); typesWithStats.forEach((type) => { - const key = type.storageType.id; - expect(type.storageType.helpPath).toBe(helpLinks[key]); + expect(type.helpPath).toBe(helpLinks[type.id]); }); }); it('adds details page path', () => { const typesWithStats = getStorageTypesFromProjectStatistics( + PROJECT_STORAGE_TYPES, projectStatistics, - {}, statisticsDetailsPaths, + {}, ); typesWithStats.forEach((type) => { - expect(type.storageType.detailsPath).toBe(statisticsDetailsPaths[type.storageType.id]); + expect(type.detailsPath).toBe(statisticsDetailsPaths[type.id]); }); }); }); -describe('parseGetProjectStorageResults', () => { - it('parses project statistics correctly', () => { - expect( - parseGetProjectStorageResults( - mockGetProjectStorageStatisticsGraphQLResponse.data, - defaultProjectProvideValues.helpLinks, - ), - ).toMatchObject(projectData); - }); - - it('includes storage type with size of 0 in returned value', () => { - const mockedResponse = cloneDeep(mockGetProjectStorageStatisticsGraphQLResponse.data); - // ensuring a specific storage type item has size of 0 - mockedResponse.project.statistics.repositorySize = 0; - - const response = parseGetProjectStorageResults( - mockedResponse, - defaultProjectProvideValues.helpLinks, - ); - - expect(response.storage.storageTypes).toEqual( - expect.arrayContaining([ - { - storageType: expect.any(Object), - value: 0, - }, - ]), - ); - }); -}); - describe('descendingStorageUsageSort', () => { it('sorts items by a given key in descending order', () => { const items = [{ k: 1 }, { k: 3 }, { k: 2 }]; diff --git a/spec/lib/gitlab/audit/auditor_spec.rb b/spec/lib/gitlab/audit/auditor_spec.rb index 386d4157e90..1a45235a4e7 100644 --- a/spec/lib/gitlab/audit/auditor_spec.rb +++ b/spec/lib/gitlab/audit/auditor_spec.rb @@ -25,22 +25,48 @@ RSpec.describe Gitlab::Audit::Auditor, feature_category: :audit_events do describe '.audit' do let(:audit!) { auditor.audit(context) } + before do + allow(Gitlab::Audit::Type::Definition).to receive(:defined?).and_call_original + allow(Gitlab::Audit::Type::Definition).to receive(:defined?).with(name).and_return(true) + end + context 'when yaml definition is not defined' do before do - allow(Gitlab::Audit::Type::Definition).to receive(:defined?).and_return(false) + allow(Gitlab::Audit::Type::Definition).to receive(:defined?).and_call_original + allow(Gitlab::Audit::Type::Definition).to receive(:defined?).with(name).and_return(false) allow(Gitlab::AppLogger).to receive(:warn).and_return(app_logger) end - it 'logs a warning when YAML is not defined' do - expected_warning = { - message: 'Logging audit events without an event type definition will be deprecated soon ' \ - '(https://docs.gitlab.com/ee/development/audit_event_guide/#event-type-definitions)', - event_type: name - } + context 'when feature flag raise_error_for_missing_audit_event_yml is enabled' do + before do + stub_feature_flags(raise_error_for_missing_audit_event_yml: true) + end - audit! + it 'raises an error' do + expected_error = "Audit event type YML file is not defined for audit_operation. " \ + "Please read https://docs.gitlab.com/ee/development/audit_event_guide/" \ + "#how-to-instrument-new-audit-events for adding a new audit event" + + expect { audit! }.to raise_error(StandardError, expected_error) + end + end - expect(Gitlab::AppLogger).to have_received(:warn).with(expected_warning) + context 'when feature flag raise_error_for_missing_audit_event_yml is disabled' do + before do + stub_feature_flags(raise_error_for_missing_audit_event_yml: false) + end + + it 'logs a warning when YAML is not defined' do + expected_warning = { + message: 'Logging audit events without an event type definition will be deprecated soon ' \ + '(https://docs.gitlab.com/ee/development/audit_event_guide/#event-type-definitions)', + event_type: name + } + + audit! + + expect(Gitlab::AppLogger).to have_received(:warn).with(expected_warning) + end end end diff --git a/spec/lib/gitlab/ci/interpolation/block_spec.rb b/spec/lib/gitlab/ci/interpolation/block_spec.rb index 4a8709df3dc..e4ccfbdfd63 100644 --- a/spec/lib/gitlab/ci/interpolation/block_spec.rb +++ b/spec/lib/gitlab/ci/interpolation/block_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_com end let(:ctx) do - { inputs: { data: 'abc' }, env: { 'ENV' => 'dev' } } + { inputs: { data: 'abcdef' }, env: { 'ENV' => 'dev' } } end it 'knows its content' do @@ -22,7 +22,7 @@ RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_com end it 'properly evaluates the access pattern' do - expect(subject.value).to eq 'abc' + expect(subject.value).to eq 'abcdef' end describe '.match' do @@ -35,5 +35,78 @@ RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_com expect { |b| described_class.match('$[[]]', &b) } .to yield_with_args('$[[]]', '') end + + context 'when functions are specified in the block' do + it 'matches each block in a string' do + expect { |b| described_class.match('$[[ access1 | func1 ]] $[[ access2 | func1 | func2(0,1) ]]', &b) } + .to yield_successive_args(['$[[ access1 | func1 ]]', 'access1 | func1'], + ['$[[ access2 | func1 | func2(0,1) ]]', 'access2 | func1 | func2(0,1)']) + end + end + end + + describe 'when functions are specified in the block' do + let(:function_string1) { 'truncate(1,5)' } + let(:data) { "inputs.data | #{function_string1}" } + let(:access_value) { 'abcdef' } + + it 'returns the modified value' do + expect(subject).to be_valid + expect(subject.value).to eq('bcdef') + end + + context 'when there is an access error' do + let(:data) { "inputs.undefined | #{function_string1}" } + + it 'returns the access error' do + expect(subject).not_to be_valid + expect(subject.errors.first).to eq('unknown interpolation key: `undefined`') + end + end + + context 'when there is a function error' do + let(:data) { 'inputs.data | undefined' } + + it 'returns the function error' do + expect(subject).not_to be_valid + expect(subject.errors.first).to match(/no function matching `undefined`/) + end + end + + context 'when multiple functions are specified' do + let(:function_string2) { 'truncate(2,2)' } + let(:data) { "inputs.data | #{function_string1} | #{function_string2}" } + + it 'executes each function in the specified order' do + expect(subject.value).to eq('de') + end + + context 'when the data has inconsistent spacing' do + let(:data) { "inputs.data|#{function_string1} | #{function_string2} " } + + it 'executes each function in the specified order' do + expect(subject.value).to eq('de') + end + end + + context 'when a stack of functions errors in the middle' do + let(:function_string2) { 'truncate(2)' } + + it 'does not modify the value' do + expect(subject).not_to be_valid + expect(subject.errors.first).to match(/no function matching `truncate\(2\)`/) + expect(subject.instance_variable_get(:@value)).to be_nil + end + end + + context 'when too many functions are specified' do + it 'returns error' do + stub_const('Gitlab::Ci::Interpolation::Block::MAX_FUNCTIONS', 1) + + expect(subject).not_to be_valid + expect(subject.errors.first).to eq('too many functions in interpolation block') + end + end + end end end diff --git a/spec/lib/gitlab/ci/interpolation/functions/base_spec.rb b/spec/lib/gitlab/ci/interpolation/functions/base_spec.rb new file mode 100644 index 00000000000..1a15938b988 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/functions/base_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Functions::Base, feature_category: :pipeline_composition do + let(:custom_function_klass) do + Class.new(described_class) do + def self.function_expression_pattern + /.*/ + end + + def self.name + 'test_function' + end + end + end + + it 'defines an expected interface for child classes' do + expect { described_class.function_expression_pattern }.to raise_error(NotImplementedError) + expect { described_class.name }.to raise_error(NotImplementedError) + expect { custom_function_klass.new('test').execute('input') }.to raise_error(NotImplementedError) + end +end diff --git a/spec/lib/gitlab/ci/interpolation/functions/truncate_spec.rb b/spec/lib/gitlab/ci/interpolation/functions/truncate_spec.rb new file mode 100644 index 00000000000..69ce30c51ec --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/functions/truncate_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::Functions::Truncate, feature_category: :pipeline_composition do + it 'matches exactly the truncate function with 2 numeric arguments' do + expect(described_class.matches?('truncate(1,2)')).to be_truthy + expect(described_class.matches?('truncate( 11 , 222 )')).to be_truthy + expect(described_class.matches?('truncate( string , 222 )')).to be_falsey + expect(described_class.matches?('truncate(222)')).to be_falsey + expect(described_class.matches?('unknown(1,2)')).to be_falsey + end + + it 'truncates the given input' do + function = described_class.new('truncate(1,2)') + + output = function.execute('test') + + expect(function).to be_valid + expect(output).to eq('es') + end + + context 'when given a non-string input' do + it 'returns an error' do + function = described_class.new('truncate(1,2)') + + function.execute(100) + + expect(function).not_to be_valid + expect(function.errors).to contain_exactly( + 'error in `truncate` function: invalid input type: truncate can only be used with string inputs' + ) + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/functions_stack_spec.rb b/spec/lib/gitlab/ci/interpolation/functions_stack_spec.rb new file mode 100644 index 00000000000..6e31d23f764 --- /dev/null +++ b/spec/lib/gitlab/ci/interpolation/functions_stack_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Interpolation::FunctionsStack, feature_category: :pipeline_composition do + let(:functions) { ['truncate(0,4)', 'truncate(1,2)'] } + let(:input_value) { 'test_input_value' } + let(:function_klass) { Gitlab::Ci::Interpolation::Functions::Truncate } + + subject { described_class.new(functions).evaluate(input_value) } + + it 'modifies the given input value according to the function expressions' do + expect(subject).to be_success + expect(subject.value).to eq('es') + end + + context 'when applying a function fails' do + let(:input_value) { 666 } + + it 'returns the error given by the failure' do + expect(subject).not_to be_success + expect(subject.errors).to contain_exactly( + 'error in `truncate` function: invalid input type: truncate can only be used with string inputs' + ) + end + end + + context 'when function expressions do not match any function' do + let(:functions) { ['truncate(0)', 'unknown'] } + + it 'returns an error' do + expect(subject).not_to be_success + expect(subject.errors).to contain_exactly( + 'no function matching `truncate(0)`: check that the function name, arguments, and types are correct', + 'no function matching `unknown`: check that the function name, arguments, and types are correct' + ) + end + end +end diff --git a/spec/presenters/ml/model_presenter_spec.rb b/spec/presenters/ml/model_presenter_spec.rb new file mode 100644 index 00000000000..dbbd3b57033 --- /dev/null +++ b/spec/presenters/ml/model_presenter_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ml::ModelPresenter, feature_category: :mlops do + let_it_be(:project) { build_stubbed(:project) } + let_it_be(:model1) { build_stubbed(:ml_models, project: project) } + let_it_be(:model2) { build_stubbed(:ml_models, :with_latest_version_and_package, project: project) } + + describe '#latest_version_name' do + subject { model.present.latest_version_name } + + context 'when model has version' do + let(:model) { model2 } + + it 'is the version of latest_version' do + is_expected.to eq(model2.latest_version.version) + end + end + + context 'when model has no versions' do + let(:model) { model1 } + + it { is_expected.to be_nil } + end + end + + describe '#latest_package_path' do + subject { model.present.latest_package_path } + + context 'when model version does not have package' do + let(:model) { model1 } + + it { is_expected.to be_nil } + end + + context 'when latest model version has package' do + let(:model) { model2 } + + it { is_expected.to eq("/#{project.full_path}/-/packages/#{model.latest_version.package_id}") } + end + end +end diff --git a/spec/presenters/ml/models_index_presenter_spec.rb b/spec/presenters/ml/models_index_presenter_spec.rb deleted file mode 100644 index 549700cdd84..00000000000 --- a/spec/presenters/ml/models_index_presenter_spec.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Ml::ModelsIndexPresenter, feature_category: :mlops do - let_it_be(:project) { build_stubbed(:project) } - let_it_be(:model1) { build_stubbed(:ml_models, :with_latest_version_and_package, project: project) } - let_it_be(:model2) { build_stubbed(:ml_models, project: project) } - let_it_be(:models) do - [model1, model2] - end - - describe '#execute' do - subject { Gitlab::Json.parse(described_class.new(models).present)['models'] } - - it 'presents models correctly' do - expected_models = [ - { - 'name' => model1.name, - 'version' => model1.latest_version.version, - 'path' => "/#{project.full_path}/-/packages/#{model1.latest_version.package_id}" - }, - { - 'name' => model2.name, - 'version' => nil, - 'path' => nil - } - ] - - is_expected.to match_array(expected_models) - end - end -end diff --git a/spec/requests/api/graphql/mutations/work_items/create_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_spec.rb index fca3c84e534..6055390c8d5 100644 --- a/spec/requests/api/graphql/mutations/work_items/create_spec.rb +++ b/spec/requests/api/graphql/mutations/work_items/create_spec.rb @@ -272,6 +272,16 @@ RSpec.describe 'Create a work item', feature_category: :team_planning do let(:mutation) { graphql_mutation(:workItemCreate, input.merge(namespacePath: group.full_path), fields) } it_behaves_like 'creates work item' + + context 'when the namespace_level_work_items feature flag is disabled' do + before do + stub_feature_flags(namespace_level_work_items: false) + end + + it_behaves_like 'a mutation that returns top-level errors', errors: [ + Mutations::WorkItems::Create::DISABLED_FF_ERROR + ] + end end context 'when both projectPath and namespacePath are passed' do diff --git a/spec/requests/projects/ml/models_controller_spec.rb b/spec/requests/projects/ml/models_controller_spec.rb index b57f5db9b68..8569f2396d3 100644 --- a/spec/requests/projects/ml/models_controller_spec.rb +++ b/spec/requests/projects/ml/models_controller_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do let_it_be(:user) { project.first_owner } let_it_be(:model1) { create(:ml_models, :with_versions, project: project) } let_it_be(:model2) { create(:ml_models, project: project) } + let_it_be(:model_in_different_project) { create(:ml_models) } let(:model_registry_enabled) { true } @@ -35,10 +36,10 @@ RSpec.describe Projects::Ml::ModelsController, feature_category: :mlops do index_request end - it 'prepares model view using the presenter' do - expect(::Ml::ModelsIndexPresenter).to receive(:new).and_call_original - + it 'fetches the correct models' do index_request + + expect(assigns(:models)).to match_array([model1, model2]) end it 'does not perform N+1 sql queries' do |