diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-13 21:08:56 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-03-13 21:08:56 +0300 |
commit | 7b7bc31c5ba07eebe62e2f2582f111ce24285cd4 (patch) | |
tree | 70c795a932a603e49176d30ee5f0835fcfed46c2 /spec | |
parent | cb38c5062c623059d311c4e9e37428eacdea95d6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
21 files changed, 961 insertions, 499 deletions
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index 5185aa64d9f..3476c7b8465 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -7,8 +7,7 @@ RSpec.describe Oauth::AuthorizationsController do let(:application_scopes) { 'api read_user' } let!(:application) do - create(:oauth_application, scopes: application_scopes, - redirect_uri: 'http://example.com') + create(:oauth_application, scopes: application_scopes, redirect_uri: 'http://example.com') end let(:params) do diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index 1f78314c372..70876f137c3 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -334,9 +334,10 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category: expect(controller).to receive(:atlassian_oauth2).and_wrap_original do |m, *args| m.call(*args) - expect(Gitlab::ApplicationContext.current) - .to include('meta.user' => user.username, - 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2') + expect(Gitlab::ApplicationContext.current).to include( + 'meta.user' => user.username, + 'meta.caller_id' => 'OmniauthCallbacksController#atlassian_oauth2' + ) end post :atlassian_oauth2 @@ -441,8 +442,12 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category: before do stub_last_request_id(last_request_id) - stub_omniauth_saml_config(enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'], - providers: [saml_config]) + stub_omniauth_saml_config( + enabled: true, + auto_link_saml_user: true, + allow_single_sign_on: ['saml'], + providers: [saml_config] + ) mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response) request.env['devise.mapping'] = Devise.mappings[:user] request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth'] @@ -533,9 +538,10 @@ RSpec.describe OmniauthCallbacksController, type: :controller, feature_category: expect(controller).to receive(:saml).and_wrap_original do |m, *args| m.call(*args) - expect(Gitlab::ApplicationContext.current) - .to include('meta.user' => user.username, - 'meta.caller_id' => 'OmniauthCallbacksController#saml') + expect(Gitlab::ApplicationContext.current).to include( + 'meta.user' => user.username, + 'meta.caller_id' => 'OmniauthCallbacksController#saml' + ) end post :saml, params: { SAMLResponse: mock_saml_response } diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 343c7f53022..098d1201939 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -113,6 +113,50 @@ RSpec.describe 'Pipeline', :js, feature_category: :projects do end end + describe 'pipeline stats text' do + let(:finished_pipeline) do + create(:ci_pipeline, :success, project: project, + ref: 'master', sha: project.commit.id, user: user) + end + + before do + finished_pipeline.update!(started_at: "2023-01-01 01:01:05", created_at: "2023-01-01 01:01:01", + finished_at: "2023-01-01 01:01:10", duration: 9) + end + + context 'pipeline has finished' do + it 'shows pipeline stats with flag on' do + visit project_pipeline_path(project, finished_pipeline) + + within '.pipeline-info' do + expect(page).to have_content("in #{finished_pipeline.duration} seconds") + expect(page).to have_content("and was queued for #{finished_pipeline.queued_duration} seconds") + end + end + + it 'shows pipeline stats with flag off' do + stub_feature_flags(refactor_ci_minutes_consumption: false) + + visit project_pipeline_path(project, finished_pipeline) + + within '.pipeline-info' do + expect(page).to have_content("in #{finished_pipeline.duration} seconds " \ + "and was queued for #{finished_pipeline.queued_duration} seconds") + end + end + end + + context 'pipeline has not finished' do + it 'does not show pipeline stats' do + visit_pipeline + + within '.pipeline-info' do + expect(page).not_to have_selector('[data-testid="pipeline-stats-text"]') + end + end + end + end + describe 'related merge requests' do context 'when there are no related merge requests' do it 'shows a "no related merge requests" message' do diff --git a/spec/fixtures/api/schemas/internal/pages/lookup_path.json b/spec/fixtures/api/schemas/internal/pages/lookup_path.json index 9d81ea495f1..8ca71870911 100644 --- a/spec/fixtures/api/schemas/internal/pages/lookup_path.json +++ b/spec/fixtures/api/schemas/internal/pages/lookup_path.json @@ -23,7 +23,8 @@ }, "additionalProperties": false }, - "prefix": { "type": "string" } + "prefix": { "type": "string" }, + "unique_domain": { "type": ["string", "null"] } }, "additionalProperties": false } diff --git a/spec/frontend/analytics/shared/components/daterange_spec.js b/spec/frontend/analytics/shared/components/daterange_spec.js index 8e4b60efa67..5f0847e0db6 100644 --- a/spec/frontend/analytics/shared/components/daterange_spec.js +++ b/spec/frontend/analytics/shared/components/daterange_spec.js @@ -86,18 +86,19 @@ describe('Daterange component', () => { }); describe('set', () => { - it('emits the change event with an object containing startDate and endDate', () => { + it('emits the change event with an object containing startDate and endDate', async () => { const startDate = new Date('2019-10-01'); const endDate = new Date('2019-10-05'); - wrapper.vm.dateRange = { startDate, endDate }; - expect(wrapper.emitted().change).toEqual([[{ startDate, endDate }]]); + await findDaterangePicker().vm.$emit('input', { startDate, endDate }); + + expect(wrapper.emitted('change')).toEqual([[{ startDate, endDate }]]); }); }); describe('get', () => { - it("returns value of dateRange from state's startDate and endDate", () => { - expect(wrapper.vm.dateRange).toEqual({ + it("datepicker to have default of dateRange from state's startDate and endDate", () => { + expect(findDaterangePicker().props('value')).toEqual({ startDate: defaultProps.startDate, endDate: defaultProps.endDate, }); diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js deleted file mode 100644 index 86c4ad9b361..00000000000 --- a/spec/frontend/work_items/components/notes/activity_filter_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import { - WORK_ITEM_NOTES_FILTER_ALL_NOTES, - WORK_ITEM_NOTES_FILTER_ONLY_HISTORY, - WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, - TRACKING_CATEGORY_SHOW, -} from '~/work_items/constants'; - -import { mockTracking } from 'helpers/tracking_helper'; - -describe('Work Item Activity/Discussions Filtering', () => { - let wrapper; - - const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findOnlyCommentsItem = () => wrapper.findByTestId('comments-activity'); - const findOnlyHistoryItem = () => wrapper.findByTestId('history-activity'); - - const createComponent = ({ - discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES, - loading = false, - workItemType = 'Task', - } = {}) => { - wrapper = shallowMountExtended(ActivityFilter, { - propsData: { - discussionFilter, - loading, - workItemType, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - describe('Default', () => { - it('has a dropdown with 3 options', () => { - expect(findDropdown().exists()).toBe(true); - expect(findAllDropdownItems()).toHaveLength(ActivityFilter.filterOptions.length); - }); - - it('has local storage sync with the correct props', () => { - expect(findLocalStorageSync().props('asString')).toBe(true); - }); - - it('emits `changeFilter` event when local storage input is emitted', () => { - findLocalStorageSync().vm.$emit('input', WORK_ITEM_NOTES_FILTER_ONLY_HISTORY); - - expect(wrapper.emitted('changeFilter')).toEqual([[WORK_ITEM_NOTES_FILTER_ONLY_HISTORY]]); - }); - }); - - describe('Changing filter value', () => { - it.each` - dropdownLabel | filterValue | dropdownItem - ${'Comments only'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${findOnlyCommentsItem} - ${'History only'} | ${WORK_ITEM_NOTES_FILTER_ONLY_HISTORY} | ${findOnlyHistoryItem} - `( - 'when `$dropdownLabel` is clicked it emits `$filterValue` with tracking info', - ({ dropdownItem, filterValue }) => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - dropdownItem().vm.$emit('click'); - - expect(wrapper.emitted('changeFilter')).toEqual([[filterValue]]); - - expect(trackingSpy).toHaveBeenCalledWith( - TRACKING_CATEGORY_SHOW, - 'work_item_notes_filter_changed', - { - category: TRACKING_CATEGORY_SHOW, - label: 'item_track_notes_filtering', - property: 'type_Task', - }, - ); - }, - ); - }); -}); diff --git a/spec/frontend/work_items/components/notes/activity_sort_spec.js b/spec/frontend/work_items/components/notes/activity_sort_spec.js deleted file mode 100644 index 289823dc59e..00000000000 --- a/spec/frontend/work_items/components/notes/activity_sort_spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import ActivitySort from '~/work_items/components/notes/activity_sort.vue'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import { ASC, DESC } from '~/notes/constants'; - -import { mockTracking } from 'helpers/tracking_helper'; -import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants'; - -describe('Work Item Activity Sorting', () => { - let wrapper; - - const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); - const findNewestFirstItem = () => wrapper.findByTestId('newest-first'); - - const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => { - wrapper = shallowMountExtended(ActivitySort, { - propsData: { - sortOrder, - loading, - workItemType, - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - describe('default', () => { - it('has a dropdown with 2 options', () => { - expect(findDropdown().exists()).toBe(true); - expect(findAllDropdownItems()).toHaveLength(ActivitySort.sortOptions.length); - }); - - it('has local storage sync with the correct props', () => { - expect(findLocalStorageSync().props('asString')).toBe(true); - }); - - it('emits `changeSort` event when update is emitted', () => { - findLocalStorageSync().vm.$emit('input', ASC); - - expect(wrapper.emitted('changeSort')).toEqual([[ASC]]); - }); - }); - - describe('when asc', () => { - describe('when the dropdown is clicked', () => { - it('calls the right actions', () => { - const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); - findNewestFirstItem().vm.$emit('click'); - - expect(wrapper.emitted('changeSort')).toEqual([[DESC]]); - - expect(trackingSpy).toHaveBeenCalledWith( - TRACKING_CATEGORY_SHOW, - 'work_item_notes_sort_order_changed', - { - category: TRACKING_CATEGORY_SHOW, - label: 'item_track_notes_sorting', - property: 'type_Task', - }, - ); - }); - }); - }); -}); diff --git a/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js new file mode 100644 index 00000000000..5ed9d581446 --- /dev/null +++ b/spec/frontend/work_items/components/notes/work_item_activity_sort_filter_spec.js @@ -0,0 +1,109 @@ +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import WorkItemActivitySortFilter from '~/work_items/components/notes/work_item_activity_sort_filter.vue'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import { ASC, DESC } from '~/notes/constants'; +import { + WORK_ITEM_ACTIVITY_SORT_OPTIONS, + WORK_ITEM_NOTES_SORT_ORDER_KEY, + WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS, + WORK_ITEM_NOTES_FILTER_KEY, + WORK_ITEM_NOTES_FILTER_ALL_NOTES, + WORK_ITEM_ACTIVITY_FILTER_OPTIONS, + TRACKING_CATEGORY_SHOW, +} from '~/work_items/constants'; + +import { mockTracking } from 'helpers/tracking_helper'; + +describe('Work Item Activity/Discussions Filtering', () => { + let wrapper; + + const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findDropdown = () => wrapper.findComponent(GlDropdown); + const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem); + const findByDataTestId = (dataTestId) => wrapper.findByTestId(dataTestId); + + const createComponent = ({ + loading = false, + workItemType = 'Task', + sortFilterProp = ASC, + filterOptions = WORK_ITEM_ACTIVITY_SORT_OPTIONS, + trackingLabel = 'item_track_notes_sorting', + trackingAction = 'work_item_notes_sort_order_changed', + filterEvent = 'changeSort', + defaultSortFilterProp = ASC, + storageKey = WORK_ITEM_NOTES_SORT_ORDER_KEY, + } = {}) => { + wrapper = shallowMountExtended(WorkItemActivitySortFilter, { + propsData: { + loading, + workItemType, + sortFilterProp, + filterOptions, + trackingLabel, + trackingAction, + filterEvent, + defaultSortFilterProp, + storageKey, + }, + }); + }; + + describe.each` + usedFor | filterOptions | storageKey | filterEvent | newInputOption | trackingLabel | trackingAction | defaultSortFilterProp | sortFilterProp | nonDefaultDataTestId + ${'Sorting'} | ${WORK_ITEM_ACTIVITY_SORT_OPTIONS} | ${WORK_ITEM_NOTES_SORT_ORDER_KEY} | ${'changeSort'} | ${DESC} | ${'item_track_notes_sorting'} | ${'work_item_notes_sort_order_changed'} | ${ASC} | ${ASC} | ${'newest-first'} + ${'Filtering'} | ${WORK_ITEM_ACTIVITY_FILTER_OPTIONS} | ${WORK_ITEM_NOTES_FILTER_KEY} | ${'changeFilter'} | ${WORK_ITEM_NOTES_FILTER_ONLY_COMMENTS} | ${'item_track_notes_sorting'} | ${'work_item_notes_filter_changed'} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${WORK_ITEM_NOTES_FILTER_ALL_NOTES} | ${'comments-activity'} + `( + 'When used for $usedFor', + ({ + filterOptions, + storageKey, + filterEvent, + trackingLabel, + trackingAction, + newInputOption, + defaultSortFilterProp, + sortFilterProp, + nonDefaultDataTestId, + }) => { + beforeEach(() => { + createComponent({ + sortFilterProp, + filterOptions, + trackingLabel, + trackingAction, + filterEvent, + defaultSortFilterProp, + storageKey, + }); + }); + + it('has a dropdown with options equal to the length of `filterOptions`', () => { + expect(findDropdown().exists()).toBe(true); + expect(findAllDropdownItems()).toHaveLength(filterOptions.length); + }); + + it('has local storage sync with the correct props', () => { + expect(findLocalStorageSync().props('asString')).toBe(true); + expect(findLocalStorageSync().props('storageKey')).toBe(storageKey); + }); + + it(`emits ${filterEvent} event when local storage input is emitted`, () => { + findLocalStorageSync().vm.$emit('input', newInputOption); + + expect(wrapper.emitted(filterEvent)).toEqual([[newInputOption]]); + }); + + it('emits tracking event when the a non default dropdown item is clicked', () => { + const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + findByDataTestId(nonDefaultDataTestId).vm.$emit('click'); + + expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, trackingAction, { + category: TRACKING_CATEGORY_SHOW, + label: trackingLabel, + property: 'type_Task', + }); + }); + }, + ); +}); diff --git a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js index 3b87a5e3e88..daf74f7a93b 100644 --- a/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_notes_activity_header_spec.js @@ -1,7 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import WorkItemNotesActivityHeader from '~/work_items/components/notes/work_item_notes_activity_header.vue'; -import ActivitySort from '~/work_items/components/notes/activity_sort.vue'; -import ActivityFilter from '~/work_items/components/notes/activity_filter.vue'; import { ASC } from '~/notes/constants'; import { WORK_ITEM_NOTES_FILTER_ALL_NOTES, @@ -12,8 +10,8 @@ describe('Work Item Note Activity Header', () => { let wrapper; const findActivityLabelHeading = () => wrapper.find('h3'); - const findActivityFilterDropdown = () => wrapper.findComponent(ActivityFilter); - const findActivitySortDropdown = () => wrapper.findComponent(ActivitySort); + const findActivityFilterDropdown = () => wrapper.findByTestId('work-item-filter'); + const findActivitySortDropdown = () => wrapper.findByTestId('work-item-sort'); const createComponent = ({ disableActivityFilterSort = false, @@ -21,7 +19,7 @@ describe('Work Item Note Activity Header', () => { workItemType = 'Task', discussionFilter = WORK_ITEM_NOTES_FILTER_ALL_NOTES, } = {}) => { - wrapper = shallowMount(WorkItemNotesActivityHeader, { + wrapper = shallowMountExtended(WorkItemNotesActivityHeader, { propsData: { disableActivityFilterSort, sortOrder, diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 63a470c82a1..1af69f85a54 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::MigrationHelpers do +RSpec.describe Gitlab::Database::MigrationHelpers, feature_category: :database do include Database::TableSchemaHelpers include Database::TriggerHelpers @@ -979,6 +979,65 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end end + + context 'when creating foreign key on a partitioned table' do + before do + model.execute(<<~SQL) + CREATE TABLE public._test_source_partitioned_table ( + id serial NOT NULL, + partition_id serial NOT NULL, + owner_id bigint NOT NULL, + PRIMARY KEY (id, partition_id) + ) PARTITION BY LIST(partition_id); + + CREATE TABLE _test_source_partitioned_table_1 + PARTITION OF public._test_source_partitioned_table + FOR VALUES IN (1); + + CREATE TABLE public._test_dest_partitioned_table ( + id serial NOT NULL, + partition_id serial NOT NULL, + PRIMARY KEY (id, partition_id) + ); + SQL + end + + it 'creates the FK without using NOT VALID', :aggregate_failures do + allow(model).to receive(:execute).and_call_original + + expect(model).to receive(:with_lock_retries).and_yield + + expect(model).to receive(:execute).with( + "ALTER TABLE _test_source_partitioned_table\n" \ + "ADD CONSTRAINT fk_multiple_columns\n" \ + "FOREIGN KEY \(partition_id, owner_id\)\n" \ + "REFERENCES _test_dest_partitioned_table \(partition_id, id\)\n" \ + "ON UPDATE CASCADE\n" \ + "ON DELETE CASCADE\n;\n" + ) + + model.add_concurrent_foreign_key( + :_test_source_partitioned_table, + :_test_dest_partitioned_table, + column: [:partition_id, :owner_id], + target_column: [:partition_id, :id], + name: :fk_multiple_columns, + on_update: :cascade, + allow_partitioned: true + ) + end + + it 'raises an error if allow_partitioned is not set' do + expect(model).not_to receive(:with_lock_retries).and_yield + expect(model).not_to receive(:execute).with(/FOREIGN KEY/) + + args = [:_test_source_partitioned_table, :_test_dest_partitioned_table] + options = { column: [:partition_id, :owner_id], target_column: [:partition_id, :id] } + + expect { model.add_concurrent_foreign_key(*args, **options) } + .to raise_error ArgumentError, /use add_concurrent_partitioned_foreign_key/ + end + end end end @@ -2896,4 +2955,18 @@ RSpec.describe Gitlab::Database::MigrationHelpers do it { is_expected.to be_falsey } end end + + describe "#table_partitioned?" do + subject { model.table_partitioned?(table_name) } + + let(:table_name) { 'p_ci_builds_metadata' } + + it { is_expected.to be_truthy } + + context 'with a non-partitioned table' do + let(:table_name) { 'users' } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb index f0e34476cf2..d5f4afd7ba4 100644 --- a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb +++ b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers do +RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers, feature_category: :database do include Database::TableSchemaHelpers let(:migration) do @@ -16,15 +16,23 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers let(:partition_schema) { 'gitlab_partitions_dynamic' } let(:partition1_name) { "#{partition_schema}.#{source_table_name}_202001" } let(:partition2_name) { "#{partition_schema}.#{source_table_name}_202002" } + let(:validate) { true } let(:options) do { column: column_name, name: foreign_key_name, on_delete: :cascade, - validate: true + on_update: nil, + primary_key: :id } end + let(:create_options) do + options + .except(:primary_key) + .merge!(reverse_lock_order: false, target_column: :id, validate: validate) + end + before do allow(migration).to receive(:puts) allow(migration).to receive(:transaction_open?).and_return(false) @@ -67,12 +75,11 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers expect(migration).to receive(:concurrent_partitioned_foreign_key_name).and_return(foreign_key_name) - expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **options) - expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **options) + expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **create_options) + expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **create_options) - expect(migration).to receive(:with_lock_retries).ordered.and_yield - expect(migration).to receive(:add_foreign_key) - .with(source_table_name, target_table_name, **options) + expect(migration).to receive(:add_concurrent_foreign_key) + .with(source_table_name, target_table_name, allow_partitioned: true, **create_options) .ordered .and_call_original @@ -81,6 +88,39 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers expect_foreign_key_to_exist(source_table_name, foreign_key_name) end + context 'with validate: false option' do + let(:validate) { false } + let(:options) do + { + column: column_name, + name: foreign_key_name, + on_delete: :cascade, + on_update: nil, + primary_key: :id + } + end + + it 'creates the foreign key only on partitions' do + expect(migration).to receive(:foreign_key_exists?) + .with(source_table_name, target_table_name, **options) + .and_return(false) + + expect(migration).to receive(:concurrent_partitioned_foreign_key_name).and_return(foreign_key_name) + + expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **create_options) + expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **create_options) + + expect(migration).not_to receive(:add_concurrent_foreign_key) + .with(source_table_name, target_table_name, **create_options) + + migration.add_concurrent_partitioned_foreign_key( + source_table_name, target_table_name, + column: column_name, validate: false) + + expect_foreign_key_not_to_exist(source_table_name, foreign_key_name) + end + end + def expect_add_concurrent_fk_and_call_original(source_table_name, target_table_name, options) expect(migration).to receive(:add_concurrent_foreign_key) .ordered @@ -100,8 +140,6 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers .and_return(true) expect(migration).not_to receive(:add_concurrent_foreign_key) - expect(migration).not_to receive(:with_lock_retries) - expect(migration).not_to receive(:add_foreign_key) migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name, column: column_name) @@ -110,30 +148,43 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers end context 'when additional foreign key options are given' do - let(:options) do + let(:exits_options) do { column: column_name, name: '_my_fk_name', on_delete: :restrict, - validate: true + on_update: nil, + primary_key: :id } end + let(:create_options) do + exits_options + .except(:primary_key) + .merge!(reverse_lock_order: false, target_column: :id, validate: true) + end + it 'forwards them to the foreign key helper methods' do expect(migration).to receive(:foreign_key_exists?) - .with(source_table_name, target_table_name, **options) + .with(source_table_name, target_table_name, **exits_options) .and_return(false) expect(migration).not_to receive(:concurrent_partitioned_foreign_key_name) - expect_add_concurrent_fk(partition1_name, target_table_name, **options) - expect_add_concurrent_fk(partition2_name, target_table_name, **options) + expect_add_concurrent_fk(partition1_name, target_table_name, **create_options) + expect_add_concurrent_fk(partition2_name, target_table_name, **create_options) - expect(migration).to receive(:with_lock_retries).ordered.and_yield - expect(migration).to receive(:add_foreign_key).with(source_table_name, target_table_name, **options).ordered + expect(migration).to receive(:add_concurrent_foreign_key) + .with(source_table_name, target_table_name, allow_partitioned: true, **create_options) + .ordered - migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name, - column: column_name, name: '_my_fk_name', on_delete: :restrict) + migration.add_concurrent_partitioned_foreign_key( + source_table_name, + target_table_name, + column: column_name, + name: '_my_fk_name', + on_delete: :restrict + ) end def expect_add_concurrent_fk(source_table_name, target_table_name, options) @@ -153,4 +204,39 @@ RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers end end end + + describe '#validate_partitioned_foreign_key' do + context 'when run inside a transaction block' do + it 'raises an error' do + expect(migration).to receive(:transaction_open?).and_return(true) + + expect do + migration.validate_partitioned_foreign_key(source_table_name, column_name, name: '_my_fk_name') + end.to raise_error(/can not be run inside a transaction/) + end + end + + context 'when run outside a transaction block' do + before do + migration.add_concurrent_partitioned_foreign_key( + source_table_name, + target_table_name, + column: column_name, + name: foreign_key_name, + validate: false + ) + end + + it 'validates FK for each partition' do + expect(migration).to receive(:execute).with(/SET statement_timeout TO 0/).twice + expect(migration).to receive(:execute).with(/RESET statement_timeout/).twice + expect(migration).to receive(:execute) + .with(/ALTER TABLE #{partition1_name} VALIDATE CONSTRAINT #{foreign_key_name}/).ordered + expect(migration).to receive(:execute) + .with(/ALTER TABLE #{partition2_name} VALIDATE CONSTRAINT #{foreign_key_name}/).ordered + + migration.validate_partitioned_foreign_key(source_table_name, column_name, name: foreign_key_name) + end + end + end end diff --git a/spec/lib/gitlab/pages/virtual_host_finder_spec.rb b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb new file mode 100644 index 00000000000..4b584a45503 --- /dev/null +++ b/spec/lib/gitlab/pages/virtual_host_finder_spec.rb @@ -0,0 +1,214 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Pages::VirtualHostFinder, feature_category: :pages do + let_it_be(:project) { create(:project) } + + before_all do + project.update_pages_deployment!(create(:pages_deployment, project: project)) + end + + it 'returns nil when host is empty' do + expect(described_class.new(nil).execute).to be_nil + expect(described_class.new('').execute).to be_nil + end + + context 'when host is a pages custom domain host' do + let_it_be(:pages_domain) { create(:pages_domain, project: project) } + + subject(:virtual_domain) { described_class.new(pages_domain.domain).execute } + + context 'when there are no pages deployed for the project' do + before_all do + project.mark_pages_as_not_deployed + end + + it 'returns nil' do + expect(virtual_domain).to be_nil + end + end + + context 'when there are pages deployed for the project' do + before_all do + project.mark_pages_as_deployed + end + + it 'returns the virual domain when there are pages deployed for the project' do + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.cache_key).to match(/pages_domain_for_domain_#{pages_domain.id}_/) + expect(virtual_domain.lookup_paths.length).to eq(1) + expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) + end + + context 'when :cache_pages_domain_api is disabled' do + before do + stub_feature_flags(cache_pages_domain_api: false) + end + + it 'returns the virual domain when there are pages deployed for the project' do + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.cache_key).to be_nil + expect(virtual_domain.lookup_paths.length).to eq(1) + expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) + end + end + end + end + + context 'when host is a namespace domain' do + context 'when there are no pages deployed for the project' do + before_all do + project.mark_pages_as_not_deployed + end + + it 'returns no result if the provided host is not subdomain of the Pages host' do + virtual_domain = described_class.new("#{project.namespace.path}.something.io").execute + + expect(virtual_domain).to eq(nil) + end + + it 'returns the virual domain with no lookup_paths' do + virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute + + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/) + expect(virtual_domain.lookup_paths.length).to eq(0) + end + + context 'when :cache_pages_domain_api is disabled' do + before do + stub_feature_flags(cache_pages_domain_api: false) + end + + it 'returns the virual domain with no lookup_paths' do + virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}".downcase).execute + + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.cache_key).to be_nil + expect(virtual_domain.lookup_paths.length).to eq(0) + end + end + end + + context 'when there are pages deployed for the project' do + before_all do + project.mark_pages_as_deployed + project.namespace.update!(path: 'topNAMEspace') + end + + it 'returns no result if the provided host is not subdomain of the Pages host' do + virtual_domain = described_class.new("#{project.namespace.path}.something.io").execute + + expect(virtual_domain).to eq(nil) + end + + it 'returns the virual domain when there are pages deployed for the project' do + virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute + + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/) + expect(virtual_domain.lookup_paths.length).to eq(1) + expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) + end + + it 'finds domain with case-insensitive' do + virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host.upcase}").execute + + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{project.namespace.id}_/) + expect(virtual_domain.lookup_paths.length).to eq(1) + expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) + end + + context 'when :cache_pages_domain_api is disabled' do + before_all do + stub_feature_flags(cache_pages_domain_api: false) + end + + it 'returns the virual domain when there are pages deployed for the project' do + virtual_domain = described_class.new("#{project.namespace.path}.#{Settings.pages.host}").execute + + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.cache_key).to be_nil + expect(virtual_domain.lookup_paths.length).to eq(1) + expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) + end + end + end + end + + context 'when host is a unique domain' do + before_all do + project.project_setting.update!(pages_unique_domain: 'unique-domain') + end + + subject(:virtual_domain) { described_class.new("unique-domain.#{Settings.pages.host.upcase}").execute } + + context 'when pages unique domain is enabled' do + before_all do + project.project_setting.update!(pages_unique_domain_enabled: true) + end + + context 'when there are no pages deployed for the project' do + before_all do + project.mark_pages_as_not_deployed + end + + it 'returns nil' do + expect(virtual_domain).to be_nil + end + end + + context 'when there are pages deployed for the project' do + before_all do + project.mark_pages_as_deployed + end + + it 'returns the virual domain when there are pages deployed for the project' do + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.lookup_paths.length).to eq(1) + expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) + end + + context 'when :cache_pages_domain_api is disabled' do + before do + stub_feature_flags(cache_pages_domain_api: false) + end + + it 'returns the virual domain when there are pages deployed for the project' do + expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) + expect(virtual_domain.lookup_paths.length).to eq(1) + expect(virtual_domain.lookup_paths.first.project_id).to eq(project.id) + end + end + end + end + + context 'when pages unique domain is disabled' do + before_all do + project.project_setting.update!(pages_unique_domain_enabled: false) + end + + context 'when there are no pages deployed for the project' do + before_all do + project.mark_pages_as_not_deployed + end + + it 'returns nil' do + expect(virtual_domain).to be_nil + end + end + + context 'when there are pages deployed for the project' do + before_all do + project.mark_pages_as_deployed + end + + it 'returns nil' do + expect(virtual_domain).to be_nil + end + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 6f445de8aee..d529319e6e9 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -1047,41 +1047,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic end end - describe '#action_monthly_active_users', :clean_gitlab_redis_shared_state do - let(:time_period) { { created_at: 2.days.ago..time } } - let(:time) { Time.zone.now } - let(:user1) { build(:user, id: 1) } - let(:user2) { build(:user, id: 2) } - let(:user3) { build(:user, id: 3) } - let(:user4) { build(:user, id: 4) } - let(:project) { build(:project) } - - before do - counter = Gitlab::UsageDataCounters::EditorUniqueCounter - - counter.track_web_ide_edit_action(author: user1, project: project) - counter.track_web_ide_edit_action(author: user1, project: project) - counter.track_sfe_edit_action(author: user1, project: project) - counter.track_snippet_editor_edit_action(author: user1, project: project) - counter.track_snippet_editor_edit_action(author: user1, time: time - 3.days, project: project) - - counter.track_web_ide_edit_action(author: user2, project: project) - counter.track_sfe_edit_action(author: user2, project: project) - - counter.track_web_ide_edit_action(author: user3, time: time - 3.days, project: project) - counter.track_snippet_editor_edit_action(author: user3, project: project) - end - - it 'returns the distinct count of user actions within the specified time period' do - expect(described_class.action_monthly_active_users(time_period)).to eq( - { - action_monthly_active_users_sfe_edit: 2, - action_monthly_active_users_snippet_editor_edit: 2 - } - ) - end - end - describe '.service_desk_counts' do subject { described_class.send(:service_desk_counts) } diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb index 2c75d4d5c41..75c5cac899b 100644 --- a/spec/models/concerns/each_batch_spec.rb +++ b/spec/models/concerns/each_batch_spec.rb @@ -171,4 +171,36 @@ RSpec.describe EachBatch do end end end + + describe '.each_batch_count' do + let_it_be(:users) { create_list(:user, 5, updated_at: 1.day.ago) } + + it 'counts the records' do + count, last_value = User.each_batch_count + + expect(count).to eq(5) + expect(last_value).to eq(nil) + end + + context 'when using a different column' do + it 'returns correct count' do + count, _ = User.each_batch_count(column: :email, of: 2) + + expect(count).to eq(5) + end + end + + context 'when stopping and resuming the counting' do + it 'returns the correct count' do + count, last_value = User.each_batch_count(of: 1) do |current_count, _current_value| + current_count == 3 # stop when count reaches 3 + end + + expect(count).to eq(3) + + final_count, _ = User.each_batch_count(of: 1, last_value: last_value, last_count: count) + expect(final_count).to eq(5) + end + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 60338d42bfa..a6d94222b65 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -1137,43 +1137,6 @@ RSpec.describe Namespace, feature_category: :subgroups do end end - describe '.find_by_pages_host' do - it 'finds namespace by GitLab Pages host and is case-insensitive' do - namespace = create(:namespace, name: 'topNAMEspace', path: 'topNAMEspace') - create(:namespace, name: 'annother_namespace') - host = "TopNamespace.#{Settings.pages.host.upcase}" - - expect(described_class.find_by_pages_host(host)).to eq(namespace) - end - - context 'when there is non-top-level group with searched name' do - before do - create(:group, :nested, path: 'pages') - end - - it 'ignores this group' do - host = "pages.#{Settings.pages.host.upcase}" - - expect(described_class.find_by_pages_host(host)).to be_nil - end - - it 'finds right top level group' do - group = create(:group, path: 'pages') - - host = "pages.#{Settings.pages.host.upcase}" - - expect(described_class.find_by_pages_host(host)).to eq(group) - end - end - - it "returns no result if the provided host is not subdomain of the Pages host" do - create(:namespace, name: 'namespace.io') - host = "namespace.io" - - expect(described_class.find_by_pages_host(host)).to eq(nil) - end - end - describe '.top_most' do let_it_be(:namespace) { create(:namespace) } let_it_be(:group) { create(:group) } @@ -2290,34 +2253,6 @@ RSpec.describe Namespace, feature_category: :subgroups do end end - describe '#pages_virtual_domain' do - let(:project) { create(:project, namespace: namespace) } - let(:virtual_domain) { namespace.pages_virtual_domain } - - before do - project.mark_pages_as_deployed - project.update_pages_deployment!(create(:pages_deployment, project: project)) - end - - it 'returns the virual domain' do - expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) - expect(virtual_domain.lookup_paths).not_to be_empty - expect(virtual_domain.cache_key).to match(/pages_domain_for_namespace_#{namespace.root_ancestor.id}_/) - end - - context 'when :cache_pages_domain_api is disabled' do - before do - stub_feature_flags(cache_pages_domain_api: false) - end - - it 'returns the virual domain' do - expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) - expect(virtual_domain.lookup_paths).not_to be_empty - expect(virtual_domain.cache_key).to be_nil - end - end - end - describe '#any_project_with_pages_deployed?' do it 'returns true if any project nested under the group has pages deployed' do parent_1 = create(:group) # Three projects, one with pages diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb index ef79ba28d5d..38ff1bb090e 100644 --- a/spec/models/pages/lookup_path_spec.rb +++ b/spec/models/pages/lookup_path_spec.rb @@ -61,15 +61,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do it 'uses deployment from object storage' do freeze_time do - expect(source).to( - eq({ - type: 'zip', - path: deployment.file.url(expire_at: 1.day.from_now), - global_id: "gid://gitlab/PagesDeployment/#{deployment.id}", - sha256: deployment.file_sha256, - file_size: deployment.size, - file_count: deployment.file_count - }) + expect(source).to eq( + type: 'zip', + path: deployment.file.url(expire_at: 1.day.from_now), + global_id: "gid://gitlab/PagesDeployment/#{deployment.id}", + sha256: deployment.file_sha256, + file_size: deployment.size, + file_count: deployment.file_count ) end end @@ -87,15 +85,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do it 'uses file protocol' do freeze_time do - expect(source).to( - eq({ - type: 'zip', - path: 'file://' + deployment.file.path, - global_id: "gid://gitlab/PagesDeployment/#{deployment.id}", - sha256: deployment.file_sha256, - file_size: deployment.size, - file_count: deployment.file_count - }) + expect(source).to eq( + type: 'zip', + path: "file://#{deployment.file.path}", + global_id: "gid://gitlab/PagesDeployment/#{deployment.id}", + sha256: deployment.file_sha256, + file_size: deployment.size, + file_count: deployment.file_count ) end end @@ -108,15 +104,13 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do it 'uses deployment from object storage' do freeze_time do - expect(source).to( - eq({ - type: 'zip', - path: deployment.file.url(expire_at: 1.day.from_now), - global_id: "gid://gitlab/PagesDeployment/#{deployment.id}", - sha256: deployment.file_sha256, - file_size: deployment.size, - file_count: deployment.file_count - }) + expect(source).to eq( + type: 'zip', + path: deployment.file.url(expire_at: 1.day.from_now), + global_id: "gid://gitlab/PagesDeployment/#{deployment.id}", + sha256: deployment.file_sha256, + file_size: deployment.size, + file_count: deployment.file_count ) end end @@ -143,4 +137,25 @@ RSpec.describe Pages::LookupPath, feature_category: :pages do expect(lookup_path.prefix).to eq('/myproject/') end end + + describe '#unique_domain' do + let(:project) { build(:project) } + + context 'when unique domain is disabled' do + it 'returns nil' do + project.project_setting.pages_unique_domain_enabled = false + + expect(lookup_path.unique_domain).to be_nil + end + end + + context 'when unique domain is enabled' do + it 'returns the project unique domain' do + project.project_setting.pages_unique_domain_enabled = true + project.project_setting.pages_unique_domain = 'unique-domain' + + expect(lookup_path.unique_domain).to eq('unique-domain') + end + end + end end diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index f054fde78e7..ee198f73785 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -546,44 +546,6 @@ RSpec.describe PagesDomain do end end - describe '#pages_virtual_domain' do - let(:project) { create(:project) } - let(:pages_domain) { create(:pages_domain, project: project) } - - context 'when there are no pages deployed for the project' do - it 'returns nil' do - expect(pages_domain.pages_virtual_domain).to be_nil - end - end - - context 'when there are pages deployed for the project' do - let(:virtual_domain) { pages_domain.pages_virtual_domain } - - before do - project.mark_pages_as_deployed - project.update_pages_deployment!(create(:pages_deployment, project: project)) - end - - it 'returns the virual domain when there are pages deployed for the project' do - expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) - expect(virtual_domain.lookup_paths).not_to be_empty - expect(virtual_domain.cache_key).to match(/pages_domain_for_domain_#{pages_domain.id}_/) - end - - context 'when :cache_pages_domain_api is disabled' do - before do - stub_feature_flags(cache_pages_domain_api: false) - end - - it 'returns the virual domain when there are pages deployed for the project' do - expect(virtual_domain).to be_an_instance_of(Pages::VirtualDomain) - expect(virtual_domain.lookup_paths).not_to be_empty - expect(virtual_domain.cache_key).to be_nil - end - end - end - end - describe '#validate_custom_domain_count_per_project' do let_it_be(:project) { create(:project) } diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index f68ea0cd84c..c9341934ec9 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -141,7 +141,11 @@ RSpec.describe API::Files, feature_category: :source_code_management do it 'caches sha256 of the content', :use_clean_rails_redis_caching do head api(route(file_path), current_user, **options), params: params - expect(Gitlab::Cache::Client).to receive(:build_with_metadata).and_call_original + expect(Gitlab::Cache::Client).to receive(:build_with_metadata).with( + cache_identifier: 'API::Files#content_sha', + feature_category: :source_code_management, + backing_resource: :gitaly + ).and_call_original expect(Rails.cache.fetch("blob_content_sha256:#{project.full_path}:#{response.headers['X-Gitlab-Blob-Id']}")) .to eq(content_sha256) diff --git a/spec/requests/api/internal/pages_spec.rb b/spec/requests/api/internal/pages_spec.rb index 56f1089843b..67f5b7f8ccb 100644 --- a/spec/requests/api/internal/pages_spec.rb +++ b/spec/requests/api/internal/pages_spec.rb @@ -212,7 +212,98 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do 'sha256' => deployment.file_sha256, 'file_size' => deployment.size, 'file_count' => deployment.file_count - } + }, + 'unique_domain' => nil + } + ] + ) + end + end + end + + context 'unique domain' do + let(:project) { create(:project) } + + before do + project.project_setting.update!( + pages_unique_domain: 'unique-domain', + pages_unique_domain_enabled: true) + end + + context 'when there are no pages deployed for the related project' do + it 'responds with 204 No Content' do + query_host('unique-domain.example.com') + + expect(response).to have_gitlab_http_status(:no_content) + end + end + + context 'when there are pages deployed for the related project' do + context 'when the feature flag is disabled' do + before do + stub_feature_flags(pages_unique_domain: false) + end + + context 'when there are no pages deployed for the related project' do + it 'responds with 204 No Content' do + deploy_pages(project) + + query_host('unique-domain.example.com') + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + context 'when the unique domain is disabled' do + before do + project.project_setting.update!(pages_unique_domain_enabled: false) + end + + context 'when there are no pages deployed for the related project' do + it 'responds with 204 No Content' do + deploy_pages(project) + + query_host('unique-domain.example.com') + + expect(response).to have_gitlab_http_status(:no_content) + end + end + end + + it 'domain lookup is case insensitive' do + deploy_pages(project) + + query_host('Unique-Domain.example.com') + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'responds with the correct domain configuration' do + deploy_pages(project) + + query_host('unique-domain.example.com') + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('internal/pages/virtual_domain') + + deployment = project.pages_metadatum.pages_deployment + expect(json_response['lookup_paths']).to eq( + [ + { + 'project_id' => project.id, + 'access_control' => false, + 'https_only' => false, + 'prefix' => '/', + 'source' => { + 'type' => 'zip', + 'path' => deployment.file.url(expire_at: 1.day.from_now), + 'global_id' => "gid://gitlab/PagesDeployment/#{deployment.id}", + 'sha256' => deployment.file_sha256, + 'file_size' => deployment.size, + 'file_count' => deployment.file_count + }, + 'unique_domain' => 'unique-domain' } ] ) @@ -253,7 +344,8 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do 'sha256' => deployment.file_sha256, 'file_size' => deployment.size, 'file_count' => deployment.file_count - } + }, + 'unique_domain' => nil } ] ) @@ -299,7 +391,8 @@ RSpec.describe API::Internal::Pages, feature_category: :pages do 'sha256' => deployment.file_sha256, 'file_size' => deployment.size, 'file_count' => deployment.file_count - } + }, + 'unique_domain' => nil } ] ) diff --git a/spec/requests/projects/merge_requests_discussions_spec.rb b/spec/requests/projects/merge_requests_discussions_spec.rb index d82fa284a42..54ee5e489f7 100644 --- a/spec/requests/projects/merge_requests_discussions_spec.rb +++ b/spec/requests/projects/merge_requests_discussions_spec.rb @@ -59,7 +59,7 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana .to change { Gitlab::GitalyClient.get_request_count }.by_at_most(4) end - context 'caching', :use_clean_rails_memory_store_caching do + context 'caching' do let(:reference) { create(:issue, project: project) } let(:author) { create(:user) } let!(:first_note) { create(:diff_note_on_merge_request, author: author, noteable: merge_request, project: project, note: "reference: #{reference.to_reference}") } @@ -81,193 +81,180 @@ RSpec.describe 'merge requests discussions', feature_category: :source_code_mana shared_examples 'cache hit' do it 'gets cached on subsequent requests' do - expect_next_instance_of(DiscussionSerializer) do |serializer| - expect(serializer).not_to receive(:represent) - end + expect(DiscussionSerializer).not_to receive(:new) send_request end end - context 'when mr_discussions_http_cache and disabled_mr_discussions_redis_cache are enabled' do - before do - send_request - end - - it_behaves_like 'cache hit' + before do + send_request + end - context 'when a note in a discussion got updated' do - before do - first_note.update!(updated_at: 1.minute.from_now) - end + it_behaves_like 'cache hit' - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + context 'when a note in a discussion got updated' do + before do + first_note.update!(updated_at: 1.minute.from_now) end - context 'when a note in a discussion got its reference state updated' do - before do - reference.close! - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } + end + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + context 'when a note in a discussion got its reference state updated' do + before do + reference.close! end - context 'when a note in a discussion got resolved' do - before do - travel_to(1.minute.from_now) do - first_note.resolve!(user) - end - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } + end + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } + context 'when a note in a discussion got resolved' do + before do + travel_to(1.minute.from_now) do + first_note.resolve!(user) end end - context 'when a note is added to a discussion' do - let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) } - - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note, third_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when a note is removed from a discussion' do - before do - second_note.destroy! - end + context 'when a note is added to a discussion' do + let!(:third_note) { create(:diff_note_on_merge_request, in_reply_to: first_note, noteable: merge_request, project: project) } - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note, third_note] } end + end - context 'when an emoji is awarded to a note in discussion' do - before do - travel_to(1.minute.from_now) do - create(:award_emoji, awardable: first_note) - end - end + context 'when a note is removed from a discussion' do + before do + second_note.destroy! + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note] } end + end - context 'when an award emoji is removed from a note in discussion' do - before do - travel_to(1.minute.from_now) do - award_emoji.destroy! - end + context 'when an emoji is awarded to a note in discussion' do + before do + travel_to(1.minute.from_now) do + create(:award_emoji, awardable: first_note) end + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when the diff note position changes' do - before do - # This replicates a position change wherein timestamps aren't updated - # which is why `Gitlab::Timeless.timeless` is utilized. This is the - # same approach being used in Discussions::UpdateDiffPositionService - # which is responsible for updating the positions of diff discussions - # when MR updates. - first_note.position = Gitlab::Diff::Position.new( - old_path: first_note.position.old_path, - new_path: first_note.position.new_path, - old_line: first_note.position.old_line, - new_line: first_note.position.new_line + 1, - diff_refs: first_note.position.diff_refs - ) - - Gitlab::Timeless.timeless(first_note, &:save) + context 'when an award emoji is removed from a note in discussion' do + before do + travel_to(1.minute.from_now) do + award_emoji.destroy! end + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when the HEAD diff note position changes' do - before do - # This replicates a DiffNotePosition change. This is the same approach - # being used in Discussions::CaptureDiffNotePositionService which is - # responsible for updating/creating DiffNotePosition of a diff discussions - # in relation to HEAD diff. - new_position = Gitlab::Diff::Position.new( - old_path: first_note.position.old_path, - new_path: first_note.position.new_path, - old_line: first_note.position.old_line, - new_line: first_note.position.new_line + 1, - diff_refs: first_note.position.diff_refs - ) - - DiffNotePosition.create_or_update_for( - first_note, - diff_type: :head, - position: new_position, - line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521' - ) - end + context 'when the diff note position changes' do + before do + # This replicates a position change wherein timestamps aren't updated + # which is why `Gitlab::Timeless.timeless` is utilized. This is the + # same approach being used in Discussions::UpdateDiffPositionService + # which is responsible for updating the positions of diff discussions + # when MR updates. + first_note.position = Gitlab::Diff::Position.new( + old_path: first_note.position.old_path, + new_path: first_note.position.new_path, + old_line: first_note.position.old_line, + new_line: first_note.position.new_line + 1, + diff_refs: first_note.position.diff_refs + ) + + Gitlab::Timeless.timeless(first_note, &:save) + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when author detail changes' do - before do - author.update!(name: "#{author.name} (Updated)") - end + context 'when the HEAD diff note position changes' do + before do + # This replicates a DiffNotePosition change. This is the same approach + # being used in Discussions::CaptureDiffNotePositionService which is + # responsible for updating/creating DiffNotePosition of a diff discussions + # in relation to HEAD diff. + new_position = Gitlab::Diff::Position.new( + old_path: first_note.position.old_path, + new_path: first_note.position.new_path, + old_line: first_note.position.old_line, + new_line: first_note.position.new_line + 1, + diff_refs: first_note.position.diff_refs + ) + + DiffNotePosition.create_or_update_for( + first_note, + diff_type: :head, + position: new_position, + line_code: 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521' + ) + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when author status changes' do - before do - Users::SetStatusService.new(author, message: "updated status").execute - end + context 'when author detail changes' do + before do + author.update!(name: "#{author.name} (Updated)") + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when author role changes' do - before do - Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership) - end + context 'when author status changes' do + before do + Users::SetStatusService.new(author, message: "updated status").execute + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end + end - context 'when current_user role changes' do - before do - Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user)) - end + context 'when author role changes' do + before do + Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(author_membership) + end - it_behaves_like 'cache miss' do - let(:changed_notes) { [first_note, second_note] } - end + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } end end - context 'when disabled_mr_discussions_redis_cache is disabled' do + context 'when current_user role changes' do before do - stub_feature_flags(disabled_mr_discussions_redis_cache: false) - send_request + Members::UpdateService.new(owner, access_level: Gitlab::Access::GUEST).execute(project.member(user)) end - it_behaves_like 'cache hit' + it_behaves_like 'cache miss' do + let(:changed_notes) { [first_note, second_note] } + end end end end diff --git a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb index 122774a9028..565e79e14aa 100644 --- a/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/auto_disabling_hooks_shared_examples.rb @@ -61,6 +61,20 @@ disabled_until: disabled_until) # Nothing is missing expect(find_hooks.executable.to_a + find_hooks.disabled.to_a).to match_array(find_hooks.to_a) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'causes all hooks to be considered executable' do + expect(find_hooks.executable.count).to eq(16) + end + + it 'causes no hooks to be considered disabled' do + expect(find_hooks.disabled).to be_empty + end + end end describe '#executable?', :freeze_time do @@ -108,6 +122,16 @@ disabled_until: disabled_until) it 'has the correct state' do expect(web_hook.executable?).to eq(executable) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is always executable' do + expect(web_hook).to be_executable + end + end end end @@ -172,6 +196,16 @@ disabled_until: disabled_until) def run_expectation expect { hook.backoff! }.to change { hook.backoff_count }.by(1) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not increment backoff count' do + expect { hook.failed! }.not_to change { hook.backoff_count } + end + end end end end @@ -181,6 +215,16 @@ disabled_until: disabled_until) def run_expectation expect { hook.failed! }.to change { hook.recent_failures }.by(1) end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not increment recent failure count' do + expect { hook.failed! }.not_to change { hook.recent_failures } + end + end end end @@ -189,6 +233,16 @@ disabled_until: disabled_until) expect { hook.disable! }.to change { hook.executable? }.from(true).to(false) end + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'does not disable the hook' do + expect { hook.disable! }.not_to change { hook.executable? } + end + end + it 'does nothing if the hook is already disabled' do allow(hook).to receive(:permanently_disabled?).and_return(true) @@ -228,6 +282,16 @@ disabled_until: disabled_until) it 'is true' do expect(hook).to be_temporarily_disabled end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is false' do + expect(hook).not_to be_temporarily_disabled + end + end end end @@ -244,6 +308,16 @@ disabled_until: disabled_until) it 'is true' do expect(hook).to be_permanently_disabled end + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it 'is false' do + expect(hook).not_to be_permanently_disabled + end + end end end @@ -258,6 +332,14 @@ disabled_until: disabled_until) end it { is_expected.to eq :disabled } + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it { is_expected.to eq(:executable) } + end end context 'when hook has been backed off' do @@ -267,6 +349,14 @@ disabled_until: disabled_until) end it { is_expected.to eq :temporarily_disabled } + + context 'when the flag is disabled' do + before do + stub_feature_flags(auto_disabling_web_hooks: false) + end + + it { is_expected.to eq(:executable) } + end end end end |