diff options
Diffstat (limited to 'spec')
78 files changed, 3722 insertions, 578 deletions
diff --git a/spec/finders/groups/accepting_project_creations_finder_spec.rb b/spec/finders/groups/accepting_project_creations_finder_spec.rb index b1b9403748d..2ea5577dd90 100644 --- a/spec/finders/groups/accepting_project_creations_finder_spec.rb +++ b/spec/finders/groups/accepting_project_creations_finder_spec.rb @@ -100,20 +100,5 @@ RSpec.describe Groups::AcceptingProjectCreationsFinder, feature_category: :subgr shared_with_group_where_direct_owner_as_developer ]) end - - context 'when `include_groups_from_group_shares_in_project_creation_locations` flag is disabled' do - before do - stub_feature_flags(include_groups_from_group_shares_in_project_creation_locations: false) - end - - it 'returns only groups accessible via direct membership where user has access to create projects' do - expect(result).to match_array([ - group_where_direct_owner, - subgroup_of_group_where_direct_owner, - group_where_direct_maintainer, - group_where_direct_developer - ]) - end - end end end diff --git a/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js b/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js index 71e8e6d3afb..3ef5427f288 100644 --- a/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js +++ b/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js @@ -1,12 +1,13 @@ import $ from 'jquery'; -import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import htmlPipelineSchedulesEdit from 'test_fixtures/pipeline_schedules/edit.html'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list'; describe('NativeFormVariableList', () => { let $wrapper; beforeEach(() => { - loadHTMLFixture('pipeline_schedules/edit.html'); + setHTMLFixture(htmlPipelineSchedulesEdit); $wrapper = $('.js-ci-variable-list-section'); setupNativeFormVariableList({ diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap deleted file mode 100644 index c278bb4579f..00000000000 --- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap +++ /dev/null @@ -1,120 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`self-monitor component When the self-monitor project has not been created default state to match the default snapshot 1`] = ` -<section - class="settings no-animate js-self-monitoring-settings" -> - <div - class="settings-header" - > - <h4 - class="js-section-header settings-title js-settings-toggle js-settings-toggle-trigger-only" - > - - Self-monitoring - - </h4> - - <gl-button-stub - buttontextclasses="" - category="primary" - class="js-settings-toggle" - icon="" - size="medium" - variant="default" - > - Expand - </gl-button-stub> - - <p - class="js-section-sub-header" - > - - Activate or deactivate instance self-monitoring. - - <gl-link-stub - href="/help/administration/monitoring/gitlab_self_monitoring_project/index" - > - Learn more. - </gl-link-stub> - </p> - </div> - - <gl-alert-stub - class="gl-mb-3" - dismissible="true" - dismisslabel="Dismiss" - primarybuttonlink="" - primarybuttontext="" - secondarybuttonlink="" - secondarybuttontext="" - showicon="true" - title="Deprecation notice" - variant="danger" - > - <div> - Self-monitoring was - <a - href="/help/update/deprecations.md#gitlab-self-monitoring-project" - > - deprecated - </a> - in GitLab 14.9, and is - <a - href="https://gitlab.com/gitlab-org/gitlab/-/issues/348909" - > - scheduled for removal - </a> - in GitLab 16.0. For information on a possible replacement, - <a - href="https://gitlab.com/groups/gitlab-org/-/epics/6976" - > - learn more about Opstrace - </a> - . - </div> - </gl-alert-stub> - - <div - class="settings-content" - > - <form - name="self-monitoring-form" - > - <p> - Activate self-monitoring to create a project to use to monitor the health of your instance. - </p> - - <gl-form-group-stub - labeldescription="" - optionaltext="(optional)" - > - <gl-toggle-stub - label="Self-monitoring" - labelposition="top" - /> - </gl-form-group-stub> - </form> - </div> - - <gl-modal-stub - arialabel="" - cancel-title="Cancel" - category="primary" - dismisslabel="Close" - modalclass="" - modalid="delete-self-monitor-modal" - ok-title="Delete self-monitoring project" - ok-variant="danger" - size="md" - title="Deactivate self-monitoring?" - titletag="h4" - > - <div> - - Deactivating self-monitoring deletes the self-monitoring project. Are you sure you want to deactivate self-monitoring and delete the project? - - </div> - </gl-modal-stub> -</section> -`; diff --git a/spec/frontend/self_monitor/components/self_monitor_form_spec.js b/spec/frontend/self_monitor/components/self_monitor_form_spec.js deleted file mode 100644 index 35f2734dde3..00000000000 --- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js +++ /dev/null @@ -1,95 +0,0 @@ -import { GlButton, GlToggle } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { TEST_HOST } from 'helpers/test_constants'; -import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue'; -import { createStore } from '~/self_monitor/store'; - -describe('self-monitor component', () => { - let wrapper; - let store; - - describe('When the self-monitor project has not been created', () => { - beforeEach(() => { - store = createStore({ - projectEnabled: false, - selfMonitoringProjectExists: false, - createSelfMonitoringProjectPath: '/create', - deleteSelfMonitoringProjectPath: '/delete', - }); - }); - - afterEach(() => { - if (wrapper.destroy) { - wrapper.destroy(); - } - }); - - describe('default state', () => { - it('to match the default snapshot', () => { - wrapper = shallowMount(SelfMonitor, { store }); - - expect(wrapper.element).toMatchSnapshot(); - }); - }); - - it('renders header text', () => { - wrapper = shallowMount(SelfMonitor, { store }); - - expect(wrapper.find('.js-section-header').text()).toBe('Self-monitoring'); - }); - - describe('expand/collapse button', () => { - it('renders as an expand button by default', () => { - wrapper = shallowMount(SelfMonitor, { store }); - - const button = wrapper.findComponent(GlButton); - - expect(button.text()).toBe('Expand'); - }); - }); - - describe('sub-header', () => { - it('renders descriptive text', () => { - wrapper = shallowMount(SelfMonitor, { store }); - - expect(wrapper.find('.js-section-sub-header').text()).toContain( - 'Activate or deactivate instance self-monitoring.', - ); - }); - }); - - describe('settings-content', () => { - it('renders the form description without a link', () => { - wrapper = shallowMount(SelfMonitor, { store }); - - expect(wrapper.vm.selfMonitoringFormText).toContain( - 'Activate self-monitoring to create a project to use to monitor the health of your instance.', - ); - }); - - it('renders the form description with a link', () => { - store = createStore({ - projectEnabled: true, - selfMonitoringProjectExists: true, - createSelfMonitoringProjectPath: '/create', - deleteSelfMonitoringProjectPath: '/delete', - selfMonitoringProjectFullPath: 'instance-administrators-random/gitlab-self-monitoring', - }); - - wrapper = shallowMount(SelfMonitor, { store }); - - expect( - wrapper.findComponent({ ref: 'selfMonitoringFormText' }).find('a').attributes('href'), - ).toEqual(`${TEST_HOST}/instance-administrators-random/gitlab-self-monitoring`); - }); - - it('renders toggle', () => { - wrapper = shallowMount(SelfMonitor, { store }); - - expect(wrapper.findComponent(GlToggle).props('label')).toBe( - SelfMonitor.formLabels.createProject, - ); - }); - }); - }); -}); diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js deleted file mode 100644 index 0e28e330009..00000000000 --- a/spec/frontend/self_monitor/store/actions_spec.js +++ /dev/null @@ -1,254 +0,0 @@ -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import { - HTTP_STATUS_ACCEPTED, - HTTP_STATUS_INTERNAL_SERVER_ERROR, - HTTP_STATUS_OK, -} from '~/lib/utils/http_status'; -import * as actions from '~/self_monitor/store/actions'; -import * as types from '~/self_monitor/store/mutation_types'; -import createState from '~/self_monitor/store/state'; - -describe('self-monitor actions', () => { - let state; - let mock; - - beforeEach(() => { - state = createState(); - mock = new MockAdapter(axios); - }); - - describe('setSelfMonitor', () => { - it('commits the SET_ENABLED mutation', () => { - return testAction( - actions.setSelfMonitor, - null, - state, - [{ type: types.SET_ENABLED, payload: null }], - [], - ); - }); - }); - - describe('resetAlert', () => { - it('commits the SET_ENABLED mutation', () => { - return testAction( - actions.resetAlert, - null, - state, - [{ type: types.SET_SHOW_ALERT, payload: false }], - [], - ); - }); - }); - - describe('requestCreateProject', () => { - describe('success', () => { - beforeEach(() => { - state.createProjectEndpoint = '/create'; - state.createProjectStatusEndpoint = '/create_status'; - mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, { - job_id: '123', - }); - mock.onGet(state.createProjectStatusEndpoint).reply(HTTP_STATUS_OK, { - project_full_path: '/self-monitor-url', - }); - }); - - it('dispatches status request with job data', () => { - return testAction( - actions.requestCreateProject, - null, - state, - [ - { - type: types.SET_LOADING, - payload: true, - }, - ], - [ - { - type: 'requestCreateProjectStatus', - payload: '123', - }, - ], - ); - }); - - it('dispatches success with project path', () => { - return testAction( - actions.requestCreateProjectStatus, - null, - state, - [], - [ - { - type: 'requestCreateProjectSuccess', - payload: { project_full_path: '/self-monitor-url' }, - }, - ], - ); - }); - }); - - describe('error', () => { - beforeEach(() => { - state.createProjectEndpoint = '/create'; - mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches error', () => { - return testAction( - actions.requestCreateProject, - null, - state, - [ - { - type: types.SET_LOADING, - payload: true, - }, - ], - [ - { - type: 'requestCreateProjectError', - payload: new Error('Request failed with status code 500'), - }, - ], - ); - }); - }); - - describe('requestCreateProjectSuccess', () => { - it('should commit the received data', () => { - return testAction( - actions.requestCreateProjectSuccess, - { project_full_path: '/self-monitor-url' }, - state, - [ - { type: types.SET_LOADING, payload: false }, - { type: types.SET_PROJECT_URL, payload: '/self-monitor-url' }, - { - type: types.SET_ALERT_CONTENT, - payload: { - actionName: 'viewSelfMonitorProject', - actionText: 'View project', - message: 'Self-monitoring project successfully created.', - }, - }, - { type: types.SET_SHOW_ALERT, payload: true }, - { type: types.SET_PROJECT_CREATED, payload: true }, - ], - [ - { - payload: true, - type: 'setSelfMonitor', - }, - ], - ); - }); - }); - }); - - describe('deleteSelfMonitorProject', () => { - describe('success', () => { - beforeEach(() => { - state.deleteProjectEndpoint = '/delete'; - state.deleteProjectStatusEndpoint = '/delete-status'; - mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, { - job_id: '456', - }); - mock.onGet(state.deleteProjectStatusEndpoint).reply(HTTP_STATUS_OK, { - status: 'success', - }); - }); - - it('dispatches status request with job data', () => { - return testAction( - actions.requestDeleteProject, - null, - state, - [ - { - type: types.SET_LOADING, - payload: true, - }, - ], - [ - { - type: 'requestDeleteProjectStatus', - payload: '456', - }, - ], - ); - }); - - it('dispatches success with status', () => { - return testAction( - actions.requestDeleteProjectStatus, - null, - state, - [], - [ - { - type: 'requestDeleteProjectSuccess', - payload: { status: 'success' }, - }, - ], - ); - }); - }); - - describe('error', () => { - beforeEach(() => { - state.deleteProjectEndpoint = '/delete'; - mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches error', () => { - return testAction( - actions.requestDeleteProject, - null, - state, - [ - { - type: types.SET_LOADING, - payload: true, - }, - ], - [ - { - type: 'requestDeleteProjectError', - payload: new Error('Request failed with status code 500'), - }, - ], - ); - }); - }); - - describe('requestDeleteProjectSuccess', () => { - it('should commit mutations to remove previously set data', () => { - return testAction( - actions.requestDeleteProjectSuccess, - null, - state, - [ - { type: types.SET_PROJECT_URL, payload: '' }, - { type: types.SET_PROJECT_CREATED, payload: false }, - { - type: types.SET_ALERT_CONTENT, - payload: { - actionName: 'createProject', - actionText: 'Undo', - message: 'Self-monitoring project successfully deleted.', - }, - }, - { type: types.SET_SHOW_ALERT, payload: true }, - { type: types.SET_LOADING, payload: false }, - ], - [], - ); - }); - }); - }); -}); diff --git a/spec/frontend/self_monitor/store/mutations_spec.js b/spec/frontend/self_monitor/store/mutations_spec.js deleted file mode 100644 index 315450f3aef..00000000000 --- a/spec/frontend/self_monitor/store/mutations_spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import mutations from '~/self_monitor/store/mutations'; -import createState from '~/self_monitor/store/state'; - -describe('self-monitoring mutations', () => { - let localState; - - beforeEach(() => { - localState = createState(); - }); - - describe('SET_ENABLED', () => { - it('sets selfMonitor', () => { - mutations.SET_ENABLED(localState, true); - - expect(localState.projectEnabled).toBe(true); - }); - }); - - describe('SET_PROJECT_CREATED', () => { - it('sets projectCreated', () => { - mutations.SET_PROJECT_CREATED(localState, true); - - expect(localState.projectCreated).toBe(true); - }); - }); - - describe('SET_SHOW_ALERT', () => { - it('sets showAlert', () => { - mutations.SET_SHOW_ALERT(localState, true); - - expect(localState.showAlert).toBe(true); - }); - }); - - describe('SET_PROJECT_URL', () => { - it('sets projectPath', () => { - mutations.SET_PROJECT_URL(localState, '/url/'); - - expect(localState.projectPath).toBe('/url/'); - }); - }); - - describe('SET_LOADING', () => { - it('sets loading', () => { - mutations.SET_LOADING(localState, true); - - expect(localState.loading).toBe(true); - }); - }); - - describe('SET_ALERT_CONTENT', () => { - it('set alertContent', () => { - const alertContent = { - message: 'success', - actionText: 'undo', - actionName: 'createProject', - }; - - mutations.SET_ALERT_CONTENT(localState, alertContent); - - expect(localState.alertContent).toBe(alertContent); - }); - }); -}); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js index 0a17c5f8721..d74cea2827c 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/dropdown_contents_spec.js @@ -2,9 +2,13 @@ import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; import Vuex from 'vuex'; -import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants'; import DropdownContents from '~/sidebar/components/labels/labels_select_vue/dropdown_contents.vue'; import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store'; +import { + VARIANT_EMBEDDED, + VARIANT_SIDEBAR, + VARIANT_STANDALONE, +} from '~/sidebar/components/labels/labels_select_widget/constants'; import { mockConfig } from './mock_data'; @@ -50,10 +54,10 @@ describe('DropdownContent', () => { describe('when `renderOnTop` is true', () => { it.each` - variant | expected - ${DropdownVariant.Sidebar} | ${'bottom: 3rem'} - ${DropdownVariant.Standalone} | ${'bottom: 2rem'} - ${DropdownVariant.Embedded} | ${'bottom: 2rem'} + variant | expected + ${VARIANT_SIDEBAR} | ${'bottom: 3rem'} + ${VARIANT_STANDALONE} | ${'bottom: 2rem'} + ${VARIANT_EMBEDDED} | ${'bottom: 2rem'} `('renders upward for $variant variant', ({ variant, expected }) => { wrapper = createComponent({ ...mockConfig, variant }, { renderOnTop: true }); diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js index 806064b2202..3add96f2c03 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_vue/labels_select_root_spec.js @@ -3,15 +3,18 @@ import Vue, { nextTick } from 'vue'; import Vuex from 'vuex'; import { isInViewport } from '~/lib/utils/common_utils'; -import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants'; import DropdownButton from '~/sidebar/components/labels/labels_select_vue/dropdown_button.vue'; import DropdownContents from '~/sidebar/components/labels/labels_select_vue/dropdown_contents.vue'; import DropdownTitle from '~/sidebar/components/labels/labels_select_vue/dropdown_title.vue'; import DropdownValue from '~/sidebar/components/labels/labels_select_vue/dropdown_value.vue'; import DropdownValueCollapsed from '~/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue'; import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue'; - import labelsSelectModule from '~/sidebar/components/labels/labels_select_vue/store'; +import { + VARIANT_EMBEDDED, + VARIANT_SIDEBAR, + VARIANT_STANDALONE, +} from '~/sidebar/components/labels/labels_select_widget/constants'; import { mockConfig } from './mock_data'; @@ -169,7 +172,7 @@ describe('LabelsSelectRoot', () => { }); describe('sets content direction based on viewport', () => { - describe.each(Object.values(DropdownVariant))( + describe.each(Object.values([VARIANT_EMBEDDED, VARIANT_SIDEBAR, VARIANT_STANDALONE]))( 'when labels variant is "%s"', ({ variant }) => { beforeEach(() => { diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js index 1abc708b72f..c939856331d 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view_spec.js @@ -11,7 +11,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/alert'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants'; +import { VARIANT_SIDEBAR } from '~/sidebar/components/labels/labels_select_widget/constants'; import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue'; import projectLabelsQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql'; import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue'; @@ -48,7 +48,7 @@ describe('DropdownContentsLabelsView', () => { wrapper = shallowMount(DropdownContentsLabelsView, { apolloProvider: mockApollo, provide: { - variant: DropdownVariant.Sidebar, + variant: VARIANT_SIDEBAR, ...injected, }, propsData: { diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js index c0417a2a40b..3abd87a69d6 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/dropdown_contents_spec.js @@ -1,6 +1,9 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; -import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants'; +import { + VARIANT_EMBEDDED, + VARIANT_STANDALONE, +} from '~/sidebar/components/labels/labels_select_widget/constants'; import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue'; import DropdownContentsCreateView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue'; import DropdownContentsLabelsView from '~/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue'; @@ -89,7 +92,7 @@ describe('DropdownContent', () => { }); it('emits `setLabels` event on dropdown hide if labels changed on non-sidebar widget', async () => { - createComponent({ props: { variant: DropdownVariant.Standalone } }); + createComponent({ props: { variant: VARIANT_STANDALONE } }); const updatedLabel = { id: 28, title: 'Bug', @@ -105,7 +108,7 @@ describe('DropdownContent', () => { }); it('emits `setLabels` event on visibility change if labels changed on sidebar widget', async () => { - createComponent({ props: { variant: DropdownVariant.Standalone, isVisible: true } }); + createComponent({ props: { variant: VARIANT_STANDALONE, isVisible: true } }); const updatedLabel = { id: 28, title: 'Bug', @@ -204,13 +207,13 @@ describe('DropdownContent', () => { }); it('does not render footer on standalone dropdown', () => { - createComponent({ props: { variant: DropdownVariant.Standalone } }); + createComponent({ props: { variant: VARIANT_STANDALONE } }); expect(findDropdownFooter().exists()).toBe(false); }); it('renders footer on embedded dropdown', () => { - createComponent({ props: { variant: DropdownVariant.Embedded } }); + createComponent({ props: { variant: VARIANT_EMBEDDED } }); expect(findDropdownFooter().exists()).toBe(true); }); diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js index cadcf8c08a3..1972e4805d0 100644 --- a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js +++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js @@ -9,6 +9,7 @@ import { toggleSuperSidebarCollapsed, initSuperSidebarCollapsedState, findPage, + bindSuperSidebarCollapsedEvents, } from '~/super_sidebar/super_sidebar_collapsed_state_manager'; const { xl, sm } = breakpoints; @@ -91,4 +92,38 @@ describe('Super Sidebar Collapsed State Manager', () => { }, ); }); + + describe('bindSuperSidebarCollapsedEvents', () => { + describe('handles width change', () => { + let removeEventListener; + + afterEach(() => { + removeEventListener(); + }); + + it.each` + initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize + ${xl} | ${sm} | ${false} | ${true} + ${sm} | ${xl} | ${true} | ${false} + ${xl} | ${xl} | ${false} | ${false} + ${sm} | ${sm} | ${true} | ${true} + `( + 'when changing width from $initialWindowWidth to $updatedWindowWidth expect the page to be initialized to be done $timesInitalised times', + ({ initialWindowWidth, updatedWindowWidth, hasClassBeforeResize, hasClassAfterResize }) => { + getCookie.mockReturnValue(undefined); + window.innerWidth = initialWindowWidth; + initSuperSidebarCollapsedState(); + + pageHasCollapsedClass(hasClassBeforeResize); + + removeEventListener = bindSuperSidebarCollapsedEvents(); + + window.innerWidth = updatedWindowWidth; + window.dispatchEvent(new Event('resize')); + + pageHasCollapsedClass(hasClassAfterResize); + }, + ); + }); + }); }); diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js index ce9e23d9a00..679b1f082b4 100644 --- a/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js +++ b/spec/frontend/vue_shared/issuable/create/components/issuable_label_selector_spec.js @@ -6,7 +6,7 @@ import { } from 'jest/sidebar/components/labels/labels_select_widget/mock_data'; import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue'; import LabelsSelect from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue'; -import { DropdownVariant } from '~/sidebar/components/labels/labels_select_widget/constants'; +import { VARIANT_EMBEDDED } from '~/sidebar/components/labels/labels_select_widget/constants'; import { WORKSPACE_PROJECT } from '~/issues/constants'; import { __ } from '~/locale'; @@ -18,7 +18,7 @@ const labelsFilterBasePath = '/labels-filter-base-path'; const initialLabels = []; const issuableType = 'issue'; const labelType = WORKSPACE_PROJECT; -const variant = DropdownVariant.Embedded; +const variant = VARIANT_EMBEDDED; const workspaceType = WORKSPACE_PROJECT; describe('IssuableLabelSelector', () => { diff --git a/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb new file mode 100644 index 00000000000..aaf8c124a83 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_ci_queuing_tables_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillCiQueuingTables, :migration, + :suppress_gitlab_schemas_validate_connection, schema: 20220208115439 do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:ci_cd_settings) { table(:project_ci_cd_settings) } + let(:builds) { table(:ci_builds) } + let(:queuing_entries) { table(:ci_pending_builds) } + let(:tags) { table(:tags) } + let(:taggings) { table(:taggings) } + + subject { described_class.new } + + describe '#perform' do + let!(:namespace) do + namespaces.create!( + id: 10, + name: 'namespace10', + path: 'namespace10', + traversal_ids: [10]) + end + + let!(:other_namespace) do + namespaces.create!( + id: 11, + name: 'namespace11', + path: 'namespace11', + traversal_ids: [11]) + end + + let!(:project) do + projects.create!(id: 5, namespace_id: 10, name: 'test1', path: 'test1') + end + + let!(:ci_cd_setting) do + ci_cd_settings.create!(id: 5, project_id: 5, group_runners_enabled: true) + end + + let!(:other_project) do + projects.create!(id: 7, namespace_id: 11, name: 'test2', path: 'test2') + end + + let!(:other_ci_cd_setting) do + ci_cd_settings.create!(id: 7, project_id: 7, group_runners_enabled: false) + end + + let!(:another_project) do + projects.create!(id: 9, namespace_id: 10, name: 'test3', path: 'test3', shared_runners_enabled: false) + end + + let!(:ruby_tag) do + tags.create!(id: 22, name: 'ruby') + end + + let!(:postgres_tag) do + tags.create!(id: 23, name: 'postgres') + end + + it 'creates ci_pending_builds for all pending builds in range' do + builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build') + builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build') + builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build') + + taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 22) + taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23) + + builds.create!(id: 60, status: :pending, name: 'test1', project_id: 7, type: 'Ci::Build') + builds.create!(id: 61, status: :running, name: 'test2', project_id: 7, protected: true, type: 'Ci::Build') + builds.create!(id: 62, status: :pending, name: 'test3', project_id: 7, type: 'Ci::Build') + + taggings.create!(taggable_id: 60, taggable_type: 'CommitStatus', tag_id: 23) + taggings.create!(taggable_id: 62, taggable_type: 'CommitStatus', tag_id: 22) + + builds.create!(id: 70, status: :pending, name: 'test1', project_id: 9, protected: true, type: 'Ci::Build') + builds.create!(id: 71, status: :failed, name: 'test2', project_id: 9, type: 'Ci::Build') + builds.create!(id: 72, status: :pending, name: 'test3', project_id: 9, type: 'Ci::Build') + + taggings.create!(taggable_id: 71, taggable_type: 'CommitStatus', tag_id: 22) + + subject.perform(1, 100) + + expect(queuing_entries.all).to contain_exactly( + an_object_having_attributes( + build_id: 50, + project_id: 5, + namespace_id: 10, + protected: false, + instance_runners_enabled: true, + minutes_exceeded: false, + tag_ids: [], + namespace_traversal_ids: [10]), + an_object_having_attributes( + build_id: 52, + project_id: 5, + namespace_id: 10, + protected: true, + instance_runners_enabled: true, + minutes_exceeded: false, + tag_ids: match_array([22, 23]), + namespace_traversal_ids: [10]), + an_object_having_attributes( + build_id: 60, + project_id: 7, + namespace_id: 11, + protected: false, + instance_runners_enabled: true, + minutes_exceeded: false, + tag_ids: [23], + namespace_traversal_ids: []), + an_object_having_attributes( + build_id: 62, + project_id: 7, + namespace_id: 11, + protected: false, + instance_runners_enabled: true, + minutes_exceeded: false, + tag_ids: [22], + namespace_traversal_ids: []), + an_object_having_attributes( + build_id: 70, + project_id: 9, + namespace_id: 10, + protected: true, + instance_runners_enabled: false, + minutes_exceeded: false, + tag_ids: [], + namespace_traversal_ids: []), + an_object_having_attributes( + build_id: 72, + project_id: 9, + namespace_id: 10, + protected: false, + instance_runners_enabled: false, + minutes_exceeded: false, + tag_ids: [], + namespace_traversal_ids: []) + ) + end + + it 'skips builds that already have ci_pending_builds' do + builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build') + builds.create!(id: 51, status: :created, name: 'test2', project_id: 5, type: 'Ci::Build') + builds.create!(id: 52, status: :pending, name: 'test3', project_id: 5, protected: true, type: 'Ci::Build') + + taggings.create!(taggable_id: 50, taggable_type: 'CommitStatus', tag_id: 22) + taggings.create!(taggable_id: 52, taggable_type: 'CommitStatus', tag_id: 23) + + queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10) + + subject.perform(1, 100) + + expect(queuing_entries.all).to contain_exactly( + an_object_having_attributes( + build_id: 50, + project_id: 5, + namespace_id: 10, + protected: false, + instance_runners_enabled: false, + minutes_exceeded: false, + tag_ids: [], + namespace_traversal_ids: []), + an_object_having_attributes( + build_id: 52, + project_id: 5, + namespace_id: 10, + protected: true, + instance_runners_enabled: true, + minutes_exceeded: false, + tag_ids: [23], + namespace_traversal_ids: [10]) + ) + end + + it 'upserts values in case of conflicts' do + builds.create!(id: 50, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build') + queuing_entries.create!(build_id: 50, project_id: 5, namespace_id: 10) + + build = described_class::Ci::Build.find(50) + described_class::Ci::PendingBuild.upsert_from_build!(build) + + expect(queuing_entries.all).to contain_exactly( + an_object_having_attributes( + build_id: 50, + project_id: 5, + namespace_id: 10, + protected: false, + instance_runners_enabled: true, + minutes_exceeded: false, + tag_ids: [], + namespace_traversal_ids: [10]) + ) + end + end + + context 'Ci::Build' do + describe '.each_batch' do + let(:model) { described_class::Ci::Build } + + before do + builds.create!(id: 1, status: :pending, name: 'test1', project_id: 5, type: 'Ci::Build') + builds.create!(id: 2, status: :pending, name: 'test2', project_id: 5, type: 'Ci::Build') + builds.create!(id: 3, status: :pending, name: 'test3', project_id: 5, type: 'Ci::Build') + builds.create!(id: 4, status: :pending, name: 'test4', project_id: 5, type: 'Ci::Build') + builds.create!(id: 5, status: :pending, name: 'test5', project_id: 5, type: 'Ci::Build') + end + + it 'yields an ActiveRecord::Relation when a block is given' do + model.each_batch do |relation| + expect(relation).to be_a_kind_of(ActiveRecord::Relation) + end + end + + it 'yields a batch index as the second argument' do + model.each_batch do |_, index| + expect(index).to eq(1) + end + end + + it 'accepts a custom batch size' do + amount = 0 + + model.each_batch(of: 1) { amount += 1 } + + expect(amount).to eq(5) + end + + it 'does not include ORDER BYs in the yielded relations' do + model.each_batch do |relation| + expect(relation.to_sql).not_to include('ORDER BY') + end + end + + it 'orders ascending' do + ids = [] + + model.each_batch(of: 1) { |rel| ids.concat(rel.ids) } + + expect(ids).to eq(ids.sort) + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb b/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb index 2c2740434de..e0be5a785b8 100644 --- a/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_group_features_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillGroupFeatures, :migration, schema: 20220314184009 do +RSpec.describe Gitlab::BackgroundMigration::BackfillGroupFeatures, :migration, schema: 20220302114046 do let(:group_features) { table(:group_features) } let(:namespaces) { table(:namespaces) } diff --git a/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb b/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb new file mode 100644 index 00000000000..e6588644b4f --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_integrations_type_new_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillIntegrationsTypeNew, :migration, schema: 20220212120735 do + let(:migration) { described_class.new } + let(:integrations) { table(:integrations) } + + let(:namespaced_integrations) do + Set.new( + %w[ + Asana Assembla Bamboo Bugzilla Buildkite Campfire Confluence CustomIssueTracker Datadog + Discord DroneCi EmailsOnPush Ewm ExternalWiki Flowdock HangoutsChat Harbor Irker Jenkins Jira Mattermost + MattermostSlashCommands MicrosoftTeams MockCi MockMonitoring Packagist PipelinesEmail Pivotaltracker + Prometheus Pushover Redmine Shimo Slack SlackSlashCommands Teamcity UnifyCircuit WebexTeams Youtrack Zentao + Github GitlabSlackApplication + ]).freeze + end + + before do + integrations.connection.execute 'ALTER TABLE integrations DISABLE TRIGGER "trigger_type_new_on_insert"' + + namespaced_integrations.each_with_index do |type, i| + integrations.create!(id: i + 1, type: "#{type}Service") + end + + integrations.create!(id: namespaced_integrations.size + 1, type: 'LegacyService') + ensure + integrations.connection.execute 'ALTER TABLE integrations ENABLE TRIGGER "trigger_type_new_on_insert"' + end + + it 'backfills `type_new` for the selected records' do + # We don't want to mock `Kernel.sleep`, so instead we mock it on the migration + # class before it gets forwarded. + expect(migration).to receive(:sleep).with(0.05).exactly(5).times + + queries = ActiveRecord::QueryRecorder.new do + migration.perform(2, 10, :integrations, :id, 2, 50) + end + + expect(queries.count).to be(16) + expect(queries.log.grep(/^SELECT/).size).to be(11) + expect(queries.log.grep(/^UPDATE/).size).to be(5) + expect(queries.log.grep(/^UPDATE/).join.scan(/WHERE .*/)).to eq( + [ + 'WHERE integrations.id BETWEEN 2 AND 3', + 'WHERE integrations.id BETWEEN 4 AND 5', + 'WHERE integrations.id BETWEEN 6 AND 7', + 'WHERE integrations.id BETWEEN 8 AND 9', + 'WHERE integrations.id BETWEEN 10 AND 10' + ]) + + expect(integrations.where(id: 2..10).pluck(:type, :type_new)).to contain_exactly( + ['AssemblaService', 'Integrations::Assembla'], + ['BambooService', 'Integrations::Bamboo'], + ['BugzillaService', 'Integrations::Bugzilla'], + ['BuildkiteService', 'Integrations::Buildkite'], + ['CampfireService', 'Integrations::Campfire'], + ['ConfluenceService', 'Integrations::Confluence'], + ['CustomIssueTrackerService', 'Integrations::CustomIssueTracker'], + ['DatadogService', 'Integrations::Datadog'], + ['DiscordService', 'Integrations::Discord'] + ) + + expect(integrations.where.not(id: 2..10)).to all(have_attributes(type_new: nil)) + end +end diff --git a/spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb b/spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb index ea07079f9ee..e1ef12a1479 100644 --- a/spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_member_namespace_for_group_members_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillMemberNamespaceForGroupMembers, :migration, schema: 20220314184009 do +RSpec.describe Gitlab::BackgroundMigration::BackfillMemberNamespaceForGroupMembers, :migration, schema: 20220120211832 do let(:migration) { described_class.new } let(:members_table) { table(:members) } let(:namespaces_table) { table(:namespaces) } diff --git a/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb b/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb index f4e8fa1bbac..b821efcadb0 100644 --- a/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_namespace_id_for_namespace_route_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceIdForNamespaceRoute, :migration, schema: 20220314184009 do +RSpec.describe Gitlab::BackgroundMigration::BackfillNamespaceIdForNamespaceRoute, :migration, schema: 20220120123800 do let(:migration) { described_class.new } let(:namespaces_table) { table(:namespaces) } let(:projects_table) { table(:projects) } diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index 6f6ff9232e0..4a50d08b2aa 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 20220314184009, +RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, schema: 20211202041233, feature_category: :source_code_management do let(:gitlab_shell) { Gitlab::Shell.new } let(:users) { table(:users) } diff --git a/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb b/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb new file mode 100644 index 00000000000..c788b701d79 --- /dev/null +++ b/spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::EncryptIntegrationProperties, schema: 20220415124804 do + let(:integrations) do + table(:integrations) do |integrations| + integrations.send :attr_encrypted, :encrypted_properties_tmp, + attribute: :encrypted_properties, + mode: :per_attribute_iv, + key: ::Settings.attr_encrypted_db_key_base_32, + algorithm: 'aes-256-gcm', + marshal: true, + marshaler: ::Gitlab::Json, + encode: false, + encode_iv: false + end + end + + let!(:no_properties) { integrations.create! } + let!(:with_plaintext_1) { integrations.create!(properties: json_props(1)) } + let!(:with_plaintext_2) { integrations.create!(properties: json_props(2)) } + let!(:with_encrypted) do + x = integrations.new + x.properties = nil + x.encrypted_properties_tmp = some_props(3) + x.save! + x + end + + let(:start_id) { integrations.minimum(:id) } + let(:end_id) { integrations.maximum(:id) } + + it 'ensures all properties are encrypted', :aggregate_failures do + described_class.new.perform(start_id, end_id) + + props = integrations.all.to_h do |record| + [record.id, [Gitlab::Json.parse(record.properties), record.encrypted_properties_tmp]] + end + + expect(integrations.count).to eq(4) + + expect(props).to match( + no_properties.id => both(be_nil), + with_plaintext_1.id => both(eq some_props(1)), + with_plaintext_2.id => both(eq some_props(2)), + with_encrypted.id => match([be_nil, eq(some_props(3))]) + ) + end + + private + + def both(obj) + match [obj, obj] + end + + def some_props(id) + HashWithIndifferentAccess.new({ id: id, foo: 1, bar: true, baz: %w[a string array] }) + end + + def json_props(id) + some_props(id).to_json + end +end diff --git a/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb b/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb new file mode 100644 index 00000000000..4e7b97d33f6 --- /dev/null +++ b/spec/lib/gitlab/background_migration/encrypt_static_object_token_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::EncryptStaticObjectToken do + let(:users) { table(:users) } + let!(:user_without_tokens) { create_user!(name: 'notoken') } + let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') } + let!(:user_with_plaintext_token_2) { create_user!(name: 'plaintext_2', token: 'TOKEN') } + let!(:user_with_plaintext_empty_token) { create_user!(name: 'plaintext_3', token: '') } + let!(:user_with_encrypted_token) { create_user!(name: 'encrypted', encrypted_token: 'encrypted') } + let!(:user_with_both_tokens) { create_user!(name: 'both', token: 'token2', encrypted_token: 'encrypted2') } + + before do + allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).and_call_original + allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).with('token') { 'secure_token' } + allow(Gitlab::CryptoHelper).to receive(:aes256_gcm_encrypt).with('TOKEN') { 'SECURE_TOKEN' } + end + + subject { described_class.new.perform(start_id, end_id) } + + let(:start_id) { users.minimum(:id) } + let(:end_id) { users.maximum(:id) } + + it 'backfills encrypted tokens to users with plaintext token only', :aggregate_failures do + subject + + new_state = users.pluck(:id, :static_object_token, :static_object_token_encrypted).to_h do |row| + [row[0], [row[1], row[2]]] + end + + expect(new_state.count).to eq(6) + + expect(new_state[user_with_plaintext_token_1.id]).to match_array(%w[token secure_token]) + expect(new_state[user_with_plaintext_token_2.id]).to match_array(%w[TOKEN SECURE_TOKEN]) + + expect(new_state[user_with_plaintext_empty_token.id]).to match_array(['', nil]) + expect(new_state[user_without_tokens.id]).to match_array([nil, nil]) + expect(new_state[user_with_both_tokens.id]).to match_array(%w[token2 encrypted2]) + expect(new_state[user_with_encrypted_token.id]).to match_array([nil, 'encrypted']) + end + + context 'when id range does not include existing user ids' do + let(:arguments) { [non_existing_record_id, non_existing_record_id.succ] } + + it_behaves_like 'marks background migration job records' do + subject { described_class.new } + end + end + + private + + def create_user!(name:, token: nil, encrypted_token: nil) + email = "#{name}@example.com" + + table(:users).create!( + name: name, + email: email, + username: name, + projects_limit: 0, + static_object_token: token, + static_object_token_encrypted: encrypted_token + ) + end +end diff --git a/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb b/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb index 3cbc05b762a..af551861d47 100644 --- a/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb +++ b/spec/lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::FixVulnerabilityOccurrencesWithHashesAsRawMetadata, schema: 20220314184009 do +RSpec.describe Gitlab::BackgroundMigration::FixVulnerabilityOccurrencesWithHashesAsRawMetadata, schema: 20211209203821 do let(:users) { table(:users) } let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } diff --git a/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb b/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb new file mode 100644 index 00000000000..2c2c048992f --- /dev/null +++ b/spec/lib/gitlab/background_migration/merge_topics_with_same_name_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::MergeTopicsWithSameName, schema: 20220331133802 do + def set_avatar(topic_id, avatar) + topic = ::Projects::Topic.find(topic_id) + topic.avatar = avatar + topic.save! + topic.avatar.absolute_path + end + + it 'merges project topics with same case insensitive name' do + namespaces = table(:namespaces) + projects = table(:projects) + topics = table(:topics) + project_topics = table(:project_topics) + + group_1 = namespaces.create!(name: 'space1', type: 'Group', path: 'space1') + group_2 = namespaces.create!(name: 'space2', type: 'Group', path: 'space2') + group_3 = namespaces.create!(name: 'space3', type: 'Group', path: 'space3') + proj_space_1 = namespaces.create!(name: 'proj1', path: 'proj1', type: 'Project', parent_id: group_1.id) + proj_space_2 = namespaces.create!(name: 'proj2', path: 'proj2', type: 'Project', parent_id: group_2.id) + proj_space_3 = namespaces.create!(name: 'proj3', path: 'proj3', type: 'Project', parent_id: group_3.id) + project_1 = projects.create!(namespace_id: group_1.id, project_namespace_id: proj_space_1.id, visibility_level: 20) + project_2 = projects.create!(namespace_id: group_2.id, project_namespace_id: proj_space_2.id, visibility_level: 10) + project_3 = projects.create!(namespace_id: group_3.id, project_namespace_id: proj_space_3.id, visibility_level: 0) + topic_1_keep = topics.create!( + name: 'topic1', + title: 'Topic 1', + description: 'description 1 to keep', + total_projects_count: 2, + non_private_projects_count: 2 + ) + topic_1_remove = topics.create!( + name: 'TOPIC1', + title: 'Topic 1', + description: 'description 1 to remove', + total_projects_count: 2, + non_private_projects_count: 1 + ) + topic_2_remove = topics.create!( + name: 'topic2', + title: 'Topic 2', + total_projects_count: 0 + ) + topic_2_keep = topics.create!( + name: 'TOPIC2', + title: 'Topic 2', + description: 'description 2 to keep', + total_projects_count: 1 + ) + topic_3_remove_1 = topics.create!( + name: 'topic3', + title: 'Topic 3', + total_projects_count: 2, + non_private_projects_count: 1 + ) + topic_3_keep = topics.create!( + name: 'Topic3', + title: 'Topic 3', + total_projects_count: 2, + non_private_projects_count: 2 + ) + topic_3_remove_2 = topics.create!( + name: 'TOPIC3', + title: 'Topic 3', + description: 'description 3 to keep', + total_projects_count: 2, + non_private_projects_count: 1 + ) + topic_4_keep = topics.create!( + name: 'topic4', + title: 'Topic 4' + ) + + project_topics_1 = [] + project_topics_3 = [] + project_topics_removed = [] + + project_topics_1 << project_topics.create!(topic_id: topic_1_keep.id, project_id: project_1.id) + project_topics_1 << project_topics.create!(topic_id: topic_1_keep.id, project_id: project_2.id) + project_topics_removed << project_topics.create!(topic_id: topic_1_remove.id, project_id: project_2.id) + project_topics_1 << project_topics.create!(topic_id: topic_1_remove.id, project_id: project_3.id) + + project_topics_3 << project_topics.create!(topic_id: topic_3_keep.id, project_id: project_1.id) + project_topics_3 << project_topics.create!(topic_id: topic_3_keep.id, project_id: project_2.id) + project_topics_removed << project_topics.create!(topic_id: topic_3_remove_1.id, project_id: project_1.id) + project_topics_3 << project_topics.create!(topic_id: topic_3_remove_1.id, project_id: project_3.id) + project_topics_removed << project_topics.create!(topic_id: topic_3_remove_2.id, project_id: project_1.id) + project_topics_removed << project_topics.create!(topic_id: topic_3_remove_2.id, project_id: project_3.id) + + avatar_paths = { + topic_1_keep: set_avatar(topic_1_keep.id, fixture_file_upload('spec/fixtures/avatars/avatar1.png')), + topic_1_remove: set_avatar(topic_1_remove.id, fixture_file_upload('spec/fixtures/avatars/avatar2.png')), + topic_2_remove: set_avatar(topic_2_remove.id, fixture_file_upload('spec/fixtures/avatars/avatar3.png')), + topic_3_remove_1: set_avatar(topic_3_remove_1.id, fixture_file_upload('spec/fixtures/avatars/avatar4.png')), + topic_3_remove_2: set_avatar(topic_3_remove_2.id, fixture_file_upload('spec/fixtures/avatars/avatar5.png')) + } + + subject.perform(%w[topic1 topic2 topic3 topic4]) + + # Topics + [topic_1_keep, topic_2_keep, topic_3_keep, topic_4_keep].each(&:reload) + expect(topic_1_keep.name).to eq('topic1') + expect(topic_1_keep.description).to eq('description 1 to keep') + expect(topic_1_keep.total_projects_count).to eq(3) + expect(topic_1_keep.non_private_projects_count).to eq(2) + expect(topic_2_keep.name).to eq('TOPIC2') + expect(topic_2_keep.description).to eq('description 2 to keep') + expect(topic_2_keep.total_projects_count).to eq(0) + expect(topic_2_keep.non_private_projects_count).to eq(0) + expect(topic_3_keep.name).to eq('Topic3') + expect(topic_3_keep.description).to eq('description 3 to keep') + expect(topic_3_keep.total_projects_count).to eq(3) + expect(topic_3_keep.non_private_projects_count).to eq(2) + expect(topic_4_keep.reload.name).to eq('topic4') + + [topic_1_remove, topic_2_remove, topic_3_remove_1, topic_3_remove_2].each do |topic| + expect { topic.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + + # Topic avatars + expect(topic_1_keep.avatar).to eq('avatar1.png') + expect(File.exist?(::Projects::Topic.find(topic_1_keep.id).avatar.absolute_path)).to be_truthy + expect(topic_2_keep.avatar).to eq('avatar3.png') + expect(File.exist?(::Projects::Topic.find(topic_2_keep.id).avatar.absolute_path)).to be_truthy + expect(topic_3_keep.avatar).to eq('avatar4.png') + expect(File.exist?(::Projects::Topic.find(topic_3_keep.id).avatar.absolute_path)).to be_truthy + + [:topic_1_remove, :topic_2_remove, :topic_3_remove_1, :topic_3_remove_2].each do |topic| + expect(File.exist?(avatar_paths[topic])).to be_falsey + end + + # Project Topic assignments + project_topics_1.each do |project_topic| + expect(project_topic.reload.topic_id).to eq(topic_1_keep.id) + end + + project_topics_3.each do |project_topic| + expect(project_topic.reload.topic_id).to eq(topic_3_keep.id) + end + + project_topics_removed.each do |project_topic| + expect { project_topic.reload }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb b/spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb index 90d05ccbe1a..07e77bdbc13 100644 --- a/spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_personal_namespace_project_maintainer_to_owner_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::MigratePersonalNamespaceProjectMaintainerToOwner, :migration, schema: 20220314184009 do +RSpec.describe Gitlab::BackgroundMigration::MigratePersonalNamespaceProjectMaintainerToOwner, :migration, schema: 20220208080921 do let(:migration) { described_class.new } let(:users_table) { table(:users) } let(:members_table) { table(:members) } diff --git a/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb b/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb index 7c78350e697..2f0eef3c399 100644 --- a/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb +++ b/spec/lib/gitlab/background_migration/nullify_orphan_runner_id_on_ci_builds_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::NullifyOrphanRunnerIdOnCiBuilds, - :suppress_gitlab_schemas_validate_connection, migration: :gitlab_ci, schema: 20220314184009 do + :suppress_gitlab_schemas_validate_connection, migration: :gitlab_ci, schema: 20220223112304 do let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } let(:ci_runners) { table(:ci_runners) } diff --git a/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb b/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb new file mode 100644 index 00000000000..4a7d52ee784 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_namespace_statistics_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::PopulateNamespaceStatistics do + let!(:namespaces) { table(:namespaces) } + let!(:namespace_statistics) { table(:namespace_statistics) } + let!(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) } + let!(:dependency_proxy_blobs) { table(:dependency_proxy_blobs) } + + let!(:group1) { namespaces.create!(id: 10, type: 'Group', name: 'group1', path: 'group1') } + let!(:group2) { namespaces.create!(id: 20, type: 'Group', name: 'group2', path: 'group2') } + + let!(:group1_manifest) do + dependency_proxy_manifests.create!(group_id: 10, size: 20, file_name: 'test-file', file: 'test', digest: 'abc123') + end + + let!(:group2_manifest) do + dependency_proxy_manifests.create!(group_id: 20, size: 20, file_name: 'test-file', file: 'test', digest: 'abc123') + end + + let!(:group1_stats) { namespace_statistics.create!(id: 10, namespace_id: 10) } + + let(:ids) { namespaces.pluck(:id) } + let(:statistics) { [] } + + subject(:perform) { described_class.new.perform(ids, statistics) } + + it 'creates/updates all namespace_statistics and updates root storage statistics', :aggregate_failures do + expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(group1.id) + expect(Namespaces::ScheduleAggregationWorker).to receive(:perform_async).with(group2.id) + + expect { perform }.to change(namespace_statistics, :count).from(1).to(2) + + namespace_statistics.all.each do |stat| + expect(stat.dependency_proxy_size).to eq 20 + expect(stat.storage_size).to eq 20 + end + end + + context 'when just a stat is passed' do + let(:statistics) { [:dependency_proxy_size] } + + it 'calls the statistics update service with just that stat' do + expect(Groups::UpdateStatisticsService) + .to receive(:new) + .with(anything, statistics: [:dependency_proxy_size]) + .twice.and_call_original + + perform + end + end + + context 'when a statistics update fails' do + before do + error_response = instance_double(ServiceResponse, message: 'an error', error?: true) + + allow_next_instance_of(Groups::UpdateStatisticsService) do |instance| + allow(instance).to receive(:execute).and_return(error_response) + end + end + + it 'logs an error' do + expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance| + expect(instance).to receive(:error).twice + end + + perform + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb b/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb new file mode 100644 index 00000000000..e72e3392210 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_topics_non_private_projects_count_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::PopulateTopicsNonPrivateProjectsCount, schema: 20220125122640 do + it 'correctly populates the non private projects counters' do + namespaces = table(:namespaces) + projects = table(:projects) + topics = table(:topics) + project_topics = table(:project_topics) + + group = namespaces.create!(name: 'group', path: 'group') + project_public = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + project_internal = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::INTERNAL) + project_private = projects.create!(namespace_id: group.id, visibility_level: Gitlab::VisibilityLevel::PRIVATE) + topic_1 = topics.create!(name: 'Topic1') + topic_2 = topics.create!(name: 'Topic2') + topic_3 = topics.create!(name: 'Topic3') + topic_4 = topics.create!(name: 'Topic4') + topic_5 = topics.create!(name: 'Topic5') + topic_6 = topics.create!(name: 'Topic6') + topic_7 = topics.create!(name: 'Topic7') + topic_8 = topics.create!(name: 'Topic8') + + project_topics.create!(topic_id: topic_1.id, project_id: project_public.id) + project_topics.create!(topic_id: topic_2.id, project_id: project_internal.id) + project_topics.create!(topic_id: topic_3.id, project_id: project_private.id) + project_topics.create!(topic_id: topic_4.id, project_id: project_public.id) + project_topics.create!(topic_id: topic_4.id, project_id: project_internal.id) + project_topics.create!(topic_id: topic_5.id, project_id: project_public.id) + project_topics.create!(topic_id: topic_5.id, project_id: project_private.id) + project_topics.create!(topic_id: topic_6.id, project_id: project_internal.id) + project_topics.create!(topic_id: topic_6.id, project_id: project_private.id) + project_topics.create!(topic_id: topic_7.id, project_id: project_public.id) + project_topics.create!(topic_id: topic_7.id, project_id: project_internal.id) + project_topics.create!(topic_id: topic_7.id, project_id: project_private.id) + project_topics.create!(topic_id: topic_8.id, project_id: project_public.id) + + subject.perform(topic_1.id, topic_7.id) + + expect(topic_1.reload.non_private_projects_count).to eq(1) + expect(topic_2.reload.non_private_projects_count).to eq(1) + expect(topic_3.reload.non_private_projects_count).to eq(0) + expect(topic_4.reload.non_private_projects_count).to eq(2) + expect(topic_5.reload.non_private_projects_count).to eq(1) + expect(topic_6.reload.non_private_projects_count).to eq(1) + expect(topic_7.reload.non_private_projects_count).to eq(2) + expect(topic_8.reload.non_private_projects_count).to eq(0) + end +end diff --git a/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb new file mode 100644 index 00000000000..c0470f26d9e --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_vulnerability_reads_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::PopulateVulnerabilityReads, :migration, schema: 20220326161803 do + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + let(:vulnerability_issue_links) { table(:vulnerability_issue_links) } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(namespace_id: namespace.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + let(:sub_batch_size) { 1000 } + + before do + vulnerabilities_findings.connection.execute 'ALTER TABLE vulnerability_occurrences DISABLE TRIGGER "trigger_insert_or_update_vulnerability_reads_from_occurrences"' + vulnerabilities.connection.execute 'ALTER TABLE vulnerabilities DISABLE TRIGGER "trigger_update_vulnerability_reads_on_vulnerability_update"' + vulnerability_issue_links.connection.execute 'ALTER TABLE vulnerability_issue_links DISABLE TRIGGER "trigger_update_has_issues_on_vulnerability_issue_links_update"' + + 10.times.each do |x| + vulnerability = create_vulnerability!( + project_id: project.id, + report_type: 7, + author_id: user.id + ) + identifier = table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: Digest::SHA1.hexdigest(vulnerability.id.to_s), + name: 'Identifier for UUIDv5') + + create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + end + end + + it 'creates vulnerability_reads for the given records' do + described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size) + + expect(vulnerability_reads.count).to eq(10) + end + + it 'does not create new records when records already exists' do + described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size) + described_class.new.perform(vulnerabilities.first.id, vulnerabilities.last.id, sub_batch_size) + + expect(vulnerability_reads.count).to eq(10) + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb new file mode 100644 index 00000000000..543dd204f89 --- /dev/null +++ b/spec/lib/gitlab/background_migration/recalculate_vulnerabilities_occurrences_uuid_spec.rb @@ -0,0 +1,530 @@ +# frozen_string_literal: true + +require 'spec_helper' + +def create_background_migration_job(ids, status) + proper_status = case status + when :pending + Gitlab::Database::BackgroundMigrationJob.statuses['pending'] + when :succeeded + Gitlab::Database::BackgroundMigrationJob.statuses['succeeded'] + else + raise ArgumentError + end + + background_migration_jobs.create!( + class_name: 'RecalculateVulnerabilitiesOccurrencesUuid', + arguments: Array(ids), + status: proper_status, + created_at: Time.now.utc + ) +end + +RSpec.describe Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid, :suppress_gitlab_schemas_validate_connection, schema: 20211202041233 do + let(:background_migration_jobs) { table(:background_migration_jobs) } + let(:pending_jobs) { background_migration_jobs.where(status: Gitlab::Database::BackgroundMigrationJob.statuses['pending']) } + let(:succeeded_jobs) { background_migration_jobs.where(status: Gitlab::Database::BackgroundMigrationJob.statuses['succeeded']) } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:users) { table(:users) } + let(:user) { create_user! } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:scanners) { table(:vulnerability_scanners) } + let(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + let(:scanner2) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerability_findings) { table(:vulnerability_occurrences) } + let(:vulnerability_finding_pipelines) { table(:vulnerability_occurrence_pipelines) } + let(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) } + let(:vulnerability_identifiers) { table(:vulnerability_identifiers) } + + let(:identifier_1) { 'identifier-1' } + let!(:vulnerability_identifier) do + vulnerability_identifiers.create!( + project_id: project.id, + external_type: identifier_1, + external_id: identifier_1, + fingerprint: Gitlab::Database::ShaAttribute.serialize('ff9ef548a6e30a0462795d916f3f00d1e2b082ca'), + name: 'Identifier 1') + end + + let(:identifier_2) { 'identifier-2' } + let!(:vulnerability_identfier2) do + vulnerability_identifiers.create!( + project_id: project.id, + external_type: identifier_2, + external_id: identifier_2, + fingerprint: Gitlab::Database::ShaAttribute.serialize('4299e8ddd819f9bde9cfacf45716724c17b5ddf7'), + name: 'Identifier 2') + end + + let(:identifier_3) { 'identifier-3' } + let!(:vulnerability_identifier3) do + vulnerability_identifiers.create!( + project_id: project.id, + external_type: identifier_3, + external_id: identifier_3, + fingerprint: Gitlab::Database::ShaAttribute.serialize('8e91632f9c6671e951834a723ee221c44cc0d844'), + name: 'Identifier 3') + end + + let(:known_uuid_v4) { "b3cc2518-5446-4dea-871c-89d5e999c1ac" } + let(:known_uuid_v5) { "05377088-dc26-5161-920e-52a7159fdaa1" } + let(:desired_uuid_v5) { "f3e9a23f-9181-54bf-a5ab-c5bc7a9b881a" } + + subject { described_class.new.perform(start_id, end_id) } + + context "when finding has a UUIDv4" do + before do + @uuid_v4 = create_finding!( + vulnerability_id: nil, + project_id: project.id, + scanner_id: scanner2.id, + primary_identifier_id: vulnerability_identfier2.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"), + uuid: known_uuid_v4 + ) + end + + let(:start_id) { @uuid_v4.id } + let(:end_id) { @uuid_v4.id } + + it "replaces it with UUIDv5" do + expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v4]) + + subject + + expect(vulnerability_findings.pluck(:uuid)).to match_array([desired_uuid_v5]) + end + + it 'logs recalculation' do + expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance| + expect(instance).to receive(:info).twice + end + + subject + end + end + + context "when finding has a UUIDv5" do + before do + @uuid_v5 = create_finding!( + vulnerability_id: nil, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: vulnerability_identifier.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize("838574be0210968bf6b9f569df9c2576242cbf0a"), + uuid: known_uuid_v5 + ) + end + + let(:start_id) { @uuid_v5.id } + let(:end_id) { @uuid_v5.id } + + it "stays the same" do + expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v5]) + + subject + + expect(vulnerability_findings.pluck(:uuid)).to match_array([known_uuid_v5]) + end + end + + context 'if a duplicate UUID would be generated' do # rubocop: disable RSpec/MultipleMemoizedHelpers + let(:v1) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let!(:finding_with_incorrect_uuid) do + create_finding!( + vulnerability_id: v1.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: vulnerability_identifier.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: 'bd95c085-71aa-51d7-9bb6-08ae669c262e' + ) + end + + let(:v2) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let!(:finding_with_correct_uuid) do + create_finding!( + vulnerability_id: v2.id, + project_id: project.id, + primary_identifier_id: vulnerability_identifier.id, + scanner_id: scanner2.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: '91984483-5efe-5215-b471-d524ac5792b1' + ) + end + + let(:v3) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let!(:finding_with_incorrect_uuid2) do + create_finding!( + vulnerability_id: v3.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: vulnerability_identfier2.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: '00000000-1111-2222-3333-444444444444' + ) + end + + let(:v4) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let!(:finding_with_correct_uuid2) do + create_finding!( + vulnerability_id: v4.id, + project_id: project.id, + scanner_id: scanner2.id, + primary_identifier_id: vulnerability_identfier2.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: '1edd751e-ef9a-5391-94db-a832c8635bfc' + ) + end + + let!(:finding_with_incorrect_uuid3) do + create_finding!( + vulnerability_id: nil, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: vulnerability_identifier3.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: '22222222-3333-4444-5555-666666666666' + ) + end + + let!(:duplicate_not_in_the_same_batch) do + create_finding!( + id: 99999, + vulnerability_id: nil, + project_id: project.id, + scanner_id: scanner2.id, + primary_identifier_id: vulnerability_identifier3.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: '4564f9d5-3c6b-5cc3-af8c-7c25285362a7' + ) + end + + let(:start_id) { finding_with_incorrect_uuid.id } + let(:end_id) { finding_with_incorrect_uuid3.id } + + before do + 4.times do + create_finding_pipeline!(project_id: project.id, finding_id: finding_with_incorrect_uuid.id) + create_finding_pipeline!(project_id: project.id, finding_id: finding_with_correct_uuid.id) + create_finding_pipeline!(project_id: project.id, finding_id: finding_with_incorrect_uuid2.id) + create_finding_pipeline!(project_id: project.id, finding_id: finding_with_correct_uuid2.id) + end + end + + it 'drops duplicates and related records', :aggregate_failures do + expect(vulnerability_findings.pluck(:id)).to match_array( + [ + finding_with_correct_uuid.id, + finding_with_incorrect_uuid.id, + finding_with_correct_uuid2.id, + finding_with_incorrect_uuid2.id, + finding_with_incorrect_uuid3.id, + duplicate_not_in_the_same_batch.id + ]) + + expect { subject }.to change(vulnerability_finding_pipelines, :count).from(16).to(8) + .and change(vulnerability_findings, :count).from(6).to(3) + .and change(vulnerabilities, :count).from(4).to(2) + + expect(vulnerability_findings.pluck(:id)).to match_array([finding_with_incorrect_uuid.id, finding_with_incorrect_uuid2.id, finding_with_incorrect_uuid3.id]) + end + + context 'if there are conflicting UUID values within the batch' do # rubocop: disable RSpec/MultipleMemoizedHelpers + let(:end_id) { finding_with_broken_data_integrity.id } + let(:vulnerability_5) { create_vulnerability!(project_id: project.id, author_id: user.id) } + let(:different_project) { table(:projects).create!(namespace_id: namespace.id) } + let!(:identifier_with_broken_data_integrity) do + vulnerability_identifiers.create!( + project_id: different_project.id, + external_type: identifier_2, + external_id: identifier_2, + fingerprint: Gitlab::Database::ShaAttribute.serialize('4299e8ddd819f9bde9cfacf45716724c17b5ddf7'), + name: 'Identifier 2') + end + + let(:finding_with_broken_data_integrity) do + create_finding!( + vulnerability_id: vulnerability_5, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier_with_broken_data_integrity.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: SecureRandom.uuid + ) + end + + it 'deletes the conflicting record' do + expect { subject }.to change { vulnerability_findings.find_by_id(finding_with_broken_data_integrity.id) }.to(nil) + end + end + + context 'if a conflicting UUID is found during the migration' do # rubocop:disable RSpec/MultipleMemoizedHelpers + let(:finding_class) { Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid::VulnerabilitiesFinding } + let(:uuid) { '4564f9d5-3c6b-5cc3-af8c-7c25285362a7' } + + before do + exception = ActiveRecord::RecordNotUnique.new("(uuid)=(#{uuid})") + + call_count = 0 + allow(::Gitlab::Database::BulkUpdate).to receive(:execute) do + call_count += 1 + call_count.eql?(1) ? raise(exception) : {} + end + + allow(finding_class).to receive(:find_by).with(uuid: uuid).and_return(duplicate_not_in_the_same_batch) + end + + it 'retries the recalculation' do + subject + + expect(Gitlab::BackgroundMigration::RecalculateVulnerabilitiesOccurrencesUuid::VulnerabilitiesFinding) + .to have_received(:find_by).with(uuid: uuid).once + end + + it 'logs the conflict' do + expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance| + expect(instance).to receive(:info).exactly(6).times + end + + subject + end + + it 'marks the job as done' do + create_background_migration_job([start_id, end_id], :pending) + + subject + + expect(pending_jobs.count).to eq(0) + expect(succeeded_jobs.count).to eq(1) + end + end + + it 'logs an exception if a different uniquness problem was found' do + exception = ActiveRecord::RecordNotUnique.new("Totally not an UUID uniqueness problem") + allow(::Gitlab::Database::BulkUpdate).to receive(:execute).and_raise(exception) + allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception) + + subject + + expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_exception).with(exception).once + end + + it 'logs a duplicate found message' do + expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |instance| + expect(instance).to receive(:info).exactly(3).times + end + + subject + end + end + + context 'when finding has a signature' do + before do + @f1 = create_finding!( + vulnerability_id: nil, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: vulnerability_identifier.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: 'd15d774d-e4b1-5a1b-929b-19f2a53e35ec' + ) + + vulnerability_finding_signatures.create!( + finding_id: @f1.id, + algorithm_type: 2, # location + signature_sha: Gitlab::Database::ShaAttribute.serialize('57d4e05205f6462a73f039a5b2751aa1ab344e6e') # sha1('youshouldusethis') + ) + + vulnerability_finding_signatures.create!( + finding_id: @f1.id, + algorithm_type: 1, # hash + signature_sha: Gitlab::Database::ShaAttribute.serialize('c554d8d8df1a7a14319eafdaae24af421bf5b587') # sha1('andnotthis') + ) + + @f2 = create_finding!( + vulnerability_id: nil, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: vulnerability_identfier2.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('ca41a2544e941a007a73a666cb0592b255316ab8'), # sha1('youshouldntusethis') + uuid: '4be029b5-75e5-5ac0-81a2-50ab41726135' + ) + + vulnerability_finding_signatures.create!( + finding_id: @f2.id, + algorithm_type: 2, # location + signature_sha: Gitlab::Database::ShaAttribute.serialize('57d4e05205f6462a73f039a5b2751aa1ab344e6e') # sha1('youshouldusethis') + ) + + vulnerability_finding_signatures.create!( + finding_id: @f2.id, + algorithm_type: 1, # hash + signature_sha: Gitlab::Database::ShaAttribute.serialize('c554d8d8df1a7a14319eafdaae24af421bf5b587') # sha1('andnotthis') + ) + end + + let(:start_id) { @f1.id } + let(:end_id) { @f2.id } + + let(:uuids_before) { [@f1.uuid, @f2.uuid] } + let(:uuids_after) { %w[d3b60ddd-d312-5606-b4d3-ad058eebeacb 349d9bec-c677-5530-a8ac-5e58889c3b1a] } + + it 'is recalculated using signature' do + expect(vulnerability_findings.pluck(:uuid)).to match_array(uuids_before) + + subject + + expect(vulnerability_findings.pluck(:uuid)).to match_array(uuids_after) + end + end + + context 'if all records are removed before the job ran' do + let(:start_id) { 1 } + let(:end_id) { 9 } + + before do + create_background_migration_job([start_id, end_id], :pending) + end + + it 'does not error out' do + expect { subject }.not_to raise_error + end + + it 'marks the job as done' do + subject + + expect(pending_jobs.count).to eq(0) + expect(succeeded_jobs.count).to eq(1) + end + end + + context 'when recalculation fails' do + before do + @uuid_v4 = create_finding!( + vulnerability_id: nil, + project_id: project.id, + scanner_id: scanner2.id, + primary_identifier_id: vulnerability_identfier2.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize("fa18f432f1d56675f4098d318739c3cd5b14eb3e"), + uuid: known_uuid_v4 + ) + + allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception) + allow(::Gitlab::Database::BulkUpdate).to receive(:execute).and_raise(expected_error) + end + + let(:start_id) { @uuid_v4.id } + let(:end_id) { @uuid_v4.id } + let(:expected_error) { RuntimeError.new } + + it 'captures the errors and does not crash entirely' do + expect { subject }.not_to raise_error + + allow(Gitlab::ErrorTracking).to receive(:track_and_raise_exception) + expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_exception).with(expected_error).once + end + + it_behaves_like 'marks background migration job records' do + let(:arguments) { [1, 4] } + subject { described_class.new } + end + end + + it_behaves_like 'marks background migration job records' do + let(:arguments) { [1, 4] } + subject { described_class.new } + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerability_findings.create!({ + id: id, + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + }.compact + ) + end + # rubocop:enable Metrics/ParameterLists + + def create_user!(name: "Example User", email: "user@example.com", user_type: nil, created_at: Time.zone.now, confirmed_at: Time.zone.now) + users.create!( + name: name, + email: email, + username: name, + projects_limit: 0, + user_type: user_type, + confirmed_at: confirmed_at + ) + end + + def create_finding_pipeline!(project_id:, finding_id:) + pipeline = table(:ci_pipelines).create!(project_id: project_id) + vulnerability_finding_pipelines.create!(pipeline_id: pipeline.id, occurrence_id: finding_id) + end +end diff --git a/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb b/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb new file mode 100644 index 00000000000..eabc012f98b --- /dev/null +++ b/spec/lib/gitlab/background_migration/remove_all_trace_expiration_dates_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::RemoveAllTraceExpirationDates, :migration, + :suppress_gitlab_schemas_validate_connection, schema: 20220131000001 do + subject(:perform) { migration.perform(1, 99) } + + let(:migration) { described_class.new } + + let(:trace_in_range) { create_trace!(id: 10, created_at: Date.new(2020, 06, 20), expire_at: Date.new(2021, 01, 22)) } + let(:trace_outside_range) { create_trace!(id: 40, created_at: Date.new(2020, 06, 22), expire_at: Date.new(2021, 01, 22)) } + let(:trace_without_expiry) { create_trace!(id: 30, created_at: Date.new(2020, 06, 21), expire_at: nil) } + let(:archive_in_range) { create_archive!(id: 10, created_at: Date.new(2020, 06, 20), expire_at: Date.new(2021, 01, 22)) } + let(:trace_outside_id_range) { create_trace!(id: 100, created_at: Date.new(2020, 06, 20), expire_at: Date.new(2021, 01, 22)) } + + before do + table(:namespaces).create!(id: 1, name: 'the-namespace', path: 'the-path') + table(:projects).create!(id: 1, name: 'the-project', namespace_id: 1) + table(:ci_builds).create!(id: 1, allow_failure: false) + end + + context 'for self-hosted instances' do + it 'sets expire_at for artifacts in range to nil' do + expect { perform }.not_to change { trace_in_range.reload.expire_at } + end + + it 'does not change expire_at timestamps that are not set to midnight' do + expect { perform }.not_to change { trace_outside_range.reload.expire_at } + end + + it 'does not change expire_at timestamps that are set to midnight on a day other than the 22nd' do + expect { perform }.not_to change { trace_without_expiry.reload.expire_at } + end + + it 'does not touch artifacts outside id range' do + expect { perform }.not_to change { archive_in_range.reload.expire_at } + end + + it 'does not touch artifacts outside date range' do + expect { perform }.not_to change { trace_outside_id_range.reload.expire_at } + end + end + + private + + def create_trace!(**args) + table(:ci_job_artifacts).create!(**args, project_id: 1, job_id: 1, file_type: 3) + end + + def create_archive!(**args) + table(:ci_job_artifacts).create!(**args, project_id: 1, job_id: 1, file_type: 1) + end +end diff --git a/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb b/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb index da14381aae2..32134b99e37 100644 --- a/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb +++ b/spec/lib/gitlab/background_migration/remove_vulnerability_finding_links_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::RemoveVulnerabilityFindingLinks, :migration, schema: 20220314184009 do +RSpec.describe Gitlab::BackgroundMigration::RemoveVulnerabilityFindingLinks, :migration, schema: 20211202041233 do let(:vulnerability_findings) { table(:vulnerability_occurrences) } let(:finding_links) { table(:vulnerability_finding_links) } diff --git a/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb b/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb new file mode 100644 index 00000000000..908f11aabc3 --- /dev/null +++ b/spec/lib/gitlab/background_migration/update_timelogs_null_spent_at_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::UpdateTimelogsNullSpentAt, schema: 20211215090620 do + let!(:previous_time) { 10.days.ago } + let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') } + let!(:project) { table(:projects).create!(namespace_id: namespace.id) } + let!(:issue) { table(:issues).create!(project_id: project.id) } + let!(:merge_request) { table(:merge_requests).create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature') } + let!(:timelog1) { create_timelog!(issue_id: issue.id) } + let!(:timelog2) { create_timelog!(merge_request_id: merge_request.id) } + let!(:timelog3) { create_timelog!(issue_id: issue.id, spent_at: previous_time) } + let!(:timelog4) { create_timelog!(merge_request_id: merge_request.id, spent_at: previous_time) } + + subject(:background_migration) { described_class.new } + + before do + table(:timelogs).where.not(id: [timelog3.id, timelog4.id]).update_all(spent_at: nil) + end + + describe '#perform' do + it 'sets correct spent_at' do + background_migration.perform(timelog1.id, timelog4.id) + + expect(timelog1.reload.spent_at).to be_like_time(timelog1.created_at) + expect(timelog2.reload.spent_at).to be_like_time(timelog2.created_at) + expect(timelog3.reload.spent_at).to be_like_time(previous_time) + expect(timelog4.reload.spent_at).to be_like_time(previous_time) + expect(timelog3.reload.spent_at).not_to be_like_time(timelog3.created_at) + expect(timelog4.reload.spent_at).not_to be_like_time(timelog4.created_at) + end + end + + private + + def create_timelog!(**args) + table(:timelogs).create!(**args, time_spent: 1) + end +end diff --git a/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb b/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb new file mode 100644 index 00000000000..7be54bc13cc --- /dev/null +++ b/spec/migrations/20211203091642_add_index_to_projects_on_marked_for_deletion_at_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddIndexToProjectsOnMarkedForDeletionAt, feature_category: :projects do + it 'correctly migrates up and down' do + reversible_migration do |migration| + migration.before -> { + expect(ActiveRecord::Base.connection.indexes('projects').map(&:name)).not_to include('index_projects_not_aimed_for_deletion') + } + + migration.after -> { + expect(ActiveRecord::Base.connection.indexes('projects').map(&:name)).to include('index_projects_not_aimed_for_deletion') + } + end + end +end diff --git a/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb b/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb new file mode 100644 index 00000000000..9fa2ac2313a --- /dev/null +++ b/spec/migrations/20211207125331_remove_jobs_for_recalculate_vulnerabilities_occurrences_uuid_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require 'spec_helper' +require_migration! + +def create_background_migration_jobs(ids, status, created_at) + proper_status = case status + when :pending + Gitlab::Database::BackgroundMigrationJob.statuses['pending'] + when :succeeded + Gitlab::Database::BackgroundMigrationJob.statuses['succeeded'] + else + raise ArgumentError + end + + background_migration_jobs.create!( + class_name: 'RecalculateVulnerabilitiesOccurrencesUuid', + arguments: Array(ids), + status: proper_status, + created_at: created_at + ) +end + +RSpec.describe RemoveJobsForRecalculateVulnerabilitiesOccurrencesUuid, :migration, + feature_category: :vulnerability_management do + let!(:background_migration_jobs) { table(:background_migration_jobs) } + + context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do + before do + create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2)) + create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4)) + + create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0)) + create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2)) + create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4)) + end + + it 'removes all jobs' do + expect(background_migration_jobs.count).to eq(5) + + migrate! + + expect(background_migration_jobs.count).to eq(0) + end + end +end diff --git a/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb b/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb new file mode 100644 index 00000000000..c7401c4790d --- /dev/null +++ b/spec/migrations/20211207135331_schedule_recalculate_uuid_on_vulnerabilities_occurrences4_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ScheduleRecalculateUuidOnVulnerabilitiesOccurrences4, feature_category: :vulnerability_management do + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:users) { table(:users) } + let(:user) { create_user! } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:scanners) { table(:vulnerability_scanners) } + let(:scanner) { scanners.create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + let(:different_scanner) { scanners.create!(project_id: project.id, external_id: 'test 2', name: 'test scanner 2') } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + let(:vulnerability_finding_signatures) { table(:vulnerability_finding_signatures) } + let(:vulnerability_identifiers) { table(:vulnerability_identifiers) } + let(:vulnerability_identifier) do + vulnerability_identifiers.create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + let(:different_vulnerability_identifier) do + vulnerability_identifiers.create!( + project_id: project.id, + external_type: 'uuid-v4', + external_id: 'uuid-v4', + fingerprint: '772da93d34a1ba010bcb5efa9fb6f8e01bafcc89', + name: 'Identifier for UUIDv4') + end + + let!(:uuidv4_finding) do + create_finding!( + vulnerability_id: vulnerability_for_uuidv4.id, + project_id: project.id, + scanner_id: different_scanner.id, + primary_identifier_id: different_vulnerability_identifier.id, + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('fa18f432f1d56675f4098d318739c3cd5b14eb3e'), + uuid: 'b3cc2518-5446-4dea-871c-89d5e999c1ac' + ) + end + + let(:vulnerability_for_uuidv4) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let!(:uuidv5_finding) do + create_finding!( + vulnerability_id: vulnerability_for_uuidv5.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: vulnerability_identifier.id, + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('838574be0210968bf6b9f569df9c2576242cbf0a'), + uuid: '77211ed6-7dff-5f6b-8c9a-da89ad0a9b60' + ) + end + + let(:vulnerability_for_uuidv5) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let(:vulnerability_for_finding_with_signature) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let!(:finding_with_signature) do + create_finding!( + vulnerability_id: vulnerability_for_finding_with_signature.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: vulnerability_identifier.id, + report_type: 0, # "sast" + location_fingerprint: Gitlab::Database::ShaAttribute.serialize('123609eafffffa2207a9ca2425ba4337h34fga1b'), + uuid: '252aa474-d689-5d2b-ab42-7bbb5a100c02' + ) + end + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + end + + around do |example| + freeze_time { Sidekiq::Testing.fake! { example.run } } + end + + it 'schedules background migrations', :aggregate_failures do + migrate! + + expect(BackgroundMigrationWorker.jobs.size).to eq(3) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, uuidv4_finding.id, uuidv4_finding.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, uuidv5_finding.id, uuidv5_finding.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(6.minutes, finding_with_signature.id, finding_with_signature.id) + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + def create_finding!( + vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, location_fingerprint:, uuid:, report_type: 0) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: 'test', + severity: 7, + confidence: 7, + report_type: report_type, + project_fingerprint: '123qweasdzxc', + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location_fingerprint: location_fingerprint, + metadata_version: 'test', + raw_metadata: 'test', + uuid: uuid + ) + end + + def create_user!(name: "Example User", email: "user@example.com", user_type: nil) + users.create!( + name: name, + email: email, + username: name, + projects_limit: 0 + ) + end +end diff --git a/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb new file mode 100644 index 00000000000..f103ee54990 --- /dev/null +++ b/spec/migrations/20211210140629_encrypt_static_object_token_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true +require 'spec_helper' + +require_migration! + +RSpec.describe EncryptStaticObjectToken, :migration, feature_category: :source_code_management do + let!(:background_migration_jobs) { table(:background_migration_jobs) } + let!(:users) { table(:users) } + + let!(:user_without_tokens) { create_user!(name: 'notoken') } + let!(:user_with_plaintext_token_1) { create_user!(name: 'plaintext_1', token: 'token') } + let!(:user_with_plaintext_token_2) { create_user!(name: 'plaintext_2', token: 'TOKEN') } + let!(:user_with_encrypted_token) { create_user!(name: 'encrypted', encrypted_token: 'encrypted') } + let!(:user_with_both_tokens) { create_user!(name: 'both', token: 'token2', encrypted_token: 'encrypted2') } + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + end + + around do |example| + freeze_time { Sidekiq::Testing.fake! { example.run } } + end + + it 'schedules background migrations' do + migrate! + + expect(background_migration_jobs.count).to eq(2) + expect(background_migration_jobs.first.arguments).to match_array([user_with_plaintext_token_1.id, user_with_plaintext_token_1.id]) + expect(background_migration_jobs.second.arguments).to match_array([user_with_plaintext_token_2.id, user_with_plaintext_token_2.id]) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, user_with_plaintext_token_1.id, user_with_plaintext_token_1.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, user_with_plaintext_token_2.id, user_with_plaintext_token_2.id) + end + + private + + def create_user!(name:, token: nil, encrypted_token: nil) + email = "#{name}@example.com" + + table(:users).create!( + name: name, + email: email, + username: name, + projects_limit: 0, + static_object_token: token, + static_object_token_encrypted: encrypted_token + ) + end +end diff --git a/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb new file mode 100644 index 00000000000..0df52df43d8 --- /dev/null +++ b/spec/migrations/20211214012507_backfill_incident_issue_escalation_statuses_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillIncidentIssueEscalationStatuses, feature_category: :incident_management do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') } + let(:project) { projects.create!(namespace_id: namespace.id) } + + # Backfill removed - see db/migrate/20220321234317_remove_all_issuable_escalation_statuses.rb. + it 'does nothing' do + issues.create!(project_id: project.id, issue_type: 1) + + expect { migrate! }.not_to change { BackgroundMigrationWorker.jobs.size } + end +end diff --git a/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb new file mode 100644 index 00000000000..2d808adf578 --- /dev/null +++ b/spec/migrations/20211217174331_mark_recalculate_finding_signatures_as_completed_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true +require 'spec_helper' +require_migration! + +def create_background_migration_jobs(ids, status, created_at) + proper_status = case status + when :pending + Gitlab::Database::BackgroundMigrationJob.statuses['pending'] + when :succeeded + Gitlab::Database::BackgroundMigrationJob.statuses['succeeded'] + else + raise ArgumentError + end + + background_migration_jobs.create!( + class_name: 'RecalculateVulnerabilitiesOccurrencesUuid', + arguments: Array(ids), + status: proper_status, + created_at: created_at + ) +end + +RSpec.describe MarkRecalculateFindingSignaturesAsCompleted, :migration, feature_category: :vulnerability_management do + let!(:background_migration_jobs) { table(:background_migration_jobs) } + + context 'when RecalculateVulnerabilitiesOccurrencesUuid jobs are present' do + before do + create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 5, 5, 0, 2)) + create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 5, 5, 0, 4)) + + create_background_migration_jobs([1, 2, 3], :succeeded, DateTime.new(2021, 8, 18, 0, 0)) + create_background_migration_jobs([4, 5, 6], :pending, DateTime.new(2021, 8, 18, 0, 2)) + create_background_migration_jobs([7, 8, 9], :pending, DateTime.new(2021, 8, 18, 0, 4)) + end + + describe 'gitlab.com' do + before do + allow(::Gitlab).to receive(:com?).and_return(true) + end + + it 'marks all jobs as succeeded' do + expect(background_migration_jobs.where(status: 1).count).to eq(2) + + migrate! + + expect(background_migration_jobs.where(status: 1).count).to eq(5) + end + end + + describe 'self managed' do + before do + allow(::Gitlab).to receive(:com?).and_return(false) + end + + it 'does not change job status' do + expect(background_migration_jobs.where(status: 1).count).to eq(2) + + migrate! + + expect(background_migration_jobs.where(status: 1).count).to eq(2) + end + end + end +end diff --git a/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb new file mode 100644 index 00000000000..263289462ba --- /dev/null +++ b/spec/migrations/20220106111958_add_insert_or_update_vulnerability_reads_trigger_spec.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddInsertOrUpdateVulnerabilityReadsTrigger, feature_category: :vulnerability_management do + let(:migration) { described_class.new } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + + let(:vulnerability) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let(:vulnerability2) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let(:identifier) do + table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + let(:finding) do + create_finding!( + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + end + + describe '#up' do + before do + migrate! + end + + describe 'UPDATE trigger' do + context 'when vulnerability_id is updated' do + it 'creates a new vulnerability_reads row' do + expect do + finding.update!(vulnerability_id: vulnerability.id) + end.to change { vulnerability_reads.count }.from(0).to(1) + end + end + + context 'when vulnerability_id is not updated' do + it 'does not create a new vulnerability_reads row' do + finding.update!(vulnerability_id: nil) + + expect do + finding.update!(location: '') + end.not_to change { vulnerability_reads.count } + end + end + end + + describe 'INSERT trigger' do + context 'when vulnerability_id is set' do + it 'creates a new vulnerability_reads row' do + expect do + create_finding!( + vulnerability_id: vulnerability2.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + end.to change { vulnerability_reads.count }.from(0).to(1) + end + end + + context 'when vulnerability_id is not set' do + it 'does not create a new vulnerability_reads row' do + expect do + create_finding!( + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + end.not_to change { vulnerability_reads.count } + end + end + end + end + + describe '#down' do + before do + migration.up + migration.down + end + + it 'drops the trigger' do + expect do + finding.update!(vulnerability_id: vulnerability.id) + end.not_to change { vulnerability_reads.count } + end + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb new file mode 100644 index 00000000000..152a551bc7b --- /dev/null +++ b/spec/migrations/20220106112043_add_update_vulnerability_reads_trigger_spec.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddUpdateVulnerabilityReadsTrigger, feature_category: :vulnerability_management do + let(:migration) { described_class.new } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:issue_links) { table(:vulnerability_issue_links) } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + + let(:vulnerability) do + create_vulnerability!( + project_id: project.id, + report_type: 7, + author_id: user.id + ) + end + + let(:identifier) do + table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + describe '#up' do + before do + migrate! + end + + describe 'UPDATE trigger' do + before do + create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + report_type: 7, + primary_identifier_id: identifier.id + ) + end + + context 'when vulnerability attributes are updated' do + it 'updates vulnerability attributes in vulnerability_reads' do + expect do + vulnerability.update!(severity: 6) + end.to change { vulnerability_reads.first.severity }.from(7).to(6) + end + end + + context 'when vulnerability attributes are not updated' do + it 'does not update vulnerability attributes in vulnerability_reads' do + expect do + vulnerability.update!(title: "New vulnerability") + end.not_to change { vulnerability_reads.first } + end + end + end + end + + describe '#down' do + before do + migration.up + migration.down + create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + report_type: 7, + primary_identifier_id: identifier.id + ) + end + + it 'drops the trigger' do + expect do + vulnerability.update!(severity: 6) + end.not_to change { vulnerability_reads.first.severity } + end + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb new file mode 100644 index 00000000000..9fc40b0b5f1 --- /dev/null +++ b/spec/migrations/20220106112085_add_update_vulnerability_reads_location_trigger_spec.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddUpdateVulnerabilityReadsLocationTrigger, feature_category: :vulnerability_management do + let(:migration) { described_class.new } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:issue_links) { table(:vulnerability_issue_links) } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + + let(:vulnerability) do + create_vulnerability!( + project_id: project.id, + report_type: 7, + author_id: user.id + ) + end + + let(:identifier) do + table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + describe '#up' do + before do + migrate! + end + + describe 'UPDATE trigger' do + context 'when image is updated' do + it 'updates location_image in vulnerability_reads' do + finding = create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + report_type: 7, + location: { "image" => "alpine:3.4" }, + primary_identifier_id: identifier.id + ) + + expect do + finding.update!(location: { "image" => "alpine:4", "kubernetes_resource" => { "agent_id" => "1234" } }) + end.to change { vulnerability_reads.first.location_image }.from("alpine:3.4").to("alpine:4") + end + end + + context 'when image is not updated' do + it 'updates location_image in vulnerability_reads' do + finding = create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + report_type: 7, + location: { "image" => "alpine:3.4", "kubernetes_resource" => { "agent_id" => "1234" } }, + primary_identifier_id: identifier.id + ) + + expect do + finding.update!(project_fingerprint: "123qweasdzx") + end.not_to change { vulnerability_reads.first.location_image } + end + end + end + end + + describe '#down' do + before do + migration.up + migration.down + end + + it 'drops the trigger' do + finding = create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + + expect do + finding.update!(location: '{"image":"alpine:4"}') + end.not_to change { vulnerability_reads.first.location_image } + end + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb new file mode 100644 index 00000000000..e58fdfb1591 --- /dev/null +++ b/spec/migrations/20220106163326_add_has_issues_on_vulnerability_reads_trigger_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddHasIssuesOnVulnerabilityReadsTrigger, feature_category: :vulnerability_management do + let(:migration) { described_class.new } + let(:vulnerability_reads) { table(:vulnerability_reads) } + let(:issue_links) { table(:vulnerability_issue_links) } + let(:vulnerabilities) { table(:vulnerabilities) } + let(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:user) { table(:users).create!(id: 13, email: 'author@example.com', username: 'author', projects_limit: 10) } + let(:project) { table(:projects).create!(id: 123, namespace_id: namespace.id) } + let(:issue) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) } + let(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + + let(:vulnerability) do + create_vulnerability!( + project_id: project.id, + author_id: user.id + ) + end + + let(:identifier) do + table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: '7e394d1b1eb461a7406d7b1e08f057a1cf11287a', + name: 'Identifier for UUIDv5') + end + + before do + create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + + @vulnerability_read = vulnerability_reads.first + end + + describe '#up' do + before do + migrate! + end + + describe 'INSERT trigger' do + it 'updates has_issues in vulnerability_reads' do + expect do + issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id) + end.to change { @vulnerability_read.reload.has_issues }.from(false).to(true) + end + end + + describe 'DELETE trigger' do + let(:issue2) { table(:issues).create!(description: '1234', state_id: 1, project_id: project.id) } + + it 'does not change has_issues when there exists another issue' do + issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id) + issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id) + + expect do + issue_link1.delete + end.not_to change { @vulnerability_read.reload.has_issues } + end + + it 'unsets has_issues when all issues are deleted' do + issue_link1 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id) + issue_link2 = issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue2.id) + + expect do + issue_link1.delete + issue_link2.delete + end.to change { @vulnerability_read.reload.has_issues }.from(true).to(false) + end + end + end + + describe '#down' do + before do + migration.up + migration.down + end + + it 'drops the trigger' do + expect do + issue_links.create!(vulnerability_id: vulnerability.id, issue_id: issue.id) + end.not_to change { @vulnerability_read.has_issues } + end + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" }, location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + vulnerabilities_findings.create!( + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location: location, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + ) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb new file mode 100644 index 00000000000..1338f826537 --- /dev/null +++ b/spec/migrations/20220107064845_populate_vulnerability_reads_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true +require 'spec_helper' + +require_migration! + +RSpec.describe PopulateVulnerabilityReads, :migration, feature_category: :vulnerability_management do + let!(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let!(:user) { table(:users).create!(email: 'author@example.com', username: 'author', projects_limit: 10) } + let!(:project) { table(:projects).create!(namespace_id: namespace.id) } + let!(:scanner) { table(:vulnerability_scanners).create!(project_id: project.id, external_id: 'test 1', name: 'test scanner 1') } + let!(:background_migration_jobs) { table(:background_migration_jobs) } + let!(:vulnerabilities) { table(:vulnerabilities) } + let!(:vulnerability_reads) { table(:vulnerability_reads) } + let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) } + let!(:vulnerability_issue_links) { table(:vulnerability_issue_links) } + let!(:vulnerability_ids) { [] } + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + stub_const("#{described_class}::SUB_BATCH_SIZE", 1) + + 5.times.each do |x| + vulnerability = create_vulnerability!( + project_id: project.id, + report_type: 7, + author_id: user.id + ) + identifier = table(:vulnerability_identifiers).create!( + project_id: project.id, + external_type: 'uuid-v5', + external_id: 'uuid-v5', + fingerprint: Digest::SHA1.hexdigest(vulnerability.id.to_s), + name: 'Identifier for UUIDv5') + + create_finding!( + vulnerability_id: vulnerability.id, + project_id: project.id, + scanner_id: scanner.id, + primary_identifier_id: identifier.id + ) + + vulnerability_ids << vulnerability.id + end + end + + around do |example| + freeze_time { Sidekiq::Testing.fake! { example.run } } + end + + it 'schedules background migrations' do + migrate! + + expect(background_migration_jobs.count).to eq(5) + expect(background_migration_jobs.first.arguments).to match_array([vulnerability_ids.first, vulnerability_ids.first, 1]) + expect(background_migration_jobs.second.arguments).to match_array([vulnerability_ids.second, vulnerability_ids.second, 1]) + expect(background_migration_jobs.third.arguments).to match_array([vulnerability_ids.third, vulnerability_ids.third, 1]) + expect(background_migration_jobs.fourth.arguments).to match_array([vulnerability_ids.fourth, vulnerability_ids.fourth, 1]) + expect(background_migration_jobs.fifth.arguments).to match_array([vulnerability_ids.fifth, vulnerability_ids.fifth, 1]) + + expect(BackgroundMigrationWorker.jobs.size).to eq(5) + expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(2.minutes, vulnerability_ids.first, vulnerability_ids.first, 1) + expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(4.minutes, vulnerability_ids.second, vulnerability_ids.second, 1) + expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(6.minutes, vulnerability_ids.third, vulnerability_ids.third, 1) + expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(8.minutes, vulnerability_ids.fourth, vulnerability_ids.fourth, 1) + expect(described_class::MIGRATION_NAME).to be_scheduled_delayed_migration(10.minutes, vulnerability_ids.fifth, vulnerability_ids.fifth, 1) + end + + private + + def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0) + vulnerabilities.create!( + project_id: project_id, + author_id: author_id, + title: title, + severity: severity, + confidence: confidence, + report_type: report_type + ) + end + + # rubocop:disable Metrics/ParameterLists + def create_finding!( + vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:, id: nil, + name: "test", severity: 7, confidence: 7, report_type: 0, + project_fingerprint: '123qweasdzxc', location_fingerprint: 'test', + metadata_version: 'test', raw_metadata: 'test', uuid: SecureRandom.uuid) + params = { + vulnerability_id: vulnerability_id, + project_id: project_id, + name: name, + severity: severity, + confidence: confidence, + report_type: report_type, + project_fingerprint: project_fingerprint, + scanner_id: scanner_id, + primary_identifier_id: primary_identifier_id, + location_fingerprint: location_fingerprint, + metadata_version: metadata_version, + raw_metadata: raw_metadata, + uuid: uuid + } + params[:id] = id unless id.nil? + vulnerabilities_findings.create!(params) + end + # rubocop:enable Metrics/ParameterLists +end diff --git a/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb b/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb new file mode 100644 index 00000000000..1470f2b3cad --- /dev/null +++ b/spec/migrations/20220120094340_drop_position_from_security_findings_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration!('drop_position_from_security_findings') + +RSpec.describe DropPositionFromSecurityFindings, feature_category: :vulnerability_management do + let(:events) { table(:security_findings) } + + it 'correctly migrates up and down' do + reversible_migration do |migration| + migration.before -> { + expect(events.column_names).to include('position') + } + + migration.after -> { + events.reset_column_information + expect(events.column_names).not_to include('position') + } + end + end +end diff --git a/spec/migrations/20220124130028_dedup_runner_projects_spec.rb b/spec/migrations/20220124130028_dedup_runner_projects_spec.rb new file mode 100644 index 00000000000..b9189cbae7f --- /dev/null +++ b/spec/migrations/20220124130028_dedup_runner_projects_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe DedupRunnerProjects, :migration, :suppress_gitlab_schemas_validate_connection, + schema: 20220120085655, feature_category: :runner do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:runners) { table(:ci_runners) } + let(:runner_projects) { table(:ci_runner_projects) } + + let!(:namespace) { namespaces.create!(name: 'foo', path: 'foo') } + let!(:project) { projects.create!(namespace_id: namespace.id) } + let!(:project_2) { projects.create!(namespace_id: namespace.id) } + let!(:runner) { runners.create!(runner_type: 'project_type') } + let!(:runner_2) { runners.create!(runner_type: 'project_type') } + let!(:runner_3) { runners.create!(runner_type: 'project_type') } + + let!(:duplicated_runner_project_1) { runner_projects.create!(runner_id: runner.id, project_id: project.id) } + let!(:duplicated_runner_project_2) { runner_projects.create!(runner_id: runner.id, project_id: project.id) } + let!(:duplicated_runner_project_3) { runner_projects.create!(runner_id: runner_2.id, project_id: project_2.id) } + let!(:duplicated_runner_project_4) { runner_projects.create!(runner_id: runner_2.id, project_id: project_2.id) } + + let!(:non_duplicated_runner_project) { runner_projects.create!(runner_id: runner_3.id, project_id: project.id) } + + it 'deduplicates ci_runner_projects table' do + expect { migrate! }.to change { runner_projects.count }.from(5).to(3) + end + + it 'merges `duplicated_runner_project_1` with `duplicated_runner_project_2`', :aggregate_failures do + migrate! + + expect(runner_projects.where(id: duplicated_runner_project_1.id)).not_to(exist) + + merged_runner_projects = runner_projects.find_by(id: duplicated_runner_project_2.id) + + expect(merged_runner_projects).to be_present + expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_1.created_at) + expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_2.created_at) + end + + it 'merges `duplicated_runner_project_3` with `duplicated_runner_project_4`', :aggregate_failures do + migrate! + + expect(runner_projects.where(id: duplicated_runner_project_3.id)).not_to(exist) + + merged_runner_projects = runner_projects.find_by(id: duplicated_runner_project_4.id) + + expect(merged_runner_projects).to be_present + expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_3.created_at) + expect(merged_runner_projects.created_at).to be_like_time(duplicated_runner_project_4.created_at) + end + + it 'does not change non duplicated records' do + expect { migrate! }.not_to change { non_duplicated_runner_project.reload.attributes } + end + + it 'does nothing when there are no runner projects' do + runner_projects.delete_all + + migrate! + + expect(runner_projects.count).to eq(0) + end +end diff --git a/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb new file mode 100644 index 00000000000..3abe173196f --- /dev/null +++ b/spec/migrations/20220128155251_remove_dangling_running_builds_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration!('remove_dangling_running_builds') + +RSpec.describe RemoveDanglingRunningBuilds, :suppress_gitlab_schemas_validate_connection, + feature_category: :continuous_integration do + let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') } + let(:project) { table(:projects).create!(namespace_id: namespace.id) } + let(:runner) { table(:ci_runners).create!(runner_type: 1) } + let(:builds) { table(:ci_builds) } + let(:running_builds) { table(:ci_running_builds) } + + let(:running_build) do + builds.create!( + name: 'test 1', + status: 'running', + project_id: project.id, + type: 'Ci::Build') + end + + let(:failed_build) do + builds.create!( + name: 'test 2', + status: 'failed', + project_id: project.id, + type: 'Ci::Build') + end + + let!(:running_metadata) do + running_builds.create!( + build_id: running_build.id, + project_id: project.id, + runner_id: runner.id, + runner_type: + runner.runner_type) + end + + let!(:failed_metadata) do + running_builds.create!( + build_id: failed_build.id, + project_id: project.id, + runner_id: runner.id, + runner_type: runner.runner_type) + end + + it 'removes failed builds' do + migrate! + + expect(running_metadata.reload).to be_present + expect { failed_metadata.reload }.to raise_error(ActiveRecord::RecordNotFound) + end +end diff --git a/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb new file mode 100644 index 00000000000..3f3fdd0889d --- /dev/null +++ b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration!('fix_approval_rules_code_owners_rule_type_index') + +RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration, feature_category: :source_code_management do + let(:table_name) { :approval_merge_request_rules } + let(:index_name) { 'index_approval_rules_code_owners_rule_type' } + + it 'correctly migrates up and down' do + reversible_migration do |migration| + migration.before -> { + expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy + } + + migration.after -> { + expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy + } + end + end + + context 'when the index already exists' do + before do + subject.add_concurrent_index table_name, :merge_request_id, where: 'rule_type = 2', name: index_name + end + + it 'keeps the index' do + migrate! + + expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy + end + end +end diff --git a/spec/migrations/20220202105733_delete_service_template_records_spec.rb b/spec/migrations/20220202105733_delete_service_template_records_spec.rb new file mode 100644 index 00000000000..41762a3a5c3 --- /dev/null +++ b/spec/migrations/20220202105733_delete_service_template_records_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true +require 'spec_helper' + +require_migration! + +RSpec.describe DeleteServiceTemplateRecords, feature_category: :integrations do + let(:integrations) { table(:integrations) } + let(:chat_names) { table(:chat_names) } + let(:web_hooks) { table(:web_hooks) } + let(:slack_integrations) { table(:slack_integrations) } + let(:zentao_tracker_data) { table(:zentao_tracker_data) } + let(:jira_tracker_data) { table(:jira_tracker_data) } + let(:issue_tracker_data) { table(:issue_tracker_data) } + + before do + template = integrations.create!(template: true) + chat_names.create!(service_id: template.id, user_id: 1, team_id: 1, chat_id: 1) + web_hooks.create!(service_id: template.id) + slack_integrations.create!(service_id: template.id, team_id: 1, team_name: 'team', alias: 'alias', user_id: 1) + zentao_tracker_data.create!(integration_id: template.id) + jira_tracker_data.create!(service_id: template.id) + issue_tracker_data.create!(service_id: template.id) + + integrations.create!(template: false) + end + + it 'deletes template records and associated data' do + expect { migrate! } + .to change { integrations.where(template: true).count }.from(1).to(0) + .and change { chat_names.count }.from(1).to(0) + .and change { web_hooks.count }.from(1).to(0) + .and change { slack_integrations.count }.from(1).to(0) + .and change { zentao_tracker_data.count }.from(1).to(0) + .and change { jira_tracker_data.count }.from(1).to(0) + .and change { issue_tracker_data.count }.from(1).to(0) + end + + it 'does not delete non template records' do + expect { migrate! } + .not_to change { integrations.where(template: false).count } + end +end diff --git a/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb b/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb new file mode 100644 index 00000000000..cbae5674d78 --- /dev/null +++ b/spec/migrations/20220204095121_backfill_namespace_statistics_with_dependency_proxy_size_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillNamespaceStatisticsWithDependencyProxySize, feature_category: :dependency_proxy do + let!(:groups) { table(:namespaces) } + let!(:group1) { groups.create!(id: 10, name: 'test1', path: 'test1', type: 'Group') } + let!(:group2) { groups.create!(id: 20, name: 'test2', path: 'test2', type: 'Group') } + let!(:group3) { groups.create!(id: 30, name: 'test3', path: 'test3', type: 'Group') } + let!(:group4) { groups.create!(id: 40, name: 'test4', path: 'test4', type: 'Group') } + + let!(:dependency_proxy_blobs) { table(:dependency_proxy_blobs) } + let!(:dependency_proxy_manifests) { table(:dependency_proxy_manifests) } + + let!(:group1_manifest) { create_manifest(10, 10) } + let!(:group2_manifest) { create_manifest(20, 20) } + let!(:group3_manifest) { create_manifest(30, 30) } + + let!(:group1_blob) { create_blob(10, 10) } + let!(:group2_blob) { create_blob(20, 20) } + let!(:group3_blob) { create_blob(30, 30) } + + describe '#up' do + it 'correctly schedules background migrations' do + stub_const("#{described_class}::BATCH_SIZE", 2) + + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + aggregate_failures do + expect(described_class::MIGRATION) + .to be_scheduled_migration([10, 30], ['dependency_proxy_size']) + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(2.minutes, [20], ['dependency_proxy_size']) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + end + end + + def create_manifest(group_id, size) + dependency_proxy_manifests.create!( + group_id: group_id, + size: size, + file_name: 'test-file', + file: 'test', + digest: 'abc123' + ) + end + + def create_blob(group_id, size) + dependency_proxy_blobs.create!( + group_id: group_id, + size: size, + file_name: 'test-file', + file: 'test' + ) + end +end diff --git a/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb b/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb new file mode 100644 index 00000000000..5e728bb396c --- /dev/null +++ b/spec/migrations/20220204194347_encrypt_integration_properties_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe EncryptIntegrationProperties, :migration, schema: 20220204193000, feature_category: :integrations do + subject(:migration) { described_class.new } + + let(:integrations) { table(:integrations) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + end + + it 'correctly schedules background migrations', :aggregate_failures do + # update required + record1 = integrations.create!(properties: some_props) + record2 = integrations.create!(properties: some_props) + record3 = integrations.create!(properties: some_props) + record4 = integrations.create!(properties: nil) + record5 = integrations.create!(properties: nil) + + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_migration(record1.id, record2.id) + expect(described_class::MIGRATION).to be_scheduled_migration(record3.id, record4.id) + expect(described_class::MIGRATION).to be_scheduled_migration(record5.id, record5.id) + + expect(BackgroundMigrationWorker.jobs.size).to eq(3) + end + end + end + + def some_props + { iid: generate(:iid), url: generate(:url), username: generate(:username) }.to_json + end +end diff --git a/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb b/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb new file mode 100644 index 00000000000..89583d1050b --- /dev/null +++ b/spec/migrations/20220208080921_schedule_migrate_personal_namespace_project_maintainer_to_owner_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ScheduleMigratePersonalNamespaceProjectMaintainerToOwner, feature_category: :subgroups do + let!(:migration) { described_class::MIGRATION } + + describe '#up' do + it 'schedules background jobs for each batch of members' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :members, + column_name: :id, + interval: described_class::INTERVAL + ) + end + end +end diff --git a/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb b/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb new file mode 100644 index 00000000000..8a6a542bc5e --- /dev/null +++ b/spec/migrations/20220211214605_update_integrations_trigger_type_new_on_insert_null_safe_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe UpdateIntegrationsTriggerTypeNewOnInsertNullSafe, :migration, feature_category: :integrations do + let(:integrations) { table(:integrations) } + + before do + migrate! + end + + it 'leaves defined values alone' do + record = integrations.create!(type: 'XService', type_new: 'Integrations::Y') + + expect(integrations.find(record.id)).to have_attributes(type: 'XService', type_new: 'Integrations::Y') + end + + it 'keeps type_new synchronized with type' do + record = integrations.create!(type: 'AbcService', type_new: nil) + + expect(integrations.find(record.id)).to have_attributes( + type: 'AbcService', + type_new: 'Integrations::Abc' + ) + end + + it 'keeps type synchronized with type_new' do + record = integrations.create!(type: nil, type_new: 'Integrations::Abc') + + expect(integrations.find(record.id)).to have_attributes( + type: 'AbcService', + type_new: 'Integrations::Abc' + ) + end +end diff --git a/spec/migrations/20220213103859_remove_integrations_type_spec.rb b/spec/migrations/20220213103859_remove_integrations_type_spec.rb new file mode 100644 index 00000000000..8f6d9b0d9b5 --- /dev/null +++ b/spec/migrations/20220213103859_remove_integrations_type_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe RemoveIntegrationsType, :migration, feature_category: :integrations do + subject(:migration) { described_class.new } + + let(:integrations) { table(:integrations) } + let(:bg_migration) { instance_double(bg_migration_class) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + end + + it 'performs remaining background migrations', :aggregate_failures do + # Already migrated + integrations.create!(type: 'SlackService', type_new: 'Integrations::Slack') + # update required + record1 = integrations.create!(type: 'SlackService') + record2 = integrations.create!(type: 'JiraService') + record3 = integrations.create!(type: 'SlackService') + + migrate! + + expect(record1.reload.type_new).to eq 'Integrations::Slack' + expect(record2.reload.type_new).to eq 'Integrations::Jira' + expect(record3.reload.type_new).to eq 'Integrations::Slack' + end +end diff --git a/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb b/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb new file mode 100644 index 00000000000..b8a37dcd6d9 --- /dev/null +++ b/spec/migrations/20220222192524_create_not_null_constraint_releases_tag_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require 'spec_helper' +require_migration! + +RSpec.describe CreateNotNullConstraintReleasesTag, feature_category: :release_orchestration do + let!(:releases) { table(:releases) } + let!(:migration) { described_class.new } + + before do + allow(migration).to receive(:transaction_open?).and_return(false) + allow(migration).to receive(:with_lock_retries).and_yield + end + + it 'adds a check constraint to tags' do + constraint = releases.connection.check_constraints(:releases).find { |constraint| constraint.expression == "tag IS NOT NULL" } + expect(constraint).to be_nil + + migration.up + + constraint = releases.connection.check_constraints(:releases).find { |constraint| constraint.expression == "tag IS NOT NULL" } + expect(constraint).to be_a(ActiveRecord::ConnectionAdapters::CheckConstraintDefinition) + end +end diff --git a/spec/migrations/20220222192525_remove_null_releases_spec.rb b/spec/migrations/20220222192525_remove_null_releases_spec.rb new file mode 100644 index 00000000000..ce42dea077d --- /dev/null +++ b/spec/migrations/20220222192525_remove_null_releases_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require 'spec_helper' + +require_migration! + +RSpec.describe RemoveNullReleases, feature_category: :release_orchestration do + let(:releases) { table(:releases) } + + before do + # we need to migrate to before previous migration so an invalid record can be created + migrate! + migration_context.down(previous_migration(3).version) + + releases.create!(tag: 'good', name: 'good release', released_at: Time.now) + releases.create!(tag: nil, name: 'bad release', released_at: Time.now) + end + + it 'deletes template records and associated data' do + expect { migrate! } + .to change { releases.count }.from(2).to(1) + end +end diff --git a/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb b/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb new file mode 100644 index 00000000000..425f622581b --- /dev/null +++ b/spec/migrations/20220223124428_schedule_merge_topics_with_same_name_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ScheduleMergeTopicsWithSameName, feature_category: :projects do + let(:topics) { table(:topics) } + + describe '#up' do + before do + stub_const("#{described_class}::BATCH_SIZE", 2) + + topics.create!(name: 'topic1') + topics.create!(name: 'Topic2') + topics.create!(name: 'Topic3') + topics.create!(name: 'Topic4') + topics.create!(name: 'topic2') + topics.create!(name: 'topic3') + topics.create!(name: 'topic4') + topics.create!(name: 'TOPIC2') + topics.create!(name: 'topic5') + end + + it 'schedules MergeTopicsWithSameName background jobs', :aggregate_failures do + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(2.minutes, %w[topic2 topic3]) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(4.minutes, %w[topic4]) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + end +end diff --git a/spec/migrations/20220305223212_add_security_training_providers_spec.rb b/spec/migrations/20220305223212_add_security_training_providers_spec.rb new file mode 100644 index 00000000000..f67db3b68cd --- /dev/null +++ b/spec/migrations/20220305223212_add_security_training_providers_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddSecurityTrainingProviders, :migration, feature_category: :vulnerability_management do + include MigrationHelpers::WorkItemTypesHelper + + let!(:security_training_providers) { table(:security_training_providers) } + + it 'creates default data' do + # Need to delete all as security training providers are seeded before entire test suite + security_training_providers.delete_all + + reversible_migration do |migration| + migration.before -> { + expect(security_training_providers.count).to eq(0) + } + + migration.after -> { + expect(security_training_providers.count).to eq(2) + } + end + end +end diff --git a/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb b/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb new file mode 100644 index 00000000000..98e2ba4816b --- /dev/null +++ b/spec/migrations/20220307192610_remove_duplicate_project_tag_releases_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe RemoveDuplicateProjectTagReleases, feature_category: :release_orchestration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + let(:releases) { table(:releases) } + + let(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') } + let(:project) { projects.create!(namespace_id: namespace.id, name: 'foo') } + + let(:dup_releases) do + Array.new(4).fill do |i| + rel = releases.new(project_id: project.id, tag: "duplicate tag", released_at: (DateTime.now + i.days)) + rel.save!(validate: false) + rel + end + end + + let(:valid_release) do + releases.create!( + project_id: project.id, + tag: "valid tag", + released_at: DateTime.now + ) + end + + describe '#up' do + it "correctly removes duplicate tags from the same project" do + expect(dup_releases.length).to eq 4 + expect(valid_release).not_to be nil + expect(releases.where(tag: 'duplicate tag').count).to eq 4 + expect(releases.where(tag: 'valid tag').count).to eq 1 + + migrate! + + expect(releases.where(tag: 'duplicate tag').count).to eq 1 + expect(releases.where(tag: 'valid tag').count).to eq 1 + expect(releases.all.map(&:tag)).to match_array ['valid tag', 'duplicate tag'] + end + end +end diff --git a/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb b/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb new file mode 100644 index 00000000000..8df9907643e --- /dev/null +++ b/spec/migrations/20220309084954_remove_leftover_external_pull_request_deletions_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +require 'spec_helper' + +require_migration! + +RSpec.describe RemoveLeftoverExternalPullRequestDeletions, feature_category: :cell do + let(:deleted_records) { table(:loose_foreign_keys_deleted_records) } + + let(:pending_record1) { deleted_records.create!(id: 1, fully_qualified_table_name: 'public.external_pull_requests', primary_key_value: 1, status: 1) } + let(:pending_record2) { deleted_records.create!(id: 2, fully_qualified_table_name: 'public.external_pull_requests', primary_key_value: 2, status: 1) } + let(:other_pending_record1) { deleted_records.create!(id: 3, fully_qualified_table_name: 'public.projects', primary_key_value: 1, status: 1) } + let(:other_pending_record2) { deleted_records.create!(id: 4, fully_qualified_table_name: 'public.ci_builds', primary_key_value: 1, status: 1) } + let(:processed_record1) { deleted_records.create!(id: 5, fully_qualified_table_name: 'public.external_pull_requests', primary_key_value: 3, status: 2) } + let(:other_processed_record1) { deleted_records.create!(id: 6, fully_qualified_table_name: 'public.ci_builds', primary_key_value: 2, status: 2) } + + let!(:persisted_ids_before) do + [ + pending_record1, + pending_record2, + other_pending_record1, + other_pending_record2, + processed_record1, + other_processed_record1 + ].map(&:id).sort + end + + let!(:persisted_ids_after) do + [ + other_pending_record1, + other_pending_record2, + processed_record1, + other_processed_record1 + ].map(&:id).sort + end + + def all_ids + deleted_records.all.map(&:id).sort + end + + it 'deletes pending external_pull_requests records' do + expect { migrate! }.to change { all_ids }.from(persisted_ids_before).to(persisted_ids_after) + end +end diff --git a/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb new file mode 100644 index 00000000000..5d9be79e768 --- /dev/null +++ b/spec/migrations/20220310141349_remove_dependency_list_usage_data_from_redis_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe RemoveDependencyListUsageDataFromRedis, :migration, :clean_gitlab_redis_shared_state, + feature_category: :dependency_management do + let(:key) { "DEPENDENCY_LIST_USAGE_COUNTER" } + + describe "#up" do + it 'removes the hash from redis' do + with_redis do |redis| + redis.hincrby(key, 1, 1) + redis.hincrby(key, 2, 1) + end + + expect { migrate! }.to change { with_redis { |r| r.hgetall(key) } }.from({ '1' => '1', '2' => '1' }).to({}) + end + end + + def with_redis(&block) + Gitlab::Redis::SharedState.with(&block) + end +end diff --git a/spec/migrations/backfill_all_project_namespaces_spec.rb b/spec/migrations/backfill_all_project_namespaces_spec.rb new file mode 100644 index 00000000000..52fa46eea57 --- /dev/null +++ b/spec/migrations/backfill_all_project_namespaces_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillAllProjectNamespaces, :migration, feature_category: :subgroups do + let!(:migration) { described_class::MIGRATION } + + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:user_namespace) { namespaces.create!(name: 'user1', path: 'user1', visibility_level: 20, type: 'User') } + let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') } + let!(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) } + let!(:user_namespace_project) { projects.create!(name: 'user1_project', path: 'user1_project', namespace_id: user_namespace.id, visibility_level: 20) } + + describe '#up' do + it 'schedules background jobs for each batch of namespaces' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :projects, + column_name: :id, + job_arguments: [nil, 'up'], + interval: described_class::DELAY_INTERVAL + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb b/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb new file mode 100644 index 00000000000..47950f918c3 --- /dev/null +++ b/spec/migrations/backfill_cycle_analytics_aggregations_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillCycleAnalyticsAggregations, :migration, feature_category: :value_stream_management do + let(:migration) { described_class.new } + + let(:aggregations) { table(:analytics_cycle_analytics_aggregations) } + let(:namespaces) { table(:namespaces) } + let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) } + + context 'when there are value stream records' do + it 'inserts a record for each top-level namespace' do + group1 = namespaces.create!(path: 'aaa', name: 'aaa') + subgroup1 = namespaces.create!(path: 'bbb', name: 'bbb', parent_id: group1.id) + group2 = namespaces.create!(path: 'ccc', name: 'ccc') + + namespaces.create!(path: 'ddd', name: 'ddd') # not used + + group_value_streams.create!(name: 'for top level group', group_id: group2.id) + group_value_streams.create!(name: 'another for top level group', group_id: group2.id) + + group_value_streams.create!(name: 'for subgroup', group_id: subgroup1.id) + group_value_streams.create!(name: 'another for subgroup', group_id: subgroup1.id) + + migrate! + + expect(aggregations.pluck(:group_id)).to match_array([group1.id, group2.id]) + end + end + + it 'does nothing' do + expect { migrate! }.not_to change { aggregations.count } + end +end diff --git a/spec/migrations/backfill_group_features_spec.rb b/spec/migrations/backfill_group_features_spec.rb new file mode 100644 index 00000000000..1e7729a97d8 --- /dev/null +++ b/spec/migrations/backfill_group_features_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillGroupFeatures, :migration, feature_category: :feature_flags do + let(:migration) { described_class::MIGRATION } + + describe '#up' do + it 'schedules background jobs for each batch of namespaces' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :namespaces, + column_name: :id, + job_arguments: [described_class::BATCH_SIZE], + interval: described_class::INTERVAL, + batch_size: described_class::BATCH_SIZE + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb b/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb new file mode 100644 index 00000000000..892589dd770 --- /dev/null +++ b/spec/migrations/backfill_member_namespace_id_for_group_members_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillMemberNamespaceIdForGroupMembers, feature_category: :subgroups do + let!(:migration) { described_class::MIGRATION } + + describe '#up' do + it 'schedules background jobs for each batch of group members' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :members, + column_name: :id, + interval: described_class::INTERVAL + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb b/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb new file mode 100644 index 00000000000..627b18cd889 --- /dev/null +++ b/spec/migrations/backfill_namespace_id_for_namespace_routes_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillNamespaceIdForNamespaceRoutes, feature_category: :projects do + let!(:migration) { described_class::MIGRATION } + + describe '#up' do + it 'schedules background jobs for each batch of routes' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :routes, + column_name: :id, + interval: described_class::INTERVAL + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/migrations/backfill_project_namespaces_for_group_spec.rb b/spec/migrations/backfill_project_namespaces_for_group_spec.rb new file mode 100644 index 00000000000..b21ed6e1aa2 --- /dev/null +++ b/spec/migrations/backfill_project_namespaces_for_group_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillProjectNamespacesForGroup, feature_category: :subgroups do + let!(:migration) { described_class::MIGRATION } + + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:parent_group1) { namespaces.create!(name: 'parent_group1', path: 'parent_group1', visibility_level: 20, type: 'Group') } + let!(:parent_group1_project) { projects.create!(name: 'parent_group1_project', path: 'parent_group1_project', namespace_id: parent_group1.id, visibility_level: 20) } + + before do + allow(Gitlab).to receive(:com?).and_return(true) + end + + describe '#up' do + before do + stub_const("BackfillProjectNamespacesForGroup::GROUP_ID", parent_group1.id) + end + + it 'schedules background jobs for each batch of namespaces' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :projects, + column_name: :id, + job_arguments: [described_class::GROUP_ID, 'up'], + interval: described_class::DELAY_INTERVAL + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb new file mode 100644 index 00000000000..e2c117903d4 --- /dev/null +++ b/spec/migrations/populate_audit_event_streaming_verification_token_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe PopulateAuditEventStreamingVerificationToken, feature_category: :audit_events do + let(:groups) { table(:namespaces) } + let(:destinations) { table(:audit_events_external_audit_event_destinations) } + let(:migration) { described_class.new } + + let!(:group) { groups.create!(name: 'test-group', path: 'test-group') } + let!(:destination) { destinations.create!(namespace_id: group.id, destination_url: 'https://example.com/destination', verification_token: nil) } + + describe '#up' do + it 'adds verification tokens to records created before the migration' do + expect do + migrate! + destination.reload + end.to change { destination.verification_token }.from(nil).to(a_string_matching(/\w{24}/)) + end + end +end diff --git a/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb b/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb new file mode 100644 index 00000000000..c7709764727 --- /dev/null +++ b/spec/migrations/recreate_index_security_ci_builds_on_name_and_id_parser_with_new_features_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe RecreateIndexSecurityCiBuildsOnNameAndIdParserWithNewFeatures, :migration, feature_category: :continuous_integration do + let(:db) { described_class.new } + let(:pg_class) { table(:pg_class) } + let(:pg_index) { table(:pg_index) } + let(:async_indexes) { table(:postgres_async_indexes) } + + it 'recreates index' do + reversible_migration do |migration| + migration.before -> { + expect(async_indexes.where(name: described_class::OLD_INDEX_NAME).exists?).to be false + expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::OLD_INDEX_NAME)).to be true + expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::NEW_INDEX_NAME)).to be false + } + + migration.after -> { + expect(async_indexes.where(name: described_class::OLD_INDEX_NAME).exists?).to be true + expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::OLD_INDEX_NAME)).to be false + expect(db.index_exists?(described_class::TABLE, described_class::COLUMNS, name: described_class::NEW_INDEX_NAME)).to be true + } + end + end +end diff --git a/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb b/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb new file mode 100644 index 00000000000..91687d8d730 --- /dev/null +++ b/spec/migrations/remove_not_null_contraint_on_title_from_sprints_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe RemoveNotNullContraintOnTitleFromSprints, :migration, feature_category: :team_planning do + let(:migration) { described_class.new } + let(:namespaces) { table(:namespaces) } + let(:sprints) { table(:sprints) } + let(:iterations_cadences) { table(:iterations_cadences) } + + let!(:group) { namespaces.create!(name: 'foo', path: 'foo') } + let!(:cadence) { iterations_cadences.create!(group_id: group.id, title: "cadence 1") } + let!(:iteration1) { sprints.create!(id: 1, title: 'a', group_id: group.id, iterations_cadence_id: cadence.id, start_date: Date.new(2021, 11, 1), due_date: Date.new(2021, 11, 5), iid: 1) } + + describe '#down' do + it "removes null titles by setting them with ids" do + migration.up + + iteration2 = sprints.create!(id: 2, title: nil, group_id: group.id, iterations_cadence_id: cadence.id, start_date: Date.new(2021, 12, 1), due_date: Date.new(2021, 12, 5), iid: 2) + + migration.down + + expect(iteration1.reload.title).to eq 'a' + expect(iteration2.reload.title).to eq '2' + end + end +end diff --git a/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb b/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb new file mode 100644 index 00000000000..26764f855b7 --- /dev/null +++ b/spec/migrations/schedule_fix_incorrect_max_seats_used2_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ScheduleFixIncorrectMaxSeatsUsed2, :migration, feature_category: :purchase do + let(:migration_name) { described_class::MIGRATION.to_s.demodulize } + + describe '#up' do + it 'schedules a job on Gitlab.com' do + allow(Gitlab).to receive(:com?).and_return(true) + + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + expect(migration_name).to be_scheduled_delayed_migration(1.hour, 'batch_2_for_start_date_before_02_aug_2021') + expect(BackgroundMigrationWorker.jobs.size).to eq(1) + end + end + end + + it 'does not schedule any jobs when not Gitlab.com' do + allow(Gitlab).to receive(:com?).and_return(false) + + Sidekiq::Testing.fake! do + migrate! + + expect(migration_name).not_to be_scheduled_delayed_migration + expect(BackgroundMigrationWorker.jobs.size).to eq(0) + end + end + end +end diff --git a/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb b/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb new file mode 100644 index 00000000000..194a1d39ad1 --- /dev/null +++ b/spec/migrations/schedule_fix_incorrect_max_seats_used_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ScheduleFixIncorrectMaxSeatsUsed, :migration, feature_category: :purchase do + let(:migration) { described_class.new } + + describe '#up' do + it 'schedules a job on Gitlab.com' do + allow(Gitlab).to receive(:com?).and_return(true) + + expect(migration).to receive(:migrate_in).with(1.hour, 'FixIncorrectMaxSeatsUsed') + + migration.up + end + + it 'does not schedule any jobs when not Gitlab.com' do + allow(Gitlab::CurrentSettings).to receive(:com?).and_return(false) + + expect(migration).not_to receive(:migrate_in) + + migration.up + end + end +end diff --git a/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb b/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb new file mode 100644 index 00000000000..99ee9e58f4e --- /dev/null +++ b/spec/migrations/schedule_update_timelogs_null_spent_at_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe ScheduleUpdateTimelogsNullSpentAt, feature_category: :team_planning do + let!(:namespace) { table(:namespaces).create!(name: 'namespace', path: 'namespace') } + let!(:project) { table(:projects).create!(namespace_id: namespace.id) } + let!(:issue) { table(:issues).create!(project_id: project.id) } + let!(:merge_request) { table(:merge_requests).create!(target_project_id: project.id, source_branch: 'master', target_branch: 'feature') } + let!(:timelog1) { create_timelog!(merge_request_id: merge_request.id) } + let!(:timelog2) { create_timelog!(merge_request_id: merge_request.id) } + let!(:timelog3) { create_timelog!(merge_request_id: merge_request.id) } + let!(:timelog4) { create_timelog!(issue_id: issue.id) } + let!(:timelog5) { create_timelog!(issue_id: issue.id) } + + before do + table(:timelogs).where.not(id: timelog3.id).update_all(spent_at: nil) + end + + it 'correctly schedules background migrations' do + stub_const("#{described_class}::BATCH_SIZE", 2) + + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(2.minutes, timelog1.id, timelog2.id) + + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(4.minutes, timelog4.id, timelog5.id) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + + private + + def create_timelog!(**args) + table(:timelogs).create!(**args, time_spent: 1) + end +end diff --git a/spec/migrations/start_backfill_ci_queuing_tables_spec.rb b/spec/migrations/start_backfill_ci_queuing_tables_spec.rb new file mode 100644 index 00000000000..0a189b58c94 --- /dev/null +++ b/spec/migrations/start_backfill_ci_queuing_tables_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe StartBackfillCiQueuingTables, :suppress_gitlab_schemas_validate_connection, + feature_category: :continuous_integration do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:builds) { table(:ci_builds) } + + let!(:namespace) do + namespaces.create!(name: 'namespace1', path: 'namespace1') + end + + let!(:project) do + projects.create!(namespace_id: namespace.id, name: 'test1', path: 'test1') + end + + let!(:pending_build_1) do + builds.create!(status: :pending, name: 'test1', type: 'Ci::Build', project_id: project.id) + end + + let!(:running_build) do + builds.create!(status: :running, name: 'test2', type: 'Ci::Build', project_id: project.id) + end + + let!(:pending_build_2) do + builds.create!(status: :pending, name: 'test3', type: 'Ci::Build', project_id: project.id) + end + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + end + + it 'schedules jobs for builds that are pending' do + Sidekiq::Testing.fake! do + freeze_time do + migrate! + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration( + 2.minutes, pending_build_1.id, pending_build_1.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration( + 4.minutes, pending_build_2.id, pending_build_2.id) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end +end diff --git a/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb b/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb new file mode 100644 index 00000000000..66da9e6653d --- /dev/null +++ b/spec/migrations/update_application_settings_container_registry_exp_pol_worker_capacity_default_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe UpdateApplicationSettingsContainerRegistryExpPolWorkerCapacityDefault, + feature_category: :container_registry do + let(:settings) { table(:application_settings) } + + context 'with no rows in the application_settings table' do + it 'does not insert a row' do + expect { migrate! }.to not_change { settings.count } + end + end + + context 'with a row in the application_settings table' do + before do + settings.create!(container_registry_expiration_policies_worker_capacity: capacity) + end + + context 'with container_registry_expiration_policy_worker_capacity set to a value different than 0' do + let(:capacity) { 1 } + + it 'does not update the row' do + expect { migrate! } + .to not_change { settings.count } + .and not_change { settings.first.container_registry_expiration_policies_worker_capacity } + end + end + + context 'with container_registry_expiration_policy_worker_capacity set to 0' do + let(:capacity) { 0 } + + it 'updates the existing row' do + expect { migrate! } + .to not_change { settings.count } + .and change { settings.first.container_registry_expiration_policies_worker_capacity }.from(0).to(4) + end + end + end +end diff --git a/spec/migrations/update_application_settings_protected_paths_spec.rb b/spec/migrations/update_application_settings_protected_paths_spec.rb new file mode 100644 index 00000000000..c2bd4e8727d --- /dev/null +++ b/spec/migrations/update_application_settings_protected_paths_spec.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe UpdateApplicationSettingsProtectedPaths, :aggregate_failures, + feature_category: :system_access do + subject(:migration) { described_class.new } + + let!(:application_settings) { table(:application_settings) } + let!(:oauth_paths) { %w[/oauth/authorize /oauth/token] } + let!(:custom_paths) { %w[/foo /bar] } + + let(:default_paths) { application_settings.column_defaults.fetch('protected_paths') } + + before do + application_settings.create!(protected_paths: custom_paths) + application_settings.create!(protected_paths: custom_paths + oauth_paths) + application_settings.create!(protected_paths: custom_paths + oauth_paths.take(1)) + end + + describe '#up' do + before do + migrate! + application_settings.reset_column_information + end + + it 'removes the OAuth paths from the default value and persisted records' do + expect(default_paths).not_to include(*oauth_paths) + expect(default_paths).to eq(described_class::NEW_DEFAULT_PROTECTED_PATHS) + expect(application_settings.all).to all(have_attributes(protected_paths: custom_paths)) + end + end + + describe '#down' do + before do + migrate! + schema_migrate_down! + end + + it 'adds the OAuth paths to the default value and persisted records' do + expect(default_paths).to include(*oauth_paths) + expect(default_paths).to eq(described_class::OLD_DEFAULT_PROTECTED_PATHS) + expect(application_settings.all).to all(have_attributes(protected_paths: custom_paths + oauth_paths)) + end + end +end diff --git a/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb new file mode 100644 index 00000000000..15a8e79a610 --- /dev/null +++ b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile, feature_category: :dynamic_application_security_testing do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:dast_sites) { table(:dast_sites) } + let(:dast_site_profiles) { table(:dast_site_profiles) } + + before do + namespace = namespaces.create!(name: 'test', path: 'test') + project = projects.create!(id: 12, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab') + dast_site = dast_sites.create!(id: 1, url: 'https://www.gitlab.com', project_id: project.id) + + dast_site_profiles.create!( + id: 1, + project_id: project.id, + dast_site_id: dast_site.id, + name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 0", + scan_method: 0, + target_type: 0 + ) + + dast_site_profiles.create!( + id: 2, + project_id: project.id, + dast_site_id: dast_site.id, + name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 1", + scan_method: 0, + target_type: 1 + ) + end + + it 'updates the scan_method to 1 for profiles with target_type 1' do + migrate! + + expect(dast_site_profiles.where(scan_method: 1).count).to eq 1 + expect(dast_site_profiles.where(scan_method: 0).count).to eq 1 + end +end diff --git a/spec/migrations/update_invalid_member_states_spec.rb b/spec/migrations/update_invalid_member_states_spec.rb new file mode 100644 index 00000000000..6ae4b9f3c0f --- /dev/null +++ b/spec/migrations/update_invalid_member_states_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe UpdateInvalidMemberStates, feature_category: :subgroups do + let(:members) { table(:members) } + let(:groups) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:users) { table(:users) } + + before do + user = users.create!(first_name: 'Test', last_name: 'User', email: 'test@user.com', projects_limit: 1) + group = groups.create!(name: 'gitlab', path: 'gitlab-org') + project = projects.create!(namespace_id: group.id) + + members.create!(state: 2, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0) + members.create!(state: 2, source_id: project.id, source_type: 'Project', type: 'ProjectMember', user_id: user.id, access_level: 50, notification_level: 0) + members.create!(state: 1, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0) + members.create!(state: 0, source_id: group.id, source_type: 'Group', type: 'GroupMember', user_id: user.id, access_level: 50, notification_level: 0) + end + + it 'updates matching member record states' do + expect { migrate! } + .to change { members.where(state: 0).count }.from(1).to(3) + .and change { members.where(state: 2).count }.from(2).to(0) + .and change { members.where(state: 1).count }.by(0) + end +end diff --git a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb index 81d548e000a..db63521d1ee 100644 --- a/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/features/protected_branches_access_control_ce_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples "protected branches > access control > CE" do - ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)| + ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can push to" do visit project_protected_branches_path(project) @@ -67,7 +67,7 @@ RSpec.shared_examples "protected branches > access control > CE" do end end - ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)| + ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected branches that #{access_type_name} can merge to" do visit project_protected_branches_path(project) diff --git a/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb b/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb index 6aa9647bcec..f308b4ad372 100644 --- a/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb +++ b/spec/support/shared_examples/protected_tags/access_control_ce_shared_examples.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_examples "protected tags > access control > CE" do - ProtectedRefAccess::HUMAN_ACCESS_LEVELS.each do |(access_type_id, access_type_name)| + ProtectedRef::AccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| it "allows creating protected tags that #{access_type_name} can create" do visit project_protected_tags_path(project) |