diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-23 15:09:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-23 15:09:52 +0300 |
commit | ecf1ffc19875a94c9de675b0559adc408b202515 (patch) | |
tree | 92f76d0b7cbd9161eb4dff35ca4753f45f4bc6d2 /spec | |
parent | 65f7976d0cd11d91a4c0945b2c63a1aa2f888b07 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
55 files changed, 989 insertions, 545 deletions
diff --git a/spec/controllers/admin/cohorts_controller_spec.rb b/spec/controllers/admin/cohorts_controller_spec.rb index 77a9c8eb223..ba5406f25ab 100644 --- a/spec/controllers/admin/cohorts_controller_spec.rb +++ b/spec/controllers/admin/cohorts_controller_spec.rb @@ -12,6 +12,6 @@ RSpec.describe Admin::CohortsController do it 'redirects to Overview->Users' do get :index - expect(response).to redirect_to(admin_users_path(tab: 'cohorts')) + expect(response).to redirect_to(cohorts_admin_users_path) end end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 6d64ef86452..1afd20f5021 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -30,11 +30,6 @@ RSpec.describe Admin::UsersController do expect(assigns(:users).first.association(:authorized_projects)).to be_loaded end - it_behaves_like 'tracking unique visits', :index do - let(:target_id) { 'i_analytics_cohorts' } - let(:request_params) { { tab: 'cohorts' } } - end - context 'pagination' do context 'when number of users is over the pagination limit' do before do @@ -59,6 +54,12 @@ RSpec.describe Admin::UsersController do end end + describe 'GET #cohorts' do + it_behaves_like 'tracking unique visits', :cohorts do + let(:target_id) { 'i_analytics_cohorts' } + end + end + describe 'GET :id' do it 'finds a user case-insensitively' do user = create(:user, username: 'CaseSensitive') diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index 753223c5a4f..6236a47cde1 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -1247,4 +1247,59 @@ RSpec.describe Projects::PipelinesController do format: :json end end + + describe 'GET downloadable_artifacts.json' do + context 'when pipeline is empty' do + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'returns status not_found' do + get_downloadable_artifacts_json + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when pipeline exists' do + context 'when pipeline does not have any downloadable artifacts' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + it 'returns an empty array' do + get_downloadable_artifacts_json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['artifacts']).to be_empty + end + end + + context 'when pipeline has downloadable artifacts' do + let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) } + + before do + create(:ci_build, name: 'rspec', pipeline: pipeline).tap do |build| + create(:ci_job_artifact, :junit, job: build) + end + end + + it 'returns an array of artifacts' do + get_downloadable_artifacts_json + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['artifacts']).to be_kind_of(Array) + expect(json_response['artifacts'].size).to eq(2) + end + end + end + + private + + def get_downloadable_artifacts_json + get :downloadable_artifacts, + params: { + namespace_id: project.namespace, + project_id: project, + id: pipeline.id + }, + format: :json + end + end end diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index e4b783a81fa..6d5944002a1 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -10,36 +10,39 @@ RSpec.describe "Admin::Users" do gitlab_enable_admin_mode_sign_in(current_user) end - describe 'Tabs', :js do + describe 'Tabs' do let(:tabs_selector) { '.js-users-tabs' } let(:active_tab_selector) { '.nav-link.active' } - it 'does not add the tab param when the Users tab is selected' do - visit admin_users_path + it 'links to the Users tab' do + visit cohorts_admin_users_path within tabs_selector do click_link 'Users' + + expect(page).to have_selector active_tab_selector, text: 'Users' end expect(page).to have_current_path(admin_users_path) end - it 'adds the ?tab=cohorts param when the Cohorts tab is selected' do + it 'links to the Cohorts tab' do visit admin_users_path within tabs_selector do click_link 'Cohorts' + + expect(page).to have_selector active_tab_selector, text: 'Cohorts' end - expect(page).to have_current_path(admin_users_path(tab: 'cohorts')) + expect(page).to have_current_path(cohorts_admin_users_path) + expect(page).to have_selector active_tab_selector, text: 'Cohorts' end - it 'shows the cohorts tab when the tab param is set' do + it 'redirects legacy route' do visit admin_users_path(tab: 'cohorts') - within tabs_selector do - expect(page).to have_selector active_tab_selector, text: 'Cohorts' - end + expect(page).to have_current_path(cohorts_admin_users_path) end end diff --git a/spec/fixtures/api/schemas/entities/downloadable_artifact.json b/spec/fixtures/api/schemas/entities/downloadable_artifact.json new file mode 100644 index 00000000000..01f355f8b55 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/downloadable_artifact.json @@ -0,0 +1,19 @@ +{ + "type": "object", + "required": ["artifacts"], + "properties": { + "artifacts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "expire_at": { "type": ["string", "null"], "format": "date-time" }, + "expired": { "type": "boolean" }, + "path": { "type": "string" } + } + } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/public_api/v4/environment.json b/spec/fixtures/api/schemas/public_api/v4/environment.json index 57352017f03..dfb6e78cbda 100644 --- a/spec/fixtures/api/schemas/public_api/v4/environment.json +++ b/spec/fixtures/api/schemas/public_api/v4/environment.json @@ -18,7 +18,8 @@ { "$ref": "deployment.json" } ] }, - "state": { "type": "string" } + "state": { "type": "string" }, + "enable_advanced_logs_querying": { "type": "boolean" } }, "additionalProperties": false } diff --git a/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event.yml b/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event.yml new file mode 100644 index 00000000000..704e94a04d8 --- /dev/null +++ b/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event.yml @@ -0,0 +1,25 @@ +--- +description: +category: Groups::EmailCampaignsController +action: click +label_description: +property_description: +value_description: +extra_properties: +identifiers: +#- project +#- user +#- namespace +product_section: +product_stage: +product_group: +product_category: +milestone: "13.11" +introduced_by_url: +distributions: +- ce +- ee +tiers: +- free +- premium +- ultimate diff --git a/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event_ee.yml b/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event_ee.yml new file mode 100644 index 00000000000..b20bb9702d2 --- /dev/null +++ b/spec/fixtures/lib/generators/gitlab/snowplow_event_definition_generator/sample_event_ee.yml @@ -0,0 +1,23 @@ +--- +description: +category: Groups::EmailCampaignsController +action: click +label_description: +property_description: +value_description: +extra_properties: +identifiers: +#- project +#- user +#- namespace +product_section: +product_stage: +product_group: +product_category: +milestone: "13.11" +introduced_by_url: +distributions: +- ee +tiers: +#- premium +- ultimate diff --git a/spec/frontend/admin/users/tabs_spec.js b/spec/frontend/admin/users/tabs_spec.js deleted file mode 100644 index 39ba8618486..00000000000 --- a/spec/frontend/admin/users/tabs_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import initTabs from '~/admin/users/tabs'; -import Api from '~/api'; - -jest.mock('~/api.js'); -jest.mock('~/lib/utils/common_utils'); - -describe('tabs', () => { - beforeEach(() => { - setFixtures(` - <div> - <div class="js-users-tab-item"> - <a href="#users" data-testid='users-tab'>Users</a> - </div> - <div class="js-users-tab-item"> - <a href="#cohorts" data-testid='cohorts-tab'>Cohorts</a> - </div> - </div`); - - initTabs(); - }); - - afterEach(() => {}); - - describe('tracking', () => { - it('tracks event when cohorts tab is clicked', () => { - document.querySelector('[data-testid="cohorts-tab"]').click(); - - expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith('i_analytics_cohorts'); - }); - - it('does not track an event when users tab is clicked', () => { - document.querySelector('[data-testid="users-tab"]').click(); - - expect(Api.trackRedisHllUserEvent).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/experimentation/utils_spec.js b/spec/frontend/experimentation/utils_spec.js index ec09bbab349..2ba8c65a252 100644 --- a/spec/frontend/experimentation/utils_spec.js +++ b/spec/frontend/experimentation/utils_spec.js @@ -1,5 +1,9 @@ import { assignGitlabExperiment } from 'helpers/experimentation_helper'; -import { DEFAULT_VARIANT, CANDIDATE_VARIANT } from '~/experimentation/constants'; +import { + DEFAULT_VARIANT, + CANDIDATE_VARIANT, + TRACKING_CONTEXT_SCHEMA, +} from '~/experimentation/constants'; import * as experimentUtils from '~/experimentation/utils'; describe('experiment Utilities', () => { @@ -19,6 +23,20 @@ describe('experiment Utilities', () => { }); }); + describe('getExperimentContexts', () => { + describe.each` + gon | input | output + ${[TEST_KEY, '_data_']} | ${[TEST_KEY]} | ${[{ schema: TRACKING_CONTEXT_SCHEMA, data: { variant: '_data_' } }]} + ${[]} | ${[TEST_KEY]} | ${[]} + `('with input=$input and gon=$gon', ({ gon, input, output }) => { + assignGitlabExperiment(...gon); + + it(`returns ${output}`, () => { + expect(experimentUtils.getExperimentContexts(...input)).toEqual(output); + }); + }); + }); + describe('isExperimentVariant', () => { describe.each` gon | input | output diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js index af5434f7068..1bb9c429a2f 100644 --- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js +++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js @@ -146,6 +146,21 @@ describe('MembersFilteredSearchBar', () => { }, ]); }); + + it('parses and passes search param with multiple words to `FilteredSearchBar` component as `initialFilterValue` prop', () => { + window.location.search = '?search=foo+bar+baz'; + + createComponent(); + + expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([ + { + type: 'filtered-search-term', + value: { + data: 'foo bar baz', + }, + }, + ]); + }); }); describe('when filter bar is submitted', () => { @@ -175,6 +190,17 @@ describe('MembersFilteredSearchBar', () => { expect(window.location.href).toBe('https://localhost/?two_factor=enabled&search=foobar'); }); + it('adds search query param with multiple words', () => { + createComponent(); + + findFilteredSearchBar().vm.$emit('onFilter', [ + { type: 'two_factor', value: { data: 'enabled', operator: '=' } }, + { type: 'filtered-search-term', value: { data: 'foo bar baz' } }, + ]); + + expect(window.location.href).toBe('https://localhost/?two_factor=enabled&search=foo+bar+baz'); + }); + it('adds sort query param', () => { window.location.search = '?sort=name_asc'; diff --git a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js index fa937100982..0400547b917 100644 --- a/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js +++ b/spec/frontend/pipeline_editor/components/file-nav/branch_switcher_spec.js @@ -120,4 +120,35 @@ describe('Pipeline editor branch switcher', () => { ]); }); }); + + describe('when switching branches', () => { + beforeEach(async () => { + mockAvailableBranchQuery.mockResolvedValue(mockProjectBranches); + createComponentWithApollo(); + await waitForPromises(); + }); + + it('emits the refetchContent event when selecting a different branch', async () => { + const branch = findDropdownItems().at(1); + + expect(branch.text()).not.toBe(mockDefaultBranch); + expect(wrapper.emitted('refetchContent')).toBeUndefined(); + + await branch.vm.$emit('click'); + + expect(wrapper.emitted('refetchContent')).toBeDefined(); + expect(wrapper.emitted('refetchContent')).toHaveLength(1); + }); + + it('does not emit the refetchContent event when selecting the current branch', async () => { + const branch = findDropdownItems().at(0); + + expect(branch.text()).toBe(mockDefaultBranch); + expect(wrapper.emitted('refetchContent')).toBeUndefined(); + + await branch.vm.$emit('click'); + + expect(wrapper.emitted('refetchContent')).toBeUndefined(); + }); + }); }); diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js index d8e3436479c..adb8c8836bc 100644 --- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js @@ -92,6 +92,11 @@ describe('Pipeline editor app component', () => { const options = { localVue, + data() { + return { + currentBranch: mockDefaultBranch, + }; + }, mocks: {}, apolloProvider: mockApollo, }; @@ -116,9 +121,6 @@ describe('Pipeline editor app component', () => { }); afterEach(() => { - mockBlobContentData.mockReset(); - mockCiConfigData.mockReset(); - wrapper.destroy(); }); @@ -337,4 +339,22 @@ describe('Pipeline editor app component', () => { }); }); }); + + describe('when refetching content', () => { + beforeEach(async () => { + await createComponentWithApollo(); + + jest + .spyOn(wrapper.vm.$apollo.queries.initialCiFileContent, 'refetch') + .mockImplementation(jest.fn()); + }); + + it('refetches blob content', async () => { + expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(0); + + await wrapper.vm.refetchContent(); + + expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js index 8a37c3cae4e..24cc6e76098 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js @@ -121,6 +121,26 @@ describe('Linked Pipelines Column', () => { }); }); + describe('when graph does not use needs', () => { + beforeEach(() => { + const nonNeedsResponse = { ...wrappedPipelineReturn }; + nonNeedsResponse.data.project.pipeline.usesNeeds = false; + + createComponentWithApollo({ + props: { + viewType: LAYER_VIEW, + }, + getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse), + mountFn: mount, + }); + }); + + it('shows the stage view, even when the main graph view type is layers', async () => { + await clickExpandButtonAndAwaitTimers(); + expect(findPipelineGraph().props('viewType')).toBe(STAGE_VIEW); + }); + }); + describe('downstream', () => { describe('when successful', () => { beforeEach(() => { diff --git a/spec/frontend/security_configuration/configuration_table_spec.js b/spec/frontend/security_configuration/configuration_table_spec.js index a1789052c92..fbd72265c4b 100644 --- a/spec/frontend/security_configuration/configuration_table_spec.js +++ b/spec/frontend/security_configuration/configuration_table_spec.js @@ -1,7 +1,7 @@ import { mount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import ConfigurationTable from '~/security_configuration/components/configuration_table.vue'; -import { scanners, UPGRADE_CTA } from '~/security_configuration/components/scanners_constants'; +import { scanners, UPGRADE_CTA } from '~/security_configuration/components/constants'; import { REPORT_TYPE_SAST, @@ -12,7 +12,13 @@ describe('Configuration Table Component', () => { let wrapper; const createComponent = () => { - wrapper = extendedWrapper(mount(ConfigurationTable, {})); + wrapper = extendedWrapper( + mount(ConfigurationTable, { + provide: { + projectPath: 'testProjectPath', + }, + }), + ); }; const findHelpLinks = () => wrapper.findAll('[data-testid="help-link"]'); @@ -30,8 +36,10 @@ describe('Configuration Table Component', () => { expect(wrapper.text()).toContain(scanner.name); expect(wrapper.text()).toContain(scanner.description); if (scanner.type === REPORT_TYPE_SAST) { - expect(wrapper.findByTestId(scanner.type).text()).toBe('Configure via merge request'); - } else if (scanner.type !== REPORT_TYPE_SECRET_DETECTION) { + expect(wrapper.findByTestId(scanner.type).text()).toBe('Configure via Merge Request'); + } else if (scanner.type === REPORT_TYPE_SECRET_DETECTION) { + expect(wrapper.findByTestId(scanner.type).exists()).toBe(false); + } else { expect(wrapper.findByTestId(scanner.type).text()).toMatchInterpolatedText(UPGRADE_CTA); } }); diff --git a/spec/frontend/security_configuration/manage_sast_spec.js b/spec/frontend/security_configuration/manage_sast_spec.js deleted file mode 100644 index 15a57210246..00000000000 --- a/spec/frontend/security_configuration/manage_sast_spec.js +++ /dev/null @@ -1,136 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { mount } 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 waitForPromises from 'helpers/wait_for_promises'; -import { redirectTo } from '~/lib/utils/url_utility'; -import ManageSast from '~/security_configuration/components/manage_sast.vue'; -import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql'; - -jest.mock('~/lib/utils/url_utility', () => ({ - redirectTo: jest.fn(), -})); - -Vue.use(VueApollo); - -describe('Manage Sast Component', () => { - let wrapper; - - const findButton = () => wrapper.findComponent(GlButton); - const successHandler = async () => { - return { - data: { - configureSast: { - successPath: 'testSuccessPath', - errors: [], - __typename: 'ConfigureSastPayload', - }, - }, - }; - }; - - const noSuccessPathHandler = async () => { - return { - data: { - configureSast: { - successPath: '', - errors: [], - __typename: 'ConfigureSastPayload', - }, - }, - }; - }; - - const errorHandler = async () => { - return { - data: { - configureSast: { - successPath: 'testSuccessPath', - errors: ['foo'], - __typename: 'ConfigureSastPayload', - }, - }, - }; - }; - - const pendingHandler = () => new Promise(() => {}); - - function createMockApolloProvider(handler) { - const requestHandlers = [[configureSastMutation, handler]]; - - return createMockApollo(requestHandlers); - } - - function createComponent(options = {}) { - const { mockApollo } = options; - wrapper = extendedWrapper( - mount(ManageSast, { - apolloProvider: mockApollo, - }), - ); - } - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('should render Button with correct text', () => { - createComponent(); - expect(findButton().text()).toContain('Configure via merge request'); - }); - - describe('given a successful response', () => { - beforeEach(() => { - const mockApollo = createMockApolloProvider(successHandler); - createComponent({ mockApollo }); - }); - - it('should call redirect helper with correct value', async () => { - await wrapper.trigger('click'); - await waitForPromises(); - expect(redirectTo).toHaveBeenCalledTimes(1); - expect(redirectTo).toHaveBeenCalledWith('testSuccessPath'); - // This is done for UX reasons. If the loading prop is set to false - // on success, then there's a period where the button is clickable - // again. Instead, we want the button to display a loading indicator - // for the remainder of the lifetime of the page (i.e., until the - // browser can start painting the new page it's been redirected to). - expect(findButton().props().loading).toBe(true); - }); - }); - - describe('given a pending response', () => { - beforeEach(() => { - const mockApollo = createMockApolloProvider(pendingHandler); - createComponent({ mockApollo }); - }); - - it('renders spinner correctly', async () => { - expect(findButton().props('loading')).toBe(false); - await wrapper.trigger('click'); - await waitForPromises(); - expect(findButton().props('loading')).toBe(true); - }); - }); - - describe.each` - handler | message - ${noSuccessPathHandler} | ${'SAST merge request creation mutation failed'} - ${errorHandler} | ${'foo'} - `('given an error response', ({ handler, message }) => { - beforeEach(() => { - const mockApollo = createMockApolloProvider(handler); - createComponent({ mockApollo }); - }); - - it('should catch and emit error', async () => { - await wrapper.trigger('click'); - await waitForPromises(); - expect(wrapper.emitted('error')).toEqual([[message]]); - expect(findButton().props('loading')).toBe(false); - }); - }); -}); diff --git a/spec/frontend/security_configuration/upgrade_spec.js b/spec/frontend/security_configuration/upgrade_spec.js index 1f0cc795fc5..20bb38aa469 100644 --- a/spec/frontend/security_configuration/upgrade_spec.js +++ b/spec/frontend/security_configuration/upgrade_spec.js @@ -1,5 +1,5 @@ import { mount } from '@vue/test-utils'; -import { UPGRADE_CTA } from '~/security_configuration/components/scanners_constants'; +import { UPGRADE_CTA } from '~/security_configuration/components/constants'; import Upgrade from '~/security_configuration/components/upgrade.vue'; const TEST_URL = 'http://www.example.test'; diff --git a/spec/frontend/vue_shared/security_reports/components/apollo_mocks.js b/spec/frontend/vue_shared/security_reports/components/apollo_mocks.js new file mode 100644 index 00000000000..066f9a57bc6 --- /dev/null +++ b/spec/frontend/vue_shared/security_reports/components/apollo_mocks.js @@ -0,0 +1,12 @@ +export const buildConfigureSecurityFeatureMockFactory = (mutationType) => ({ + successPath = 'testSuccessPath', + errors = [], +} = {}) => ({ + data: { + [mutationType]: { + successPath, + errors, + __typename: `${mutationType}Payload`, + }, + }, +}); diff --git a/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js new file mode 100644 index 00000000000..29e0da7f6ed --- /dev/null +++ b/spec/frontend/vue_shared/security_reports/components/manage_via_mr_spec.js @@ -0,0 +1,145 @@ +import { GlButton } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import configureDependencyScanningMutation from 'ee/security_configuration/graphql/configure_dependency_scanning.mutation.graphql'; +import configureSecretDetectionMutation from 'ee/security_configuration/graphql/configure_secret_detection.mutation.graphql'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { redirectTo } from '~/lib/utils/url_utility'; +import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue'; +import { + REPORT_TYPE_DEPENDENCY_SCANNING, + REPORT_TYPE_SECRET_DETECTION, +} from '~/vue_shared/security_reports/constants'; +import { buildConfigureSecurityFeatureMockFactory } from './apollo_mocks'; + +jest.mock('~/lib/utils/url_utility'); + +Vue.use(VueApollo); + +describe('ManageViaMr component', () => { + let wrapper; + + const findButton = () => wrapper.findComponent(GlButton); + describe.each` + featureName | featureType | mutation | mutationId + ${'Dependency Scanning'} | ${REPORT_TYPE_DEPENDENCY_SCANNING} | ${configureDependencyScanningMutation} | ${'configureDependencyScanning'} + ${'Secret Detection'} | ${REPORT_TYPE_SECRET_DETECTION} | ${configureSecretDetectionMutation} | ${'configureSecretDetection'} + `('$featureType', ({ featureName, mutation, featureType, mutationId }) => { + const buildConfigureSecurityFeatureMock = buildConfigureSecurityFeatureMockFactory(mutationId); + const successHandler = async () => buildConfigureSecurityFeatureMock(); + const noSuccessPathHandler = async () => + buildConfigureSecurityFeatureMock({ + successPath: '', + }); + const errorHandler = async () => + buildConfigureSecurityFeatureMock({ + errors: ['foo'], + }); + const pendingHandler = () => new Promise(() => {}); + + function createMockApolloProvider(handler) { + const requestHandlers = [[mutation, handler]]; + + return createMockApollo(requestHandlers); + } + + function createComponent({ mockApollo, isFeatureConfigured = false } = {}) { + wrapper = extendedWrapper( + mount(ManageViaMr, { + apolloProvider: mockApollo, + provide: { + projectPath: 'testProjectPath', + }, + propsData: { + feature: { + name: featureName, + type: featureType, + configured: isFeatureConfigured, + }, + }, + }), + ); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when feature is configured', () => { + beforeEach(() => { + const mockApollo = createMockApolloProvider(successHandler); + createComponent({ mockApollo, isFeatureConfigured: true }); + }); + + it('it does not render a button', () => { + expect(findButton().exists()).toBe(false); + }); + }); + + describe('when feature is not configured', () => { + beforeEach(() => { + const mockApollo = createMockApolloProvider(successHandler); + createComponent({ mockApollo, isFeatureConfigured: false }); + }); + + it('it does render a button', () => { + expect(findButton().exists()).toBe(true); + }); + }); + + describe('given a pending response', () => { + beforeEach(() => { + const mockApollo = createMockApolloProvider(pendingHandler); + createComponent({ mockApollo }); + }); + + it('renders spinner correctly', async () => { + const button = findButton(); + expect(button.props('loading')).toBe(false); + await button.trigger('click'); + expect(button.props('loading')).toBe(true); + }); + }); + + describe('given a successful response', () => { + beforeEach(() => { + const mockApollo = createMockApolloProvider(successHandler); + createComponent({ mockApollo }); + }); + + it('should call redirect helper with correct value', async () => { + await wrapper.trigger('click'); + await waitForPromises(); + expect(redirectTo).toHaveBeenCalledTimes(1); + expect(redirectTo).toHaveBeenCalledWith('testSuccessPath'); + // This is done for UX reasons. If the loading prop is set to false + // on success, then there's a period where the button is clickable + // again. Instead, we want the button to display a loading indicator + // for the remainder of the lifetime of the page (i.e., until the + // browser can start painting the new page it's been redirected to). + expect(findButton().props().loading).toBe(true); + }); + }); + + describe.each` + handler | message + ${noSuccessPathHandler} | ${`${featureName} merge request creation mutation failed`} + ${errorHandler} | ${'foo'} + `('given an error response', ({ handler, message }) => { + beforeEach(() => { + const mockApollo = createMockApolloProvider(handler); + createComponent({ mockApollo }); + }); + + it('should catch and emit error', async () => { + await wrapper.trigger('click'); + await waitForPromises(); + expect(wrapper.emitted('error')).toEqual([[message]]); + expect(findButton().props('loading')).toBe(false); + }); + }); + }); +}); diff --git a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb index a10c3725ba2..8ec99070c91 100644 --- a/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb +++ b/spec/graphql/mutations/alert_management/alerts/todo/create_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Mutations::AlertManagement::Alerts::Todo::Create do let_it_be(:alert) { create(:alert_management_alert) } let_it_be(:project) { alert.project } + let(:current_user) { project.owner } let(:args) { { project_path: project.full_path, iid: alert.iid } } diff --git a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb index 47ee338ad34..4758ac526a5 100644 --- a/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb +++ b/spec/graphql/mutations/alert_management/create_alert_issue_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Mutations::AlertManagement::CreateAlertIssue do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:alert) { create(:alert_management_alert, project: project, status: 'triggered') } + let(:args) { { project_path: project.full_path, iid: alert.iid } } specify { expect(described_class).to require_graphql_authorizations(:update_alert_management_alert) } diff --git a/spec/graphql/mutations/alert_management/http_integration/create_spec.rb b/spec/graphql/mutations/alert_management/http_integration/create_spec.rb index 9aa89761aaf..be6c627e376 100644 --- a/spec/graphql/mutations/alert_management/http_integration/create_spec.rb +++ b/spec/graphql/mutations/alert_management/http_integration/create_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Mutations::AlertManagement::HttpIntegration::Create do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } + let(:args) { { project_path: project.full_path, active: true, name: 'HTTP Integration' } } specify { expect(described_class).to require_graphql_authorizations(:admin_operations) } diff --git a/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb b/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb index acd7070d0d3..1aeeba1009e 100644 --- a/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb +++ b/spec/graphql/mutations/alert_management/http_integration/destroy_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Mutations::AlertManagement::HttpIntegration::Destroy do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } + let(:integration) { create(:alert_management_http_integration, project: project) } let(:args) { { id: GitlabSchema.id_from_object(integration) } } diff --git a/spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb b/spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb index 96974c2aa6f..5a2af9e0be8 100644 --- a/spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb +++ b/spec/graphql/mutations/alert_management/http_integration/reset_token_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Mutations::AlertManagement::HttpIntegration::ResetToken do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:integration) { create(:alert_management_http_integration, project: project) } + let(:args) { { id: GitlabSchema.id_from_object(integration) } } specify { expect(described_class).to require_graphql_authorizations(:admin_operations) } diff --git a/spec/graphql/mutations/alert_management/http_integration/update_spec.rb b/spec/graphql/mutations/alert_management/http_integration/update_spec.rb index d6318e3161d..805996bf9e9 100644 --- a/spec/graphql/mutations/alert_management/http_integration/update_spec.rb +++ b/spec/graphql/mutations/alert_management/http_integration/update_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Mutations::AlertManagement::HttpIntegration::Update do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:integration) { create(:alert_management_http_integration, project: project) } + let(:args) { { id: GitlabSchema.id_from_object(integration), active: false, name: 'New Name' } } specify { expect(described_class).to require_graphql_authorizations(:admin_operations) } diff --git a/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb b/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb index 02a5e2e74e2..7ab0f43d674 100644 --- a/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb +++ b/spec/graphql/mutations/alert_management/prometheus_integration/create_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Create do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } + let(:args) { { project_path: project.full_path, active: true, api_url: 'http://prometheus.com/' } } specify { expect(described_class).to require_graphql_authorizations(:admin_project) } diff --git a/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb b/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb index ddf23909035..c9e1bf4162c 100644 --- a/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb +++ b/spec/graphql/mutations/alert_management/prometheus_integration/reset_token_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::ResetToken do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:integration) { create(:prometheus_service, project: project) } + let(:args) { { id: GitlabSchema.id_from_object(integration) } } specify { expect(described_class).to require_graphql_authorizations(:admin_project) } diff --git a/spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb b/spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb index eab4474d827..19e0d53b75f 100644 --- a/spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb +++ b/spec/graphql/mutations/alert_management/prometheus_integration/update_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Mutations::AlertManagement::PrometheusIntegration::Update do let_it_be(:current_user) { create(:user) } let_it_be(:project) { create(:project) } let_it_be(:integration) { create(:prometheus_service, project: project) } + let(:args) { { id: GitlabSchema.id_from_object(integration), active: false, api_url: 'http://new-url.com' } } specify { expect(described_class).to require_graphql_authorizations(:admin_project) } diff --git a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb index 8465393f299..2c2518e046a 100644 --- a/spec/graphql/mutations/alert_management/update_alert_status_spec.rb +++ b/spec/graphql/mutations/alert_management/update_alert_status_spec.rb @@ -6,6 +6,7 @@ RSpec.describe Mutations::AlertManagement::UpdateAlertStatus do let_it_be(:current_user) { create(:user) } let_it_be(:alert) { create(:alert_management_alert, :triggered) } let_it_be(:project) { alert.project } + let(:new_status) { Types::AlertManagement::StatusEnum.values['ACKNOWLEDGED'].value } let(:args) { { status: new_status, project_path: project.full_path, iid: alert.iid } } diff --git a/spec/graphql/types/blob_viewer_type_spec.rb b/spec/graphql/types/blob_viewer_type_spec.rb new file mode 100644 index 00000000000..1c020c63535 --- /dev/null +++ b/spec/graphql/types/blob_viewer_type_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['BlobViewer'] do + it 'has the correct fields' do + expected_fields = [:type, :load_async, :too_large, :collapsed, + :render_error, :file_type, :loading_partial_name] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb index f8647e4e964..8accee90fa3 100644 --- a/spec/graphql/types/repository/blob_type_spec.rb +++ b/spec/graphql/types/repository/blob_type_spec.rb @@ -5,5 +5,26 @@ require 'spec_helper' RSpec.describe Types::Repository::BlobType do specify { expect(described_class.graphql_name).to eq('RepositoryBlob') } - specify { expect(described_class).to have_graphql_fields(:id, :oid, :name, :path, :web_path, :lfs_oid, :mode) } + specify do + expect(described_class).to have_graphql_fields( + :id, + :oid, + :name, + :path, + :web_path, + :lfs_oid, + :mode, + :size, + :raw_size, + :raw_blob, + :raw_text_blob, + :file_type, + :edit_blob_path, + :stored_externally, + :raw_path, + :replace_path, + :simple_viewer, + :rich_viewer + ) + end end diff --git a/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb new file mode 100644 index 00000000000..4f7c44e5d4e --- /dev/null +++ b/spec/lib/generators/gitlab/snowplow_event_definition_generator_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'generator_helper' + +RSpec.describe Gitlab::SnowplowEventDefinitionGenerator do + let(:ce_temp_dir) { Dir.mktmpdir } + let(:ee_temp_dir) { Dir.mktmpdir } + let(:generator_options) { { 'category' => 'Groups::EmailCampaignsController', 'action' => 'click' } } + + before do + stub_const("#{described_class}::CE_DIR", ce_temp_dir) + stub_const("#{described_class}::EE_DIR", ee_temp_dir) + end + + after do + FileUtils.rm_rf([ce_temp_dir, ee_temp_dir]) + end + + describe 'Creating event definition file' do + before do + stub_const('Gitlab::VERSION', '13.11.0-pre') + end + + let(:sample_event_dir) { 'lib/generators/gitlab/snowplow_event_definition_generator' } + + it 'creates CE event definition file using the template' do + sample_event = ::Gitlab::Config::Loader::Yaml.new(fixture_file(File.join(sample_event_dir, 'sample_event.yml'))).load_raw! + + described_class.new([], generator_options).invoke_all + + event_definition_path = File.join(ce_temp_dir, 'groups__email_campaigns_controller_click.yml') + expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event) + end + + it 'creates EE event definition file using the template' do + sample_event = ::Gitlab::Config::Loader::Yaml.new(fixture_file(File.join(sample_event_dir, 'sample_event_ee.yml'))).load_raw! + + described_class.new([], generator_options.merge('ee' => true)).invoke_all + + event_definition_path = File.join(ee_temp_dir, 'groups__email_campaigns_controller_click.yml') + expect(::Gitlab::Config::Loader::Yaml.new(File.read(event_definition_path)).load_raw!).to eq(sample_event) + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb index 264076859cb..889c9d239a5 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb @@ -259,15 +259,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do def expected_extra_queries extra_jobs = 2 - non_handled_sql_queries = 3 - - # 1. Ci::Build Load () SELECT "ci_builds".* FROM "ci_builds" - # WHERE "ci_builds"."type" = 'Ci::Build' - # AND "ci_builds"."commit_id" IS NULL - # AND ("ci_builds"."retried" = FALSE OR "ci_builds"."retried" IS NULL) - # AND (stage_idx < 1) - # 2. Ci::InstanceVariable Load => `Ci::InstanceVariable#cached_data` => already cached with `fetch_memory_cache` - # 3. Ci::Variable Load => `Project#ci_variables_for` => already cached with `Gitlab::SafeRequestStore` + non_handled_sql_queries = 2 + + # 1. Ci::InstanceVariable Load => `Ci::InstanceVariable#cached_data` => already cached with `fetch_memory_cache` + # 2. Ci::Variable Load => `Project#ci_variables_for` => already cached with `Gitlab::SafeRequestStore` extra_jobs * non_handled_sql_queries end diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb index 39a37234822..9243adbaa06 100644 --- a/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb +++ b/spec/lib/gitlab/database/background_migration/batched_migration_wrapper_spec.rb @@ -68,6 +68,12 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' subject end + it 'reports interval' do + expect(described_class.metrics[:gauge_interval]).to receive(:set).with(labels, job_record.batched_migration.interval) + + subject + end + it 'reports updated tuples (currently based on batch_size)' do expect(described_class.metrics[:counter_updated_tuples]).to receive(:increment).with(labels, job_record.batch_size) @@ -89,18 +95,22 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper, ' subject end - it 'reports time efficiency' do + it 'reports job duration' do freeze_time do expect(Time).to receive(:current).and_return(Time.zone.now - 5.seconds).ordered expect(Time).to receive(:current).and_return(Time.zone.now).ordered - ratio = 5 / job_record.batched_migration.interval.to_f - - expect(described_class.metrics[:histogram_time_efficiency]).to receive(:observe).with(labels, ratio) + expect(described_class.metrics[:gauge_job_duration]).to receive(:set).with(labels, 5.seconds) subject end end + + it 'reports the total tuple count for the migration' do + expect(described_class.metrics[:gauge_total_tuple_count]).to receive(:set).with(labels, job_record.batched_migration.total_tuple_count) + + subject + end end context 'when the migration job does not raise an error' do diff --git a/spec/lib/gitlab/hook_data/key_builder_spec.rb b/spec/lib/gitlab/hook_data/key_builder_spec.rb new file mode 100644 index 00000000000..86f33df115f --- /dev/null +++ b/spec/lib/gitlab/hook_data/key_builder_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::HookData::KeyBuilder do + let_it_be(:personal_key) { create(:personal_key) } + let_it_be(:other_key) { create(:key) } + + describe '#build' do + let(:data) { described_class.new(key).build(event) } + let(:event_name) { data[:event_name] } + let(:common_attributes) do + [ + :event_name, :created_at, :updated_at, :key, :id + ] + end + + shared_examples_for 'includes the required attributes' do + it 'includes the required attributes' do + expect(data.keys).to contain_exactly(*attributes) + + expect(data[:key]).to eq(key.key) + expect(data[:id]).to eq(key.id) + expect(data[:created_at]).to eq(key.created_at.xmlschema) + expect(data[:updated_at]).to eq(key.updated_at.xmlschema) + end + end + + context 'for keys that belong to a user' do + let(:key) { personal_key } + let(:attributes) { common_attributes.append(:username) } + + context 'data' do + context 'on create' do + let(:event) { :create } + + it { expect(event_name).to eq('key_create') } + it { expect(data[:username]).to eq(key.user.username) } + it_behaves_like 'includes the required attributes' + end + + context 'on destroy' do + let(:event) { :destroy } + + it { expect(event_name).to eq('key_destroy') } + it { expect(data[:username]).to eq(key.user.username) } + it_behaves_like 'includes the required attributes' + end + end + end + + context 'for keys that do not belong to a user' do + let(:key) { other_key } + let(:attributes) { common_attributes } + + context 'data' do + context 'on create' do + let(:event) { :create } + + it { expect(event_name).to eq('key_create') } + it_behaves_like 'includes the required attributes' + end + + context 'on destroy' do + let(:event) { :destroy } + + it { expect(event_name).to eq('key_destroy') } + it_behaves_like 'includes the required attributes' + end + end + end + end +end diff --git a/spec/lib/sidebars/concerns/positionable_list_spec.rb b/spec/lib/sidebars/concerns/positionable_list_spec.rb deleted file mode 100644 index ac933faba13..00000000000 --- a/spec/lib/sidebars/concerns/positionable_list_spec.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Concerns::PositionableList do - subject do - Class.new do - include Sidebars::Concerns::PositionableList - end.new - end - - describe '#add_element' do - it 'adds the element to the last position of the list' do - list = [1, 2] - - subject.add_element(list, 3) - - expect(list).to eq([1, 2, 3]) - end - end - - describe '#insert_element_before' do - let(:user) { build(:user) } - let(:list) { [1, user] } - - it 'adds element before the specific element class' do - subject.insert_element_before(list, User, 2) - - expect(list).to eq [1, 2, user] - end - - context 'when reference element does not exist' do - it 'adds the element to the top of the list' do - subject.insert_element_before(list, Project, 2) - - expect(list).to eq [2, 1, user] - end - end - end - - describe '#insert_element_after' do - let(:user) { build(:user) } - let(:list) { [1, user] } - - it 'adds element after the specific element class' do - subject.insert_element_after(list, Integer, 2) - - expect(list).to eq [1, 2, user] - end - - context 'when reference element does not exist' do - it 'adds the element to the end of the list' do - subject.insert_element_after(list, Project, 2) - - expect(list).to eq [1, user, 2] - end - end - end -end diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb index 320f5f1ad1e..53483f0c924 100644 --- a/spec/lib/sidebars/menu_spec.rb +++ b/spec/lib/sidebars/menu_spec.rb @@ -8,59 +8,82 @@ RSpec.describe Sidebars::Menu do describe '#all_active_routes' do it 'gathers all active routes of items and the current menu' do - menu_item1 = Sidebars::MenuItem.new(context) - menu_item2 = Sidebars::MenuItem.new(context) - menu_item3 = Sidebars::MenuItem.new(context) - menu.add_item(menu_item1) - menu.add_item(menu_item2) - menu.add_item(menu_item3) + menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: { path: %w(bar test) })) + menu.add_item(Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: { controller: 'fooc' })) + menu.add_item(Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: { controller: 'barc' })) allow(menu).to receive(:active_routes).and_return({ path: 'foo' }) - allow(menu_item1).to receive(:active_routes).and_return({ path: %w(bar test) }) - allow(menu_item2).to receive(:active_routes).and_return({ controller: 'fooc' }) - allow(menu_item3).to receive(:active_routes).and_return({ controller: 'barc' }) expect(menu.all_active_routes).to eq({ path: %w(foo bar test), controller: %w(fooc barc) }) end - - it 'does not include routes for non renderable items' do - menu_item = Sidebars::MenuItem.new(context) - menu.add_item(menu_item) - - allow(menu).to receive(:active_routes).and_return({ path: 'foo' }) - allow(menu_item).to receive(:render?).and_return(false) - allow(menu_item).to receive(:active_routes).and_return({ controller: 'bar' }) - - expect(menu.all_active_routes).to eq({ path: ['foo'] }) - end end describe '#render?' do context 'when the menus has no items' do - it 'returns true' do - expect(menu.render?).to be true + it 'returns false' do + expect(menu.render?).to be false end end context 'when the menu has items' do - let(:menu_item) { Sidebars::MenuItem.new(context) } + it 'returns true' do + menu.add_item(Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {})) - before do - menu.add_item(menu_item) + expect(menu.render?).to be true end + end + end + + describe '#insert_element_before' do + let(:item1) { Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {}, item_id: :foo1) } + let(:item2) { Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: {}, item_id: :foo2) } + let(:item3) { Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: {}, item_id: :foo3) } + let(:list) { [item1, item2] } + + it 'adds element before the specific element class' do + menu.insert_element_before(list, :foo2, item3) + + expect(list).to eq [item1, item3, item2] + end + + it 'does not add nil elements' do + menu.insert_element_before(list, :foo2, nil) + + expect(list).to eq [item1, item2] + end - context 'when items are not renderable' do - it 'returns false' do - allow(menu_item).to receive(:render?).and_return(false) + context 'when reference element does not exist' do + it 'adds the element to the top of the list' do + menu.insert_element_before(list, :non_existent, item3) - expect(menu.render?).to be false - end + expect(list).to eq [item3, item1, item2] end + end + end + + describe '#insert_element_after' do + let(:item1) { Sidebars::MenuItem.new(title: 'foo1', link: 'foo1', active_routes: {}, item_id: :foo1) } + let(:item2) { Sidebars::MenuItem.new(title: 'foo2', link: 'foo2', active_routes: {}, item_id: :foo2) } + let(:item3) { Sidebars::MenuItem.new(title: 'foo3', link: 'foo3', active_routes: {}, item_id: :foo3) } + let(:list) { [item1, item2] } + + it 'adds element after the specific element class' do + menu.insert_element_after(list, :foo1, item3) + + expect(list).to eq [item1, item3, item2] + end + + it 'does not add nil elements' do + menu.insert_element_after(list, :foo1, nil) + + expect(list).to eq [item1, item2] + end + + context 'when reference element does not exist' do + it 'adds the element to the end of the list' do + menu.insert_element_after(list, :non_existent, item3) - context 'when there are renderable items' do - it 'returns true' do - expect(menu.render?).to be true - end + expect(list).to eq [item1, item2, item3] end end end diff --git a/spec/lib/sidebars/panel_spec.rb b/spec/lib/sidebars/panel_spec.rb index 0e539460810..5142b0bf07e 100644 --- a/spec/lib/sidebars/panel_spec.rb +++ b/spec/lib/sidebars/panel_spec.rb @@ -26,9 +26,79 @@ RSpec.describe Sidebars::Panel do end it 'returns true when no renderable menus' do + allow(menu1).to receive(:render?).and_return(true) + panel.add_menu(menu1) expect(panel.has_renderable_menus?).to be true end end + + describe '#add_element' do + it 'adds the element to the last position of the list' do + list = [1, 2] + + panel.add_element(list, 3) + + expect(list).to eq([1, 2, 3]) + end + + it 'does not add nil elements' do + list = [] + + panel.add_element(list, nil) + + expect(list).to be_empty + end + end + + describe '#insert_element_before' do + let(:user) { build(:user) } + let(:list) { [1, user] } + + it 'adds element before the specific element class' do + panel.insert_element_before(list, User, 2) + + expect(list).to eq [1, 2, user] + end + + it 'does not add nil elements' do + panel.insert_element_before(list, User, nil) + + expect(list).to eq [1, user] + end + + context 'when reference element does not exist' do + it 'adds the element to the top of the list' do + panel.insert_element_before(list, Project, 2) + + expect(list).to eq [2, 1, user] + end + end + end + + describe '#insert_element_after' do + let(:user) { build(:user) } + let(:list) { [1, user] } + + it 'adds element after the specific element class' do + panel.insert_element_after(list, Integer, 2) + + expect(list).to eq [1, 2, user] + end + + it 'does not add nil elements' do + panel.insert_element_after(list, Integer, nil) + + expect(list).to eq [1, user] + end + + context 'when reference element does not exist' do + it 'adds the element to the end of the list' do + panel.insert_element_after(list, Project, 2) + + expect(list).to eq [1, user, 2] + end + end + end end diff --git a/spec/lib/sidebars/projects/menus/learn_gitlab/menu_spec.rb b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb index bc1815558d3..3dfb4c6e9e8 100644 --- a/spec/lib/sidebars/projects/menus/learn_gitlab/menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Sidebars::Projects::Menus::LearnGitlab::Menu do +RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do let(:project) { build(:project) } let(:experiment_enabled) { true } let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project, learn_gitlab_experiment_enabled: experiment_enabled) } diff --git a/spec/lib/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb b/spec/lib/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb deleted file mode 100644 index db124c2252e..00000000000 --- a/spec/lib/sidebars/projects/menus/project_overview/menu_items/releases_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases do - let_it_be(:project) { create(:project, :repository) } - - let(:user) { project.owner } - let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } - - subject { described_class.new(context) } - - describe '#render?' do - context 'when project repository is empty' do - it 'returns false' do - allow(project).to receive(:empty_repo?).and_return(true) - - expect(subject.render?).to eq false - end - end - - context 'when project repository is not empty' do - context 'when user can read releases' do - it 'returns true' do - expect(subject.render?).to eq true - end - end - - context 'when user cannot read releases' do - let(:user) { nil } - - it 'returns false' do - expect(subject.render?).to eq false - end - end - end - end -end diff --git a/spec/lib/sidebars/projects/menus/project_overview/menu_spec.rb b/spec/lib/sidebars/projects/menus/project_overview/menu_spec.rb deleted file mode 100644 index 105a28ce953..00000000000 --- a/spec/lib/sidebars/projects/menus/project_overview/menu_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Sidebars::Projects::Menus::ProjectOverview::Menu do - let(:project) { build(:project) } - let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) } - - subject { described_class.new(context) } - - it 'has the required items' do - items = subject.instance_variable_get(:@items) - - expect(items[0]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Details) - expect(items[1]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Activity) - expect(items[2]).to be_a(Sidebars::Projects::Menus::ProjectOverview::MenuItems::Releases) - end -end diff --git a/spec/lib/sidebars/projects/menus/project_overview_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_overview_menu_spec.rb new file mode 100644 index 00000000000..91682a9e415 --- /dev/null +++ b/spec/lib/sidebars/projects/menus/project_overview_menu_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Projects::Menus::ProjectOverviewMenu do + let_it_be(:project) { create(:project, :repository) } + + let(:user) { project.owner } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } + + describe 'Releases' do + subject { described_class.new(context).items.index { |e| e.item_id == :releases } } + + context 'when project repository is empty' do + it 'does not include releases menu item' do + allow(project).to receive(:empty_repo?).and_return(true) + + is_expected.to be_nil + end + end + + context 'when project repository is not empty' do + context 'when user can download code' do + it 'includes releases menu item' do + is_expected.to be_present + end + end + + context 'when user cannot download code' do + let(:user) { nil } + + it 'does not include releases menu item' do + is_expected.to be_nil + end + end + end + end +end diff --git a/spec/lib/sidebars/projects/menus/repository/menu_spec.rb b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb index 04eb3357a6f..554a4e3f532 100644 --- a/spec/lib/sidebars/projects/menus/repository/menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/repository_menu_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Sidebars::Projects::Menus::Repository::Menu do +RSpec.describe Sidebars::Projects::Menus::RepositoryMenu do let_it_be(:project) { create(:project, :repository) } let(:user) { project.owner } - let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project, current_ref: 'master') } subject { described_class.new(context) } diff --git a/spec/lib/sidebars/projects/panel_spec.rb b/spec/lib/sidebars/projects/panel_spec.rb index bad9b17bc83..f4609682e6d 100644 --- a/spec/lib/sidebars/projects/panel_spec.rb +++ b/spec/lib/sidebars/projects/panel_spec.rb @@ -9,6 +9,6 @@ RSpec.describe Sidebars::Projects::Panel do subject { described_class.new(context) } it 'has a scope menu' do - expect(subject.scope_menu).to be_a(Sidebars::Projects::Menus::Scope::Menu) + expect(subject.scope_menu).to be_a(Sidebars::Projects::Menus::ScopeMenu) end end diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb index f2659771a49..e520f82c118 100644 --- a/spec/models/pages/lookup_path_spec.rb +++ b/spec/models/pages/lookup_path_spec.rb @@ -47,15 +47,11 @@ RSpec.describe Pages::LookupPath do describe '#source' do let(:source) { lookup_path.source } - shared_examples 'uses disk storage' do - it 'uses disk storage', :aggregate_failures do - expect(source[:type]).to eq('file') - expect(source[:path]).to eq(project.full_path + "/public/") - end + it 'uses disk storage', :aggregate_failures do + expect(source[:type]).to eq('file') + expect(source[:path]).to eq(project.full_path + "/public/") end - include_examples 'uses disk storage' - it 'return nil when legacy storage is disabled and there is no deployment' do stub_feature_flags(pages_serve_from_legacy_storage: false) expect(Gitlab::ErrorTracking).to receive(:track_exception) @@ -107,14 +103,6 @@ RSpec.describe Pages::LookupPath do ) end end - - context 'when pages_serve_with_zip_file_protocol feature flag is disabled' do - before do - stub_feature_flags(pages_serve_with_zip_file_protocol: false) - end - - include_examples 'uses disk storage' - end end context 'when deployment were created during migration' do diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index c4146b347d7..caa0a886abf 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -452,11 +452,15 @@ RSpec.describe Todo do end end - describe '.pluck_user_id' do - subject { described_class.pluck_user_id } + describe '.distinct_user_ids' do + subject { described_class.distinct_user_ids } - let_it_be(:todo) { create(:todo) } + let_it_be(:user1) { create(:user) } + let_it_be(:user2) { create(:user) } + let_it_be(:todo) { create(:todo, user: user1) } + let_it_be(:todo) { create(:todo, user: user1) } + let_it_be(:todo) { create(:todo, user: user2) } - it { is_expected.to eq([todo.user_id]) } + it { is_expected.to contain_exactly(user1.id, user2.id) } end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ff2ee4adc1c..1f7d5c5f91f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -20,6 +20,10 @@ RSpec.describe User do it { is_expected.to include_module(AsyncDeviseEmail) } end + describe 'constants' do + it { expect(described_class::COUNT_CACHE_VALIDITY_PERIOD).to be_a(Integer) } + end + describe 'delegations' do it { is_expected.to delegate_method(:path).to(:namespace).with_prefix } diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb index 47402fea2b5..d6acc20396f 100644 --- a/spec/presenters/blob_presenter_spec.rb +++ b/spec/presenters/blob_presenter_spec.rb @@ -2,52 +2,59 @@ require 'spec_helper' -RSpec.describe BlobPresenter, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - - let(:git_blob) do - Gitlab::Git::Blob.find( - repository, - 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', - 'files/ruby/regex.rb' - ) +RSpec.describe BlobPresenter do + let_it_be(:project) { create(:project, :repository) } + + let(:repository) { project.repository } + let(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') } + + subject(:presenter) { described_class.new(blob) } + + describe '#web_url' do + it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } end - let(:blob) { Blob.new(git_blob) } + describe '#web_path' do + it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + end - describe '.web_url' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } - let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.blobs.first, repository) } + describe '#edit_blob_path' do + it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{blob.commit_id}/#{blob.path}") } + end - subject { described_class.new(blob) } + describe '#raw_path' do + it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{blob.commit_id}/#{blob.path}") } + end - it { expect(subject.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + describe '#replace_path' do + it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/create/#{blob.commit_id}/#{blob.path}") } end - describe '#web_path' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } - let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.blobs.first, repository) } + context 'given a Gitlab::Graphql::Representation::TreeEntry' do + let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(super(), repository) } - subject { described_class.new(blob) } + describe '#web_url' do + it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + end - it { expect(subject.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + describe '#web_path' do + it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + end end describe '#highlight' do - subject { described_class.new(blob) } + let(:git_blob) { blob.__getobj__ } it 'returns highlighted content' do expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil) - subject.highlight + presenter.highlight end it 'returns plain content when :plain is true' do expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil) - subject.highlight(plain: true) + presenter.highlight(plain: true) end context '"to" param is present' do @@ -60,7 +67,7 @@ RSpec.describe BlobPresenter, :seed_helper do it 'returns limited highlighted content' do expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "line one\n", plain: nil, language: nil) - subject.highlight(to: 1) + presenter.highlight(to: 1) end end @@ -72,7 +79,7 @@ RSpec.describe BlobPresenter, :seed_helper do it 'passes language to inner call' do expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby') - subject.highlight + presenter.highlight end end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index aa1a4643593..3da372c86a1 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' RSpec.describe API::Environments do - let(:user) { create(:user) } - let(:non_member) { create(:user) } - let(:project) { create(:project, :private, :repository, namespace: user.namespace) } - let!(:environment) { create(:environment, project: project) } + let_it_be(:user) { create(:user) } + let_it_be(:non_member) { create(:user) } + let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace) } + let_it_be_with_reload(:environment) { create(:environment, project: project) } before do project.add_maintainer(user) @@ -34,11 +34,51 @@ RSpec.describe API::Environments do expect(json_response.first['name']).to eq(environment.name) expect(json_response.first['external_url']).to eq(environment.external_url) expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys) - expect(json_response.first).not_to have_key("last_deployment") + expect(json_response.first['enable_advanced_logs_querying']).to eq(false) + expect(json_response.first).not_to have_key('last_deployment') + end + + context 'when elastic stack is available' do + before do + allow_next_found_instance_of(Environment) do |env| + allow(env).to receive(:elastic_stack_available?).and_return(true) + end + end + + context 'when the user can read pod logs' do + it 'returns environment with enable_advanced_logs_querying' do + get api("/projects/#{project.id}/environments", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['enable_advanced_logs_querying']).to eq(true) + end + end + + context 'when the user cannot read pod logs' do + before do + allow_next_found_instance_of(User) do |user| + allow(user).to receive(:can?).and_call_original + allow(user).to receive(:can?).with(:read_pod_logs, project).and_return(false) + end + end + + it 'does not contain enable_advanced_logs_querying' do + get api("/projects/#{project.id}/environments", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to include_pagination_headers + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first).not_to have_key('enable_advanced_logs_querying') + end + end end context 'when filtering' do - let!(:environment2) { create(:environment, project: project) } + let_it_be(:environment2) { create(:environment, project: project) } it 'returns environment by name' do get api("/projects/#{project.id}/environments?name=#{environment.name}", user) diff --git a/spec/serializers/ci/downloadable_artifact_entity_spec.rb b/spec/serializers/ci/downloadable_artifact_entity_spec.rb new file mode 100644 index 00000000000..34a271e7422 --- /dev/null +++ b/spec/serializers/ci/downloadable_artifact_entity_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::DownloadableArtifactEntity do + let(:pipeline) { create(:ci_pipeline, :with_codequality_reports) } + let(:user) { create(:user) } + let(:request) { EntityRequest.new({ current_user: user }) } + let(:entity) { described_class.new(pipeline, request: request) } + + describe '#as_json' do + subject { entity.as_json } + + it 'contains required fields', :aggregate_failures do + expect(subject).to include(:artifacts) + expect(subject[:artifacts].size).to eq(1) + end + + context 'when user cannot read job artifact' do + let!(:build) { create(:ci_build, :success, :artifacts, :non_public_artifacts, pipeline: pipeline) } + + it 'returns only artifacts readable by user', :aggregate_failures do + expect(subject[:artifacts].size).to eq(1) + expect(subject[:artifacts].first[:name]).to eq("test:codequality") + end + end + end +end diff --git a/spec/serializers/ci/downloadable_artifact_serializer_spec.rb b/spec/serializers/ci/downloadable_artifact_serializer_spec.rb new file mode 100644 index 00000000000..90f159a06f9 --- /dev/null +++ b/spec/serializers/ci/downloadable_artifact_serializer_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::DownloadableArtifactSerializer do + let(:pipeline) { create(:ci_pipeline, :with_codequality_reports) } + let(:user) { create(:user) } + let(:serializer) { described_class.new(current_user: user).represent(pipeline) } + + describe '#as_json' do + subject { serializer.as_json } + + it 'matches schema' do + expect(subject).to match_schema('entities/downloadable_artifact') + end + end +end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index d8435c72896..5d60b6e0487 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -3,133 +3,68 @@ require 'spec_helper' RSpec.describe SystemHooksService do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:project_member) { create(:project_member) } - let(:key) { create(:key, user: user) } - let(:deploy_key) { create(:key) } - let(:group) { create(:group) } - let(:group_member) { create(:group_member) } - - context 'event data' do - it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } - it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } - it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project, :update)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } - it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } - it { expect(event_data(project_member, :update)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } - it { expect(event_data(key, :create)).to include(:username, :key, :id) } - it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } - it { expect(event_data(deploy_key, :create)).to include(:key, :id) } - it { expect(event_data(deploy_key, :destroy)).to include(:key, :id) } - - it do - project.old_path_with_namespace = 'renamed_from_path' - expect(event_data(project, :rename)).to include( - :event_name, :name, :created_at, :updated_at, :path, :project_id, - :owner_name, :owner_email, :project_visibility, - :old_path_with_namespace - ) + describe '#execute_hooks_for' do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project) } + let_it_be(:group_member) { create(:group_member, source: group, user: user) } + let_it_be(:project_member) { create(:project_member, source: project, user: user) } + let_it_be(:key) { create(:key, user: user) } + let_it_be(:deploy_key) { create(:key) } + + let(:event) { :create } + + using RSpec::Parameterized::TableSyntax + + where(:model_name, :builder_class) do + :group_member | Gitlab::HookData::GroupMemberBuilder + :group | Gitlab::HookData::GroupBuilder + :project_member | Gitlab::HookData::ProjectMemberBuilder + :user | Gitlab::HookData::UserBuilder + :project | Gitlab::HookData::ProjectBuilder + :key | Gitlab::HookData::KeyBuilder + :deploy_key | Gitlab::HookData::KeyBuilder end - it do - project.old_path_with_namespace = 'transferred_from_path' - expect(event_data(project, :transfer)).to include( - :event_name, :name, :created_at, :updated_at, :path, :project_id, - :owner_name, :owner_email, :project_visibility, - :old_path_with_namespace - ) - end - - it do - expect(event_data(group, :create)).to include( - :event_name, :name, :created_at, :updated_at, :path, :group_id - ) - end + with_them do + it 'builds the data with the relevant builder class and then calls #execute_hooks with the obtained data' do + data = double + model = public_send(model_name) - it do - expect(event_data(group, :destroy)).to include( - :event_name, :name, :created_at, :updated_at, :path, :group_id - ) - end + expect_next_instance_of(builder_class, model) do |builder| + expect(builder).to receive(:build).with(event).and_return(data) + end - it do - expect(event_data(group_member, :create)).to include( - :event_name, :created_at, :updated_at, :group_name, :group_path, - :group_id, :user_id, :user_username, :user_name, :user_email, :group_access - ) - end + service = described_class.new - it do - expect(event_data(group_member, :destroy)).to include( - :event_name, :created_at, :updated_at, :group_name, :group_path, - :group_id, :user_id, :user_username, :user_name, :user_email, :group_access - ) - end - - it do - expect(event_data(group_member, :update)).to include( - :event_name, :created_at, :updated_at, :group_name, :group_path, - :group_id, :user_id, :user_username, :user_name, :user_email, :group_access - ) - end - - it 'includes the correct project visibility level' do - data = event_data(project, :create) - - expect(data[:project_visibility]).to eq('private') - end + expect_next_instance_of(SystemHooksService) do |system_hook_service| + expect(system_hook_service).to receive(:execute_hooks).with(data) + end - it 'handles nil datetime columns' do - user.update!(created_at: nil, updated_at: nil) - data = event_data(user, :destroy) - - expect(data[:created_at]).to be(nil) - expect(data[:updated_at]).to be(nil) + service.execute_hooks_for(model, event) + end end + end - context 'group_rename' do - it 'contains old and new path' do - allow(group).to receive(:path_before_last_save).and_return('old-path') + describe '#execute_hooks' do + let(:data) { { key: :value } } - data = event_data(group, :rename) + subject { described_class.new.execute_hooks(data) } - expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path) - expect(data[:path]).to eq(group.path) - expect(data[:full_path]).to eq(group.path) - expect(data[:old_path]).to eq(group.path_before_last_save) - expect(data[:old_full_path]).to eq(group.path_before_last_save) - end + it 'executes system hooks with the given data' do + hook = create(:system_hook) - it 'contains old and new full_path for subgroup' do - subgroup = create(:group, parent: group) - allow(subgroup).to receive(:path_before_last_save).and_return('old-path') + allow(SystemHook).to receive_message_chain(:hooks_for, :find_each).and_yield(hook) - data = event_data(subgroup, :rename) + expect(hook).to receive(:async_execute).with(data, 'system_hooks') - expect(data[:full_path]).to eq(subgroup.full_path) - expect(data[:old_path]).to eq('old-path') - end + subject end - end - context 'event names' do - it { expect(event_name(project, :create)).to eq "project_create" } - it { expect(event_name(project, :destroy)).to eq "project_destroy" } - it { expect(event_name(project, :rename)).to eq "project_rename" } - it { expect(event_name(project, :transfer)).to eq "project_transfer" } - it { expect(event_name(project, :update)).to eq "project_update" } - it { expect(event_name(key, :create)).to eq 'key_create' } - it { expect(event_name(key, :destroy)).to eq 'key_destroy' } - end + it 'executes FileHook with the given data' do + expect(Gitlab::FileHook).to receive(:execute_all_async).with(data) - def event_data(*args) - SystemHooksService.new.send :build_event_data, *args - end - - def event_name(*args) - SystemHooksService.new.send :build_event_name, *args + subject + end end end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 35503010b53..6a8e6dc8970 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -348,7 +348,7 @@ RSpec.describe TodoService do create(:todo, state: :pending, target: issue, user: author, author: author, project: issue.project) create(:todo, state: :done, target: issue, user: assignee, author: assignee, project: issue.project) - expect_next(Users::UpdateTodoCountCacheService, [author, assignee]).to receive(:execute) + expect_next(Users::UpdateTodoCountCacheService, [author.id, assignee.id]).to receive(:execute) service.destroy_target(issue) { issue.destroy! } end @@ -1094,7 +1094,7 @@ RSpec.describe TodoService do it 'updates cached counts when a todo is created' do issue = create(:issue, project: project, assignees: [john_doe], author: author) - expect_next(Users::UpdateTodoCountCacheService, [john_doe]).to receive(:execute) + expect_next(Users::UpdateTodoCountCacheService, [john_doe.id]).to receive(:execute) service.new_issue(issue, author) end diff --git a/spec/services/users/update_todo_count_cache_service_spec.rb b/spec/services/users/update_todo_count_cache_service_spec.rb index 3e3618b1291..3d96af928df 100644 --- a/spec/services/users/update_todo_count_cache_service_spec.rb +++ b/spec/services/users/update_todo_count_cache_service_spec.rb @@ -14,13 +14,21 @@ RSpec.describe Users::UpdateTodoCountCacheService do let_it_be(:todo5) { create(:todo, user: user2, state: :pending) } let_it_be(:todo6) { create(:todo, user: user2, state: :pending) } + def execute_all + described_class.new([user1.id, user2.id]).execute + end + + def execute_single + described_class.new([user1.id]).execute + end + it 'updates the todos_counts for users', :use_clean_rails_memory_store_caching do Rails.cache.write(['users', user1.id, 'todos_done_count'], 0) Rails.cache.write(['users', user1.id, 'todos_pending_count'], 0) Rails.cache.write(['users', user2.id, 'todos_done_count'], 0) Rails.cache.write(['users', user2.id, 'todos_pending_count'], 0) - expect { described_class.new([user1, user2]).execute } + expect { execute_all } .to change(user1, :todos_done_count).from(0).to(2) .and change(user1, :todos_pending_count).from(0).to(1) .and change(user2, :todos_done_count).from(0).to(1) @@ -28,7 +36,7 @@ RSpec.describe Users::UpdateTodoCountCacheService do Todo.delete_all - expect { described_class.new([user1, user2]).execute } + expect { execute_all } .to change(user1, :todos_done_count).from(2).to(0) .and change(user1, :todos_pending_count).from(1).to(0) .and change(user2, :todos_done_count).from(1).to(0) @@ -36,26 +44,24 @@ RSpec.describe Users::UpdateTodoCountCacheService do end it 'avoids N+1 queries' do - control_count = ActiveRecord::QueryRecorder.new { described_class.new([user1]).execute }.count + control_count = ActiveRecord::QueryRecorder.new { execute_single }.count - expect { described_class.new([user1, user2]).execute }.not_to exceed_query_limit(control_count) + expect { execute_all }.not_to exceed_query_limit(control_count) end it 'executes one query per batch of users' do stub_const("#{described_class}::QUERY_BATCH_SIZE", 1) - expect(ActiveRecord::QueryRecorder.new { described_class.new([user1]).execute }.count).to eq(1) - expect(ActiveRecord::QueryRecorder.new { described_class.new([user1, user2]).execute }.count).to eq(2) + expect(ActiveRecord::QueryRecorder.new { execute_single }.count).to eq(1) + expect(ActiveRecord::QueryRecorder.new { execute_all }.count).to eq(2) end - it 'sets the cache expire time to the users count_cache_validity_period' do - allow(user1).to receive(:count_cache_validity_period).and_return(1.minute) - allow(user2).to receive(:count_cache_validity_period).and_return(1.hour) - - expect(Rails.cache).to receive(:write).with(['users', user1.id, anything], anything, expires_in: 1.minute).twice - expect(Rails.cache).to receive(:write).with(['users', user2.id, anything], anything, expires_in: 1.hour).twice + it 'sets the correct cache expire time' do + expect(Rails.cache).to receive(:write) + .with(['users', user1.id, anything], anything, expires_in: User::COUNT_CACHE_VALIDITY_PERIOD) + .twice - described_class.new([user1, user2]).execute + execute_single end end end diff --git a/spec/views/shared/nav/_sidebar.html.haml_spec.rb b/spec/views/shared/nav/_sidebar.html.haml_spec.rb index 268d2952683..7c7c7176e0f 100644 --- a/spec/views/shared/nav/_sidebar.html.haml_spec.rb +++ b/spec/views/shared/nav/_sidebar.html.haml_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe 'shared/nav/_sidebar.html.haml' do let(:project) { build(:project, id: non_existing_record_id) } - let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project)} + let(:context) { Sidebars::Projects::Context.new(current_user: nil, container: project) } let(:sidebar) { Sidebars::Projects::Panel.new(context) } before do |