diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml index 22df689a9cf..98bfa38d1fc 100644 --- a/.gitlab/ci/qa-common/main.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml @@ -15,7 +15,7 @@ include: gitlab_auth_token_variable_name: "PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE" allure_job_name: "${QA_RUN_TYPE}" - project: gitlab-org/quality/pipeline-common - ref: 8.3.0 + ref: 8.3.2 file: - /ci/base.gitlab-ci.yml - /ci/knapsack-report.yml diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue index b9d6173a777..929175b964f 100644 --- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue +++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue @@ -49,7 +49,7 @@ export default { return getIdFromGraphQLId(this.resource.id); }, hasLatestVersion() { - return this.latestVersion?.tagName; + return this.latestVersion?.name; }, hasPipelineStatus() { return this.pipelineStatus?.text; @@ -58,7 +58,7 @@ export default { return this.resource.latestVersion; }, versionBadgeText() { - return this.latestVersion.tagName; + return this.latestVersion.name; }, webPath() { return cleanLeadingSeparator(this.resource?.webPath); @@ -92,7 +92,7 @@ export default { v-if="hasLatestVersion" size="sm" class="gl-ml-3 gl-my-1" - :href="latestVersion.tagPath" + :href="latestVersion.path" > {{ versionBadgeText }} diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue index 57d19af614f..42f8cea8727 100644 --- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue +++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue @@ -67,8 +67,8 @@ export default { releasedAt() { return getTimeago().format(this.latestVersion?.releasedAt); }, - tagName() { - return this.latestVersion?.tagName || this.$options.i18n.unreleased; + name() { + return this.latestVersion?.name || this.$options.i18n.unreleased; }, webPath() { return cleanLeadingSeparator(this.resource?.webPath); @@ -117,7 +117,7 @@ export default { {{ resource.name }}
- {{ tagName }} + {{ name }} -import { GlButton, GlModal, GlModalDirective, GlCard, GlIcon } from '@gitlab/ui'; +import { + GlButton, + GlModal, + GlModalDirective, + GlCard, + GlIcon, + GlDisclosureDropdown, + GlCollapsibleListbox, + GlFormGroup, +} from '@gitlab/ui'; import { createAlert } from '~/alert'; import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { expandSection } from '~/settings_panels'; import { scrollToElement } from '~/lib/utils/common_utils'; +import createBranchRuleMutation from './graphql/mutations/create_branch_rule.mutation.graphql'; import BranchRule from './components/branch_rule.vue'; import { I18N, PROTECTED_BRANCHES_ANCHOR, BRANCH_PROTECTION_MODAL_ID } from './constants'; @@ -14,12 +25,16 @@ export default { BranchRule, GlButton, GlModal, + GlFormGroup, GlCard, GlIcon, + GlCollapsibleListbox, + GlDisclosureDropdown, }, directives: { GlModal: GlModalDirective, }, + mixins: [glFeatureFlagsMixin()], apollo: { branchRules: { query: branchRulesQuery, @@ -38,18 +53,87 @@ export default { }, inject: { projectPath: { default: '' }, + branchRulesPath: { default: '' }, }, data() { return { branchRules: [], + branchRuleName: '', + searchQuery: '', }; }, + computed: { + addRuleItems() { + return [{ text: this.$options.i18n.branchName, action: () => this.openCreateRuleModal() }]; + }, + createRuleItems() { + return this.isWildcardAvailable ? [this.wildcardItem] : this.filteredOpenBranches; + }, + filteredOpenBranches() { + const openBranches = window.gon.open_branches.map((item) => ({ + text: item.text, + value: item.text, + })); + return openBranches.filter((item) => item.text.includes(this.searchQuery)); + }, + wildcardItem() { + return { text: this.$options.i18n.createWildcard, value: this.searchQuery }; + }, + isWildcardAvailable() { + return this.searchQuery.includes('*'); + }, + createRuleText() { + return this.branchRuleName || this.$options.i18n.branchNamePlaceholder; + }, + branchRuleEditPath() { + return `${this.branchRulesPath}?branch=${encodeURIComponent(this.branchRuleName)}`; + }, + primaryProps() { + return { + text: this.$options.i18n.createProtectedBranch, + attributes: { + variant: 'confirm', + disabled: !this.branchRuleName, + }, + }; + }, + cancelProps() { + return { + text: this.$options.i18n.createBranchRule, + }; + }, + }, methods: { showProtectedBranches() { // Protected branches section is on the same page as the branch rules section. expandSection(this.$options.protectedBranchesAnchor); scrollToElement(this.$options.protectedBranchesAnchor); }, + openCreateRuleModal() { + this.$refs[this.$options.modalId].show(); + }, + handleBranchRuleSearch(query) { + this.searchQuery = query; + }, + addBranchRule() { + this.$apollo + .mutate({ + mutation: createBranchRuleMutation, + variables: { + projectPath: this.projectPath, + name: this.branchRuleName, + }, + }) + .then(() => { + window.location.assign(this.branchRuleEditPath); + }) + .catch(() => { + createAlert({ message: this.$options.i18n.createBranchRuleError }); + }); + }, + selectBranchRuleName(branchName) { + this.branchRuleName = branchName; + }, }, modalId: BRANCH_PROTECTION_MODAL_ID, protectedBranchesAnchor: PROTECTED_BRANCHES_ANCHOR, @@ -72,7 +156,15 @@ export default { {{ branchRules.length }}
+ + + + + + + + `](../../discussions/index.md#mentions)) in issues, commits, and merge requests +Mentioning subgroups ([`@`](../../discussions/index.md#mentions)) in epics, issues, commits, and merge requests notifies all direct members of that group. Inherited members of a subgroup are not notified by mentions. Mentioning works the same as for projects and groups, and you can choose the group of members to be notified. diff --git a/doc/user/project/pages/getting_started/pages_from_scratch.md b/doc/user/project/pages/getting_started/pages_from_scratch.md index 9de2703b82b..0695cc20ccd 100644 --- a/doc/user/project/pages/getting_started/pages_from_scratch.md +++ b/doc/user/project/pages/getting_started/pages_from_scratch.md @@ -70,7 +70,7 @@ This specific Ruby image is maintained on [DockerHub](https://hub.docker.com/_/r Edit your `.gitlab-ci.yml` file and add this text as the first line: ```yaml -image: ruby:2.7 +image: ruby:3.2 ``` If your SSG needs [NodeJS](https://nodejs.org/) to build, you must specify an @@ -156,7 +156,7 @@ pages: Your `.gitlab-ci.yml` file should now look like this: ```yaml -image: ruby:2.7 +image: ruby:3.2 pages: script: @@ -198,7 +198,7 @@ First, add a `workflow` section to force the pipeline to run only when changes a pushed to branches: ```yaml -image: ruby:2.7 +image: ruby:3.2 workflow: rules: @@ -218,7 +218,7 @@ Then configure the pipeline to run the job for the [default branch](../../repository/branches/default.md) (here, `main`) only. ```yaml -image: ruby:2.7 +image: ruby:3.2 workflow: rules: @@ -249,7 +249,7 @@ To specify a stage for your job to run in, add a `stage` line to your CI file: ```yaml -image: ruby:2.7 +image: ruby:3.2 workflow: rules: @@ -273,7 +273,7 @@ Now add another job to the CI file, telling it to test every push to every branch **except** the `main` branch: ```yaml -image: ruby:2.7 +image: ruby:3.2 workflow: rules: @@ -325,7 +325,7 @@ for both jobs, `pages` and `test`. Move these commands to a `before_script` section: ```yaml -image: ruby:2.7 +image: ruby:3.2 workflow: rules: @@ -366,7 +366,7 @@ This example caches Jekyll dependencies in a `vendor` directory when you run `bundle install`: ```yaml -image: ruby:2.7 +image: ruby:3.2 workflow: rules: diff --git a/lib/gitlab/application_setting_fetcher.rb b/lib/gitlab/application_setting_fetcher.rb index ec33a36f028..cc8f67dc541 100644 --- a/lib/gitlab/application_setting_fetcher.rb +++ b/lib/gitlab/application_setting_fetcher.rb @@ -8,7 +8,15 @@ module Gitlab end def current_application_settings - cached_application_settings + cached_application_settings || uncached_application_settings + end + + def current_application_settings? + ::ApplicationSetting.current.present? + end + + def expire_current_application_settings + ::ApplicationSetting.expire end private @@ -25,9 +33,48 @@ module Gitlab end end + def uncached_application_settings + return fake_application_settings if Gitlab::Runtime.rake? && !connect_to_db? + + current_settings = ::ApplicationSetting.current + + # If there are pending migrations, it's possible there are columns that + # need to be added to the application settings. To prevent Rake tasks + # and other callers from failing, use any loaded settings and return + # defaults for missing columns. + if Gitlab::Runtime.rake? && ::ApplicationSetting.connection.migration_context.needs_migration? + db_attributes = current_settings&.attributes || {} + fake_application_settings(db_attributes) + elsif current_settings.present? + current_settings + else + ::ApplicationSetting.create_from_defaults + end + rescue ::ApplicationSetting::Recursion + in_memory_application_settings + end + def in_memory_application_settings @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults end + + def fake_application_settings(attributes = {}) + Gitlab::FakeApplicationSettings.new(::ApplicationSetting.defaults.merge(attributes || {})) + end + + def connect_to_db? + # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised + active_db_connection = begin + ::ApplicationSetting.connection.active? + rescue StandardError + false + end + + active_db_connection && + ApplicationSetting.database.cached_table_exists? + rescue ActiveRecord::NoDatabaseError + false + end end end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 9b8d9880434..64e0478734b 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -12,22 +12,18 @@ module Gitlab end def current_application_settings - Gitlab::SafeRequestStore.fetch(:current_application_settings) { ensure_application_settings! } + Gitlab::SafeRequestStore.fetch(:current_application_settings) { Gitlab::ApplicationSettingFetcher.current_application_settings } end def current_application_settings? - Gitlab::SafeRequestStore.exist?(:current_application_settings) || ::ApplicationSetting.current.present? + Gitlab::SafeRequestStore.exist?(:current_application_settings) || Gitlab::ApplicationSettingFetcher.current_application_settings? end def expire_current_application_settings - ::ApplicationSetting.expire + Gitlab::ApplicationSettingFetcher.expire_current_application_settings Gitlab::SafeRequestStore.delete(:current_application_settings) end - def clear_in_memory_application_settings! - @in_memory_application_settings = nil - end - def method_missing(name, *args, **kwargs, &block) current_application_settings.send(name, *args, **kwargs, &block) # rubocop:disable GitlabSecurity/PublicSend end @@ -35,54 +31,6 @@ module Gitlab def respond_to_missing?(name, include_private = false) current_application_settings.respond_to?(name, include_private) || super end - - private - - def ensure_application_settings! - Gitlab::ApplicationSettingFetcher.current_application_settings || uncached_application_settings - end - - def uncached_application_settings - return fake_application_settings if Gitlab::Runtime.rake? && !connect_to_db? - - current_settings = ::ApplicationSetting.current - # If there are pending migrations, it's possible there are columns that - # need to be added to the application settings. To prevent Rake tasks - # and other callers from failing, use any loaded settings and return - # defaults for missing columns. - if Gitlab::Runtime.rake? && ::ApplicationSetting.connection.migration_context.needs_migration? - db_attributes = current_settings&.attributes || {} - fake_application_settings(db_attributes) - elsif current_settings.present? - current_settings - else - ::ApplicationSetting.create_from_defaults - end - rescue ::ApplicationSetting::Recursion - in_memory_application_settings - end - - def fake_application_settings(attributes = {}) - Gitlab::FakeApplicationSettings.new(::ApplicationSetting.defaults.merge(attributes || {})) - end - - def in_memory_application_settings - @in_memory_application_settings ||= ::ApplicationSetting.build_from_defaults - end - - def connect_to_db? - # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised - active_db_connection = begin - ::ApplicationSetting.connection.active? - rescue StandardError - false - end - - active_db_connection && - ApplicationSetting.database.cached_table_exists? - rescue ActiveRecord::NoDatabaseError - false - end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 75fbc14023f..33368bfd4d5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8663,15 +8663,27 @@ msgstr "" msgid "BranchRules|Branch name or pattern" msgstr "" +msgid "BranchRules|Branch rule created." +msgstr "" + msgid "BranchRules|Branch rules details" msgstr "" +msgid "BranchRules|Cancel" +msgstr "" + msgid "BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Learn more.%{linkEnd}" msgstr "" +msgid "BranchRules|Create branch rule" +msgstr "" + msgid "BranchRules|Create protected branch" msgstr "" +msgid "BranchRules|Create wildcard" +msgstr "" + msgid "BranchRules|Create wildcard: %{searchTerm}" msgstr "" @@ -8729,6 +8741,12 @@ msgstr "" msgid "BranchRules|Roles" msgstr "" +msgid "BranchRules|Select Branch or create wildcard" +msgstr "" + +msgid "BranchRules|Something went wrong while creating branch rule." +msgstr "" + msgid "BranchRules|Status checks" msgstr "" @@ -8747,6 +8765,9 @@ msgstr "" msgid "BranchRules|View details" msgstr "" +msgid "BranchRules|Wildcards such as *-stable or production/* are supported" +msgstr "" + msgid "BranchRules|default" msgstr "" diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js index 658a135534b..1c791857df9 100644 --- a/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js +++ b/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js @@ -12,8 +12,8 @@ describe('CiResourceAbout', () => { openMergeRequestsCount: 9, latestVersion: { id: 1, - tagName: 'v1.0.0', - tagPath: 'path/to/release', + name: 'v1.0.0', + path: 'path/to/release', releasedAt: '2022-08-23T17:19:09Z', }, webPath: 'path/to/project', diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js index 6af9daabea0..b35c8a40744 100644 --- a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js +++ b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js @@ -113,7 +113,7 @@ describe('CiResourceHeader', () => { createComponent({ props: { pipelineStatus: status, - latestVersion: { tagName: '1.0.0', tagPath: 'path/to/release' }, + latestVersion: { name: '1.0.0', path: 'path/to/release' }, }, }); }); diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js index d74b133f386..15add3f307f 100644 --- a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js +++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js @@ -21,7 +21,7 @@ describe('CiResourcesListItem', () => { const release = { author: { name: 'author', webUrl: '/user/1' }, releasedAt: Date.now(), - tagName: '1.0.0', + name: '1.0.0', }; const defaultProps = { resource, @@ -114,7 +114,7 @@ describe('CiResourcesListItem', () => { it('renders the version badge', () => { expect(findBadge().exists()).toBe(true); - expect(findBadge().text()).toBe(release.tagName); + expect(findBadge().text()).toBe(release.name); }); }); }); diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js index e370ac5054f..9ca35943ae5 100644 --- a/spec/frontend/ci/catalog/mock.js +++ b/spec/frontend/ci/catalog/mock.js @@ -298,8 +298,8 @@ export const catalogSharedDataMock = { latestVersion: { __typename: 'Release', id: '3', - tagName: '1.0.0', - tagPath: 'path/to/release', + name: '1.0.0', + path: 'path/to/release', releasedAt: Date.now(), author: { id: 1, webUrl: 'profile/1', name: 'username' }, }, @@ -344,7 +344,7 @@ export const catalogAdditionalDetailsMock = { ], }, }, - tagName: 'v1.0.2', + name: 'v1.0.2', releasedAt: '2022-08-23T17:19:09Z', }, ], @@ -366,8 +366,8 @@ const generateResourcesNodes = (count = 20, startId = 0) => { latestVersion: { __typename: 'Release', id: '3', - tagName: '1.0.0', - tagPath: 'path/to/release', + name: '1.0.0', + path: 'path/to/release', releasedAt: Date.now(), author: { id: 1, webUrl: 'profile/1', name: 'username' }, }, diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js index dd534bec25d..e86759ec6ca 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js @@ -1,16 +1,21 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; -import { GlModal } from '@gitlab/ui'; +import { GlModal, GlCollapsibleListbox, GlDisclosureDropdown } from '@gitlab/ui'; +import setWindowLocation from 'helpers/set_window_location_helper'; +import { TEST_HOST } from 'helpers/test_constants'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import BranchRules from '~/projects/settings/repository/branch_rules/app.vue'; import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue'; import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'; +import createBranchRuleMutation from '~/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql'; + import { createAlert } from '~/alert'; import { branchRulesMockResponse, appProvideMock, + createBranchRuleMockResponse, } from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data'; import { I18N, @@ -31,16 +36,33 @@ Vue.use(VueApollo); describe('Branch rules app', () => { let wrapper; let fakeApollo; - + const openBranches = [ + { text: 'branch1', id: 'branch1', title: 'branch1' }, + { text: 'branch2', id: 'branch2', title: 'branch2' }, + ]; const branchRulesQuerySuccessHandler = jest.fn().mockResolvedValue(branchRulesMockResponse); + const addRuleMutationSuccessHandler = jest.fn().mockResolvedValue(createBranchRuleMockResponse); - const createComponent = async ({ queryHandler = branchRulesQuerySuccessHandler } = {}) => { - fakeApollo = createMockApollo([[branchRulesQuery, queryHandler]]); + const createComponent = async ({ + glFeatures = { addBranchRule: true }, + queryHandler = branchRulesQuerySuccessHandler, + mutationHandler = addRuleMutationSuccessHandler, + } = {}) => { + fakeApollo = createMockApollo([ + [branchRulesQuery, queryHandler], + [createBranchRuleMutation, mutationHandler], + ]); wrapper = mountExtended(BranchRules, { apolloProvider: fakeApollo, - provide: appProvideMock, - stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) }, + provide: { + ...appProvideMock, + glFeatures, + }, + stubs: { + GlDisclosureDropdown, + GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }), + }, directives: { GlModal: createMockDirective('gl-modal') }, }); @@ -51,19 +73,18 @@ describe('Branch rules app', () => { const findEmptyState = () => wrapper.findByTestId('empty'); const findAddBranchRuleButton = () => wrapper.findByRole('button', I18N.addBranchRule); const findModal = () => wrapper.findComponent(GlModal); + const findAddBranchRuleDropdown = () => wrapper.findComponent(GlDisclosureDropdown); + const findCreateBranchRuleListbox = () => wrapper.findComponent(GlCollapsibleListbox); + + beforeEach(() => { + window.gon = { + open_branches: openBranches, + }; + setWindowLocation(TEST_HOST); + }); beforeEach(() => createComponent()); - it('displays an error if branch rules query fails', async () => { - await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); - expect(createAlert).toHaveBeenCalledWith({ message: I18N.queryError }); - }); - - it('displays an empty state if no branch rules are present', async () => { - await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); - expect(findEmptyState().text()).toBe(I18N.emptyState); - }); - it('renders branch rules', () => { const { nodes } = branchRulesMockResponse.data.project.branchRules; @@ -78,7 +99,75 @@ describe('Branch rules app', () => { expect(findAllBranchRules().at(1).props('branchProtection')).toEqual(nodes[1].branchProtection); }); + it('displays an error if branch rules query fails', async () => { + await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); + expect(createAlert).toHaveBeenCalledWith({ message: I18N.queryError }); + }); + + it('displays an empty state if no branch rules are present', async () => { + await createComponent({ queryHandler: jest.fn().mockRejectedValue() }); + expect(findEmptyState().text()).toBe(I18N.emptyState); + }); + describe('Add branch rule', () => { + it('renders an Add branch rule dropdown', () => { + expect(findAddBranchRuleDropdown().props('toggleText')).toBe('Add branch rule'); + }); + + it('renders a modal with correct props/attributes', () => { + expect(findModal().props()).toMatchObject({ + title: I18N.createBranchRule, + modalId: BRANCH_PROTECTION_MODAL_ID, + actionCancel: { + text: 'Create branch rule', + }, + actionPrimary: { + attributes: { + disabled: true, + variant: 'confirm', + }, + text: 'Create protected branch', + }, + }); + }); + + it('renders listbox with branch names', () => { + expect(findCreateBranchRuleListbox().exists()).toBe(true); + expect(findCreateBranchRuleListbox().props('items')).toHaveLength(openBranches.length); + expect(findCreateBranchRuleListbox().props('toggleText')).toBe( + 'Select Branch or create wildcard', + ); + }); + + it('when the primary modal action is clicked it calls create rule mutation', async () => { + findCreateBranchRuleListbox().vm.$emit('select', openBranches[0].text); + await nextTick(); + findModal().vm.$emit('primary'); + await nextTick(); + await nextTick(); + expect(addRuleMutationSuccessHandler).toHaveBeenCalledWith({ + name: 'branch1', + projectPath: 'some/project/path', + }); + }); + + it('shows alert when mutation fails', async () => { + createComponent({ mutationHandler: jest.fn().mockRejectedValue() }); + findCreateBranchRuleListbox().vm.$emit('select', openBranches[0].text); + await nextTick(); + findModal().vm.$emit('primary'); + await waitForPromises(); + expect(createAlert).toHaveBeenCalledWith({ + message: 'Something went wrong while creating branch rule.', + }); + }); + }); + + describe('Add branch rule when addBranchRule FF disabled', () => { + beforeEach(() => { + window.gon.open_branches = openBranches; + createComponent({ glFeatures: { addBranchRule: false } }); + }); it('renders an Add branch rule button', () => { expect(findAddBranchRuleButton().exists()).toBe(true); }); diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js index d169397241d..5981647ce38 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js +++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js @@ -65,8 +65,22 @@ export const branchRulesMockResponse = { }, }; +export const createBranchRuleMockResponse = { + data: { + branchRuleCreate: { + errors: [], + branchRule: { + name: '*dkd', + __typename: 'BranchRule', + }, + __typename: 'BranchRuleCreatePayload', + }, + }, +}; + export const appProvideMock = { projectPath: 'some/project/path', + branchRulesPath: 'settings/repository/branch_rules', }; export const branchRuleProvideMock = { diff --git a/spec/lib/gitlab/application_setting_fetcher_spec.rb b/spec/lib/gitlab/application_setting_fetcher_spec.rb index 0cfb4eea2a3..0225a7608cb 100644 --- a/spec/lib/gitlab/application_setting_fetcher_spec.rb +++ b/spec/lib/gitlab/application_setting_fetcher_spec.rb @@ -44,23 +44,181 @@ RSpec.describe Gitlab::ApplicationSettingFetcher, feature_category: :cell do end context 'when ENV["IN_MEMORY_APPLICATION_SETTINGS"] is false' do + let_it_be(:settings) { create(:application_setting) } + context 'and an error is raised' do before do - allow(ApplicationSetting).to receive(:cached).and_raise(StandardError) + # The cached method is called twice: + # - ApplicationSettingFetcher + # - ApplicationSetting (CachedAttribute module) + # For this test, the first needs to raise an exception + # The second is swallowed on production so that should not raise an exception + # So we only let the first call raise an exception + # Alternatively, we could mock Rails.env.production? but I prefer not to + raise_exception = true + allow(ApplicationSetting).to receive(:cached).twice do + if raise_exception + raise_exception = false + raise(StandardError) + else + ApplicationSetting.last + end + end end - it 'returns nil' do - expect(current_application_settings).to be_nil + it 'will retrieve uncached ApplicationSetting' do + expect(ApplicationSetting).to receive(:current).and_call_original + + expect(current_application_settings).to eq(settings) end end context 'and settings in cache' do + before do + # Warm the cache + ApplicationSetting.current + end + it 'fetches the settings from cache' do expect(::ApplicationSetting).to receive(:cached).and_call_original - expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0) + expect(ActiveRecord::QueryRecorder.new { current_application_settings }.count).to eq(0) + end + end + + context 'and settings are not in cache' do + before do + allow(ApplicationSetting).to receive(:cached).and_return(nil) + end + + context 'and we are running a Rake task' do + before do + allow(Gitlab::Runtime).to receive(:rake?).and_return(true) + end + + context 'and database does not exist' do + before do + allow(::ApplicationSetting.database) + .to receive(:cached_table_exists?).and_raise(ActiveRecord::NoDatabaseError) + end + + it 'uses Gitlab::FakeApplicationSettings' do + expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end + end + + context 'and database connection is not active' do + before do + allow(::ApplicationSetting.connection).to receive(:active?).and_return(false) + end + + it 'uses Gitlab::FakeApplicationSettings' do + expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end + end + + context 'and table does not exist' do + before do + allow(::ApplicationSetting.database).to receive(:cached_table_exists?).and_return(false) + end + + it 'uses Gitlab::FakeApplicationSettings' do + expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end + end + + context 'and database connection raises some error' do + before do + allow(::ApplicationSetting.connection).to receive(:active?).and_raise(StandardError) + end + + it 'uses Gitlab::FakeApplicationSettings' do + expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end + end + + context 'and there are pending database migrations' do + before do + allow_next_instance_of(ActiveRecord::MigrationContext) do |migration_context| + allow(migration_context).to receive(:needs_migration?).and_return(true) + end + end + + it 'uses Gitlab::FakeApplicationSettings' do + expect(current_application_settings).to be_a(Gitlab::FakeApplicationSettings) + end + + context 'when a new setting is used but the migration did not run yet' do + let(:default_attributes) { { new_column: 'some_value' } } + + before do + allow(ApplicationSetting).to receive(:defaults).and_return(default_attributes) + end + + it 'uses the default value if present' do + expect(current_application_settings.new_column).to eq( + default_attributes[:new_column] + ) + end + end + end + end + + context 'and settings are in database' do + it 'returns settings from database' do + expect(current_application_settings).to eq(settings) + end + end + + context 'and settings are not in the database' do + before do + allow(ApplicationSetting).to receive(:current).and_return(nil) + end + + it 'returns default settings' do + expect(ApplicationSetting).to receive(:create_from_defaults).and_call_original + + expect(current_application_settings).to eq(settings) + end + end + + context 'when we hit a recursive loop' do + before do + allow(ApplicationSetting).to receive(:current).and_raise(ApplicationSetting::Recursion) + end + + it 'recovers and returns in-memory settings' do + settings = described_class.current_application_settings + + expect(settings).to be_a(ApplicationSetting) + expect(settings).not_to be_persisted + end end end end end + + describe '.expire_current_application_settings' do + subject(:expire) { described_class.expire_current_application_settings } + + it 'expires ApplicationSetting' do + expect(ApplicationSetting).to receive(:expire) + + expire + end + end + + describe '.current_application_settings?' do + subject(:settings?) { described_class.current_application_settings? } + + context 'when settings exist' do + let_it_be(:settings) { create(:application_setting) } + + it { is_expected.to be(true) } + end + + context 'when settings do not exist' do + it { is_expected.to be(false) } + end + end end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 9f666669c91..7fc50438c95 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -102,193 +102,39 @@ RSpec.describe Gitlab::CurrentSettings, feature_category: :shared do described_class.home_page_url end - - context 'in a Rake task with DB unavailable' do - before do - allow(Gitlab::Runtime).to receive(:rake?).and_return(true) - allow(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings).and_return(nil) - - # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues - # during the initialization phase of the test suite, so instead let's mock the internals of it - allow(ApplicationSetting.connection).to receive(:active?).and_return(false) - end - - context 'and no settings in cache' do - it 'returns a FakeApplicationSettings object' do - expect(described_class.current_application_settings).to be_a(Gitlab::FakeApplicationSettings) - end - - it 'does not issue any query' do - expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0) - end - end - end - - context 'with DB available' do - # This method returns the ::ApplicationSetting.defaults hash - # but with respect of custom attribute accessors of ApplicationSetting model - def settings_from_defaults - ar_wrapped_defaults = ::ApplicationSetting.build_from_defaults.attributes - ar_wrapped_defaults.slice(*::ApplicationSetting.defaults.keys) - end - - context 'and settings in cache' do - include_context 'with settings in cache' - - it 'fetches the settings from cache' do - # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues - # during the initialization phase of the test suite, so instead let's mock the internals of it - expect(ApplicationSetting.connection).not_to receive(:active?) - expect(ApplicationSetting.connection).not_to receive(:cached_table_exists?) - expect_any_instance_of(ActiveRecord::MigrationContext).not_to receive(:needs_migration?) - expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0) - end - end - - context 'and no settings in cache' do - before do - allow(ApplicationSetting.connection).to receive(:active?).and_return(true) - allow(ApplicationSetting.connection).to receive(:cached_table_exists?).with('application_settings').and_return(true) - end - - context 'with RequestStore enabled', :request_store do - it 'fetches the settings from DB only once' do - described_class.current_application_settings # warm the cache - - expect(ActiveRecord::QueryRecorder.new { described_class.current_application_settings }.count).to eq(0) - end - end - - it 'creates default ApplicationSettings if none are present' do - settings = described_class.current_application_settings - - expect(settings).to be_a(ApplicationSetting) - expect(settings).to be_persisted - expect(settings).to have_attributes(settings_from_defaults) - end - - context 'when we hit a recursive loop' do - before do - expect(ApplicationSetting).to receive(:create_from_defaults) do - raise ApplicationSetting::Recursion - end - end - - it 'recovers and returns in-memory settings' do - settings = described_class.current_application_settings - - expect(settings).to be_a(ApplicationSetting) - expect(settings).not_to be_persisted - end - end - - context 'when ApplicationSettings does not have a primary key' do - before do - allow(ApplicationSetting.connection).to receive(:primary_key).with('application_settings').and_return(nil) - end - - it 'raises an exception if ApplicationSettings does not have a primary key' do - expect { described_class.current_application_settings }.to raise_error(/table is missing a primary key constraint/) - end - end - - context 'with pending migrations' do - let(:current_settings) { described_class.current_application_settings } - - before do - allow(Gitlab::Runtime).to receive(:rake?).and_return(false) - end - - shared_examples 'a non-persisted ApplicationSetting object' do - it 'uses the default value from ApplicationSetting.defaults' do - expect(current_settings.signup_enabled).to eq(ApplicationSetting.defaults[:signup_enabled]) - end - - it 'uses the default value from custom ApplicationSetting accessors' do - expect(current_settings.commit_email_hostname).to eq(ApplicationSetting.default_commit_email_hostname) - end - - it 'responds to predicate methods' do - expect(current_settings.signup_enabled?).to eq(current_settings.signup_enabled) - end - end - - context 'in a Rake task' do - before do - allow(Gitlab::Runtime).to receive(:rake?).and_return(true) - expect_any_instance_of(ActiveRecord::MigrationContext).to receive(:needs_migration?).and_return(true) - end - - it_behaves_like 'a non-persisted ApplicationSetting object' - - it 'returns a FakeApplicationSettings object' do - expect(current_settings).to be_a(Gitlab::FakeApplicationSettings) - end - - context 'when a new column is used before being migrated' do - before do - allow(ApplicationSetting).to receive(:defaults).and_return({ foo: 'bar' }) - end - - it 'uses the default value if present' do - expect(current_settings.foo).to eq('bar') - end - end - end - - context 'with no ApplicationSetting DB record' do - it_behaves_like 'a non-persisted ApplicationSetting object' - end - - context 'with an existing ApplicationSetting DB record' do - before do - described_class.update!(home_page_url: 'http://mydomain.com') - end - - it_behaves_like 'a non-persisted ApplicationSetting object' - - it 'uses the value from the DB attribute if present and not overridden by an accessor' do - expect(current_settings.home_page_url).to eq('http://mydomain.com') - end - end - end - - context 'when ApplicationSettings.current is present' do - it 'returns the existing application settings' do - expect(ApplicationSetting).to receive(:current).and_return(:current_settings) - - expect(described_class.current_application_settings).to eq(:current_settings) - end - end - end - end end - describe '#current_application_settings?', :use_clean_rails_memory_store_caching do + describe '#current_application_settings?' do + subject(:settings_set) { described_class.current_application_settings? } + before do + # unstub, it is stubbed in spec/spec_helper.rb allow(described_class).to receive(:current_application_settings?).and_call_original - ApplicationSetting.delete_all # ensure no settings exist end - it 'returns true when settings exist' do - described_class.update!( - home_page_url: 'http://mydomain.com', - signup_enabled: false) + context 'when settings are cached in RequestStore' do + before do + allow(Gitlab::SafeRequestStore).to receive(:exist?).with(:current_application_settings).and_return(true) + end - expect(described_class.current_application_settings?).to eq(true) + it 'returns true' do + expect(settings_set).to be(true) + end end - it 'returns false when settings do not exist' do - expect(described_class.current_application_settings?).to eq(false) + context 'when ApplicationSettingFetcher.current_application_settings? returns true' do + before do + allow(Gitlab::ApplicationSettingFetcher).to receive(:current_application_settings?).and_return(true) + end + + it 'returns true' do + expect(settings_set).to be(true) + end end - context 'with cache', :request_store do - include_context 'with settings in cache' - - it 'returns an in-memory ApplicationSetting object' do - expect(ApplicationSetting).not_to receive(:current) - - expect(described_class.current_application_settings?).to eq(true) + context 'when not cached and not in ApplicationSettingFetcher' do + it 'returns false' do + expect(settings_set).to be(false) end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d5e301cb986..226e200fa25 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -420,7 +420,6 @@ RSpec.configure do |config| config.after do Fog.unmock! if Fog.mock? - Gitlab::CurrentSettings.clear_in_memory_application_settings! Gitlab::ApplicationSettingFetcher.clear_in_memory_application_settings! # Reset all feature flag stubs to default for testing diff --git a/spec/support/migration.rb b/spec/support/migration.rb index d9db7ae52fe..dadcbb1941e 100644 --- a/spec/support/migration.rb +++ b/spec/support/migration.rb @@ -17,7 +17,6 @@ RSpec.configure do |config| end config.after(:context, :migration) do - Gitlab::CurrentSettings.clear_in_memory_application_settings! Gitlab::ApplicationSettingFetcher.clear_in_memory_application_settings! end