diff options
72 files changed, 665 insertions, 650 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 8db781ddff2..e61eb0b5ca6 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -130,7 +130,8 @@ trigger-omnibus-env: echo "OMNIBUS_GITLAB_BUILD_ON_ALL_OS=${OMNIBUS_GITLAB_BUILD_ON_ALL_OS:-false}" >> $BUILD_ENV echo "GITLAB_ASSETS_TAG=$(assets_image_tag)" >> $BUILD_ENV echo "EE=$([[ $FOSS_ONLY == '1' ]] && echo 'false' || echo 'true')" >> $BUILD_ENV - echo "TRIGGER_BRANCH=$([[ "$CI_COMMIT_REF_NAME" =~ ^[0-9-]+-stable(-ee)?$ ]] && echo ${CI_COMMIT_REF_NAME%-ee} || echo 'master')" >> $BUILD_ENV + target_branch_name="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-${CI_COMMIT_REF_NAME}}" + echo "TRIGGER_BRANCH=$([[ "${target_branch_name}" =~ ^[0-9-]+-stable(-ee)?$ ]] && echo ${target_branch_name%-ee} || echo 'master')" >> $BUILD_ENV echo "Built environment file for omnibus build:" cat $BUILD_ENV artifacts: diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index a710a8ea871..a54bd87b1ae 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -506,16 +506,6 @@ Layout/ArgumentAlignment: - 'app/graphql/types/work_items/widgets/start_and_due_date_update_input_type.rb' - 'app/graphql/types/x509_certificate_type.rb' - 'app/graphql/types/x509_issuer_type.rb' - - 'app/models/bulk_imports/configuration.rb' - - 'app/models/bulk_imports/entity.rb' - - 'app/models/clusters/kubernetes_namespace.rb' - - 'app/models/container_repository.rb' - - 'app/models/cycle_analytics/project_level_stage_adapter.rb' - - 'app/models/deployment.rb' - - 'app/models/design_management/design.rb' - - 'app/models/design_management/version.rb' - - 'app/models/diff_discussion.rb' - - 'app/models/diff_viewer/base.rb' - 'app/models/discussion.rb' - 'app/models/environment.rb' - 'app/models/generic_commit_status.rb' diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js index 364f68cefb7..cc13a237aca 100644 --- a/app/assets/javascripts/environments/mount_show.js +++ b/app/assets/javascripts/environments/mount_show.js @@ -94,7 +94,7 @@ export const initPage = async () => { router, provide: { projectPath: dataSet.projectFullPath, - graphqlEtagKey: dataSet.graphqlEtagPath, + graphqlEtagKey: dataSet.graphqlEtagKey, }, render(createElement) { return createElement('router-view'); diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js index 198d9f980f0..6a64e8a2fa8 100644 --- a/app/assets/javascripts/graphql_shared/utils.js +++ b/app/assets/javascripts/graphql_shared/utils.js @@ -16,7 +16,10 @@ export const isGid = (id) => { return false; }; -const parseGid = (gid) => parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, ''), 10); +const parseGid = (gid) => { + const [type, id] = `${gid}`.replace(/gid:\/\/gitlab\//g, '').split('/'); + return { type, id }; +}; /** * Ids generated by GraphQL endpoints are usually in the format @@ -27,8 +30,24 @@ const parseGid = (gid) => parseInt(`${gid}`.replace(/gid:\/\/gitlab\/.*\//g, '') * @returns {Number} */ export const getIdFromGraphQLId = (gid = '') => { - const parsedGid = parseGid(gid); - return Number.isInteger(parsedGid) ? parsedGid : null; + const rawId = isGid(gid) ? parseGid(gid).id : gid; + const id = parseInt(rawId, 10); + return Number.isInteger(id) ? id : null; +}; + +/** + * Ids generated by GraphQL endpoints are usually in the format + * gid://gitlab/Environments/123. This method extracts Type string + * from the Id path + * + * @param {String} gid GraphQL global ID + * @returns {String} + */ +export const getTypeFromGraphQLId = (gid = '') => { + if (!isGid(gid)) return null; + + const { type } = parseGid(gid); + return type || null; }; export const MutationOperationMode = { diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue index 66b8db1f764..d57b3fda342 100644 --- a/app/assets/javascripts/security_configuration/components/app.vue +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -12,7 +12,6 @@ import FeatureCard from './feature_card.vue'; import TrainingProviderList from './training_provider_list.vue'; export const i18n = { - compliance: s__('SecurityConfiguration|Compliance'), configurationHistory: s__('SecurityConfiguration|Configuration history'), securityTesting: s__('SecurityConfiguration|Security testing'), latestPipelineDescription: s__( @@ -59,10 +58,6 @@ export default { type: Array, required: true, }, - augmentedComplianceFeatures: { - type: Array, - required: true, - }, gitlabCiPresent: { type: Boolean, required: false, @@ -101,9 +96,7 @@ export default { }, computed: { canUpgrade() { - return [...this.augmentedSecurityFeatures, ...this.augmentedComplianceFeatures].some( - ({ available }) => !available, - ); + return [...this.augmentedSecurityFeatures].some(({ available }) => !available); }, canViewCiHistory() { return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath); @@ -226,44 +219,6 @@ export default { </section-layout> </gl-tab> <gl-tab - data-testid="compliance-testing-tab" - :title="$options.i18n.compliance" - query-param-value="compliance-testing" - > - <section-layout :heading="$options.i18n.compliance"> - <template #description> - <p> - <span data-testid="latest-pipeline-info-compliance"> - <gl-sprintf - v-if="latestPipelinePath" - :message="$options.i18n.latestPipelineDescription" - > - <template #link="{ content }"> - <gl-link :href="latestPipelinePath">{{ content }}</gl-link> - </template> - </gl-sprintf> - </span> - - {{ $options.i18n.description }} - </p> - <p v-if="canViewCiHistory"> - <gl-link data-testid="compliance-view-history-link" :href="gitlabCiHistoryPath">{{ - $options.i18n.configurationHistory - }}</gl-link> - </p> - </template> - <template #features> - <feature-card - v-for="feature in augmentedComplianceFeatures" - :key="feature.type" - :feature="feature" - class="gl-mb-6" - @error="onError" - /> - </template> - </section-layout> - </gl-tab> - <gl-tab data-testid="vulnerability-management-tab" :title="$options.i18n.vulnerabilityManagement" query-param-value="vulnerability-management" diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index d46e9983a44..3bf0401ef5e 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -13,7 +13,6 @@ import { REPORT_TYPE_COVERAGE_FUZZING, REPORT_TYPE_CORPUS_MANAGEMENT, REPORT_TYPE_API_FUZZING, - REPORT_TYPE_LICENSE_COMPLIANCE, } from '~/vue_shared/security_reports/constants'; import kontraLogo from 'images/vulnerability/kontra-logo.svg'; @@ -150,14 +149,6 @@ export const API_FUZZING_NAME = __('API Fuzzing'); export const API_FUZZING_DESCRIPTION = __('Find bugs in your code with API fuzzing.'); export const API_FUZZING_HELP_PATH = helpPagePath('user/application_security/api_fuzzing/index'); -export const LICENSE_COMPLIANCE_NAME = __('License Compliance'); -export const LICENSE_COMPLIANCE_DESCRIPTION = __( - 'Search your project dependencies for their licenses and apply policies.', -); -export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath( - 'user/compliance/license_compliance/index', -); - export const CLUSTER_IMAGE_SCANNING_NAME = s__('ciReport|Cluster Image Scanning'); export const SCANNER_NAMES_MAP = { @@ -273,15 +264,6 @@ export const securityFeatures = [ }, ]; -export const complianceFeatures = [ - { - name: LICENSE_COMPLIANCE_NAME, - description: LICENSE_COMPLIANCE_DESCRIPTION, - helpPath: LICENSE_COMPLIANCE_HELP_PATH, - type: REPORT_TYPE_LICENSE_COMPLIANCE, - }, -]; - export const featureToMutationMap = { [REPORT_TYPE_SAST]: { mutationId: 'configureSast', diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js index 637d510e684..aa3c9c87622 100644 --- a/app/assets/javascripts/security_configuration/index.js +++ b/app/assets/javascripts/security_configuration/index.js @@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils'; import SecurityConfigurationApp from './components/app.vue'; -import { securityFeatures, complianceFeatures } from './components/constants'; +import { securityFeatures } from './components/constants'; import { augmentFeatures } from './utils'; export const initSecurityConfiguration = (el) => { @@ -28,9 +28,8 @@ export const initSecurityConfiguration = (el) => { vulnerabilityTrainingDocsPath, } = el.dataset; - const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures( + const { augmentedSecurityFeatures } = augmentFeatures( securityFeatures, - complianceFeatures, features ? JSON.parse(features) : [], ); @@ -48,7 +47,6 @@ export const initSecurityConfiguration = (el) => { render(createElement) { return createElement(SecurityConfigurationApp, { props: { - augmentedComplianceFeatures, augmentedSecurityFeatures, latestPipelinePath, gitlabCiHistoryPath, diff --git a/app/assets/javascripts/security_configuration/utils.js b/app/assets/javascripts/security_configuration/utils.js index df23698ba7e..72e6d870e13 100644 --- a/app/assets/javascripts/security_configuration/utils.js +++ b/app/assets/javascripts/security_configuration/utils.js @@ -2,19 +2,18 @@ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants'; /** - * This function takes in 3 arrays of objects, securityFeatures, complianceFeatures and features. - * securityFeatures and complianceFeatures are static arrays living in the constants. + * This function takes in 3 arrays of objects, securityFeatures and features. + * securityFeatures are static arrays living in the constants. * features is dynamic and coming from the backend. * This function builds a superset of those arrays. * It looks for matching keys within the dynamic and the static arrays * and will enrich the objects with the available static data. * @param [{}] securityFeatures - * @param [{}] complianceFeatures * @param [{}] features * @returns {Object} Object with enriched features from constants divided into Security and Compliance Features */ -export const augmentFeatures = (securityFeatures, complianceFeatures, features = []) => { +export const augmentFeatures = (securityFeatures, features = []) => { const featuresByType = features.reduce((acc, feature) => { acc[feature.type] = convertObjectPropsToCamelCase(feature, { deep: true }); return acc; @@ -39,7 +38,6 @@ export const augmentFeatures = (securityFeatures, complianceFeatures, features = return { augmentedSecurityFeatures: securityFeatures.map((feature) => augmentFeature(feature)), - augmentedComplianceFeatures: complianceFeatures.map((feature) => augmentFeature(feature)), }; }; diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js index 8b523645973..a1d75e08be9 100644 --- a/app/assets/javascripts/vue_shared/security_reports/constants.js +++ b/app/assets/javascripts/vue_shared/security_reports/constants.js @@ -27,7 +27,6 @@ export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning'; export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning'; export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing'; export const REPORT_TYPE_CORPUS_MANAGEMENT = 'corpus_management'; -export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_scanning'; export const REPORT_TYPE_API_FUZZING = 'api_fuzzing'; export const REPORT_TYPE_MANUALLY_ADDED = 'generic'; diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index ddf7c789c09..ef99001c0e8 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -46,7 +46,8 @@ import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscr import workItemMilestoneSubscription from '../graphql/work_item_milestone.subscription.graphql'; import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql'; import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql'; -import { findHierarchyWidgetChildren, getWorkItemQuery } from '../utils'; +import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql'; +import { findHierarchyWidgetChildren } from '../utils'; import WorkItemTree from './work_item_links/work_item_tree.vue'; import WorkItemActions from './work_item_actions.vue'; @@ -137,18 +138,15 @@ export default { }, apollo: { workItem: { - query() { - return getWorkItemQuery(this.fetchByIid); - }, + query: workItemByIidQuery, variables() { return this.queryVariables; }, skip() { - return !this.workItemId && !this.workItemIid; + return !this.workItemIid; }, update(data) { - const workItem = this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem; - return workItem ?? {}; + return data.workspace.workItems.nodes[0] ?? {}; }, error() { this.setEmptyState(); @@ -316,18 +314,11 @@ export default { workItemNotes() { return this.isWidgetPresent(WIDGET_TYPE_NOTES); }, - fetchByIid() { - return true; - }, queryVariables() { - return this.fetchByIid - ? { - fullPath: this.fullPath, - iid: this.workItemIid, - } - : { - id: this.workItemId, - }; + return { + fullPath: this.fullPath, + iid: this.workItemIid, + }; }, children() { return this.workItem ? findHierarchyWidgetChildren(this.workItem) : []; @@ -408,14 +399,12 @@ export default { }, toggleChildFromCache(workItem, childId, store) { const sourceData = store.readQuery({ - query: getWorkItemQuery(this.fetchByIid), + query: workItemByIidQuery, variables: this.queryVariables, }); const newData = produce(sourceData, (draftState) => { - const widgets = this.fetchByIid - ? draftState.workspace.workItems.nodes[0].widgets - : draftState.workItem.widgets; + const { widgets } = draftState.workspace.workItems.nodes[0]; const widgetHierarchy = widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY); const index = widgetHierarchy.children.nodes.findIndex((child) => child.id === childId); @@ -428,7 +417,7 @@ export default { }); store.writeQuery({ - query: getWorkItemQuery(this.fetchByIid), + query: workItemByIidQuery, variables: this.queryVariables, data: newData, }); @@ -475,12 +464,8 @@ export default { this.$emit('has-notes'); }, updateUrl(modalWorkItem) { - const params = this.fetchByIid - ? { work_item_iid: modalWorkItem?.iid } - : { work_item_id: getIdFromGraphQLId(modalWorkItem?.id) }; - updateHistory({ - url: setUrlParams(params), + url: setUrlParams({ work_item_iid: modalWorkItem?.iid }), replace: true, }); }, @@ -722,7 +707,6 @@ export default { :can-update="canUpdate" :project-path="fullPath" :confidential="workItem.confidential" - :fetch-by-iid="fetchByIid" @addWorkItemChild="addChild" @removeChild="removeChild" @show-modal="openInModal" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index 3e5d9453fef..4dcc4d51957 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -61,11 +61,6 @@ export default { type: String, required: true, }, - fetchByIid: { - type: Boolean, - required: false, - default: false, - }, }, data() { return { @@ -174,7 +169,7 @@ export default { :work-item-id="workItemId" :work-item-iid="workItemIid" :work-item-type="workItemType" - :fetch-by-iid="fetchByIid" + fetch-by-iid @removeChild="$emit('removeChild', $event)" @show-modal="showModal" /> diff --git a/app/assets/stylesheets/page_bundles/tree.scss b/app/assets/stylesheets/page_bundles/tree.scss index 9d13ccc676d..a13b8704095 100644 --- a/app/assets/stylesheets/page_bundles/tree.scss +++ b/app/assets/stylesheets/page_bundles/tree.scss @@ -219,10 +219,3 @@ width: calc(100% + 24px); margin: -28px -12px 0; } - -.ai-genie-chat-message { - pre, - code { - @include gl-font-sm; - } -} diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss index 619336686e8..3a18f735217 100644 --- a/app/assets/stylesheets/themes/dark_mode_overrides.scss +++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss @@ -305,3 +305,8 @@ body.gl-dark { // lightens chat bubble in darkmode as $gray-50 matches drawer background. See tanuki_bot_chat.scss background-color: $gray-100; } + +.ai-genie-chat, +.ai-genie-chat .gl-form-input { + background-color: $gray-10; +} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d967aa89eb7..dbcbd2467b6 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -51,7 +51,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:code_quality_inline_drawer, project) push_frontend_feature_flag(:hide_create_issue_resolve_all, project) push_frontend_feature_flag(:auto_merge_labels_mr_widget, project) - push_frontend_feature_flag(:summarize_my_code_review, current_user) + push_force_frontend_feature_flag(:summarize_my_code_review, summarize_my_code_review_enabled?) push_frontend_feature_flag(:mr_activity_filters, current_user) end @@ -603,6 +603,18 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo Date.strptime(date, "%Y-%m-%d")&.to_time&.to_i if date rescue Date::Error, TypeError end + + def summarize_my_code_review_enabled? + namespace = project&.group&.root_ancestor + return false if namespace.nil? + + Feature.enabled?(:summarize_my_code_review, current_user) && + namespace.group_namespace? && + namespace.licensed_feature_available?(:summarize_my_mr_code_review) && + namespace.experiment_features_enabled && + namespace.third_party_ai_features_enabled && + merge_request.send_to_ai? + end end Projects::MergeRequestsController.prepend_mod_with('Projects::MergeRequestsController') diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb index 8251e1cba8a..9ef347fff16 100644 --- a/app/helpers/sessions_helper.rb +++ b/app/helpers/sessions_helper.rb @@ -48,4 +48,8 @@ module SessionsHelper # Moved to Gitlab::Utils::Email in 15.9 Gitlab::Utils::Email.obfuscated_email(email) end + + def remember_me_enabled? + Gitlab::CurrentSettings.remember_me_enabled? + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index aba8727c030..8f57d73a6f4 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -38,10 +38,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord add_authentication_token_field :static_objects_external_storage_auth_token, encrypted: :required add_authentication_token_field :error_tracking_access_token, encrypted: :required - belongs_to :self_monitoring_project, class_name: "Project", foreign_key: :instance_administration_project_id, - inverse_of: :application_setting belongs_to :push_rule - alias_attribute :self_monitoring_project_id, :instance_administration_project_id belongs_to :instance_group, class_name: "Group", foreign_key: :instance_administrators_group_id, inverse_of: :application_setting diff --git a/app/models/bulk_imports/configuration.rb b/app/models/bulk_imports/configuration.rb index 3b263ed0340..6d9f598583e 100644 --- a/app/models/bulk_imports/configuration.rb +++ b/app/models/bulk_imports/configuration.rb @@ -9,7 +9,7 @@ class BulkImports::Configuration < ApplicationRecord validates :url, :access_token, length: { maximum: 255 }, presence: true validates :url, public_url: { schemes: %w[http https], enforce_sanitization: true, ascii_only: true }, - allow_nil: true + allow_nil: true attr_encrypted :url, key: Settings.attr_encrypted_db_key_base_32, diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index b3540917197..94e4a8165eb 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -41,22 +41,14 @@ class BulkImports::Entity < ApplicationRecord validates :project, absence: true, if: :group validates :group, absence: true, if: :project validates :source_type, presence: true - validates :source_full_path, - presence: true, - format: { with: Gitlab::Regex.bulk_import_source_full_path_regex, - message: Gitlab::Regex.bulk_import_source_full_path_regex_message } + validates :source_full_path, presence: true, format: { + with: Gitlab::Regex.bulk_import_source_full_path_regex, + message: Gitlab::Regex.bulk_import_source_full_path_regex_message + } - validates :destination_name, - presence: true, - if: -> { group || project } - - validates :destination_namespace, - exclusion: [nil], - if: :group - - validates :destination_namespace, - presence: true, - if: :project? + validates :destination_name, presence: true, if: -> { group || project } + validates :destination_namespace, exclusion: [nil], if: :group + validates :destination_namespace, presence: true, if: :project? validate :validate_parent_is_a_group, if: :parent validate :validate_imported_entity_type @@ -72,9 +64,8 @@ class BulkImports::Entity < ApplicationRecord alias_attribute :destination_slug, :destination_name - delegate :default_project_visibility, - :default_group_visibility, - to: :'Gitlab::CurrentSettings.current_application_settings' + delegate :default_project_visibility, :default_group_visibility, + to: :'Gitlab::CurrentSettings.current_application_settings' state_machine :status, initial: :created do state :created, value: 0 diff --git a/app/models/clusters/kubernetes_namespace.rb b/app/models/clusters/kubernetes_namespace.rb index 42332bdc193..dfb5c4cc5eb 100644 --- a/app/models/clusters/kubernetes_namespace.rb +++ b/app/models/clusters/kubernetes_namespace.rb @@ -22,9 +22,9 @@ module Clusters delegate :api_url, to: :platform_kubernetes, allow_nil: true attr_encrypted :service_account_token, - mode: :per_attribute_iv, - key: Settings.attr_encrypted_db_key_base_truncated, - algorithm: 'aes-256-cbc' + mode: :per_attribute_iv, + key: Settings.attr_encrypted_db_key_base_truncated, + algorithm: 'aes-256-cbc' scope :has_service_account_token, -> { where.not(encrypted_service_account_token: nil) } scope :with_environment_name, -> (name) { joins(:environment).where(environments: { name: name }) } diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 62b6effeb89..0f0abeae795 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -38,8 +38,8 @@ class ContainerRepository < ApplicationRecord validates :migration_aborted_in_state, inclusion: { in: ABORTABLE_MIGRATION_STATES }, allow_nil: true validates :migration_retries_count, presence: true, - numericality: { greater_than_or_equal_to: 0 }, - allow_nil: false + numericality: { greater_than_or_equal_to: 0 }, + allow_nil: false enum status: { delete_scheduled: 0, delete_failed: 1, delete_ongoing: 2 } enum expiration_policy_cleanup_status: { cleanup_unscheduled: 0, cleanup_scheduled: 1, cleanup_unfinished: 2, cleanup_ongoing: 3 } @@ -124,9 +124,7 @@ class ContainerRepository < ApplicationRecord state :import_done state :import_skipped do - validates :migration_skipped_reason, - :migration_skipped_at, - presence: true + validates :migration_skipped_reason, :migration_skipped_at, presence: true end state :import_aborted do @@ -603,8 +601,7 @@ class ContainerRepository < ApplicationRecord end def self.build_from_path(path) - self.new(project: path.repository_project, - name: path.repository_name) + self.new(project: path.repository_project, name: path.repository_name) end def self.find_or_create_from_path(path) @@ -622,13 +619,11 @@ class ContainerRepository < ApplicationRecord end def self.find_by_path!(path) - self.find_by!(project: path.repository_project, - name: path.repository_name) + self.find_by!(project: path.repository_project, name: path.repository_name) end def self.find_by_path(path) - self.find_by(project: path.repository_project, - name: path.repository_name) + self.find_by(project: path.repository_project, name: path.repository_name) end private diff --git a/app/models/cycle_analytics/project_level_stage_adapter.rb b/app/models/cycle_analytics/project_level_stage_adapter.rb index 9b9c0822f63..ae21a4a6bfe 100644 --- a/app/models/cycle_analytics/project_level_stage_adapter.rb +++ b/app/models/cycle_analytics/project_level_stage_adapter.rb @@ -16,12 +16,12 @@ module CycleAnalytics presenter = Analytics::CycleAnalytics::StagePresenter.new(stage) serializer.new.represent(ProjectLevelStage.new( - title: presenter.title, - description: presenter.description, - legend: presenter.legend, - name: stage.name, - project_median: median - )) + title: presenter.title, + description: presenter.description, + legend: presenter.legend, + name: stage.name, + project_median: median + )) end # rubocop: enable CodeReuse/Presenter diff --git a/app/models/deployment.rb b/app/models/deployment.rb index f8873d388a3..f3ee21ea4e0 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -372,9 +372,11 @@ class Deployment < ApplicationRecord # i.e.: # MergeRequest.select(1, 2).to_sql #=> SELECT 1, 2 FROM "merge_requests" # MergeRequest.select(1, 1).to_sql #=> SELECT 1 FROM "merge_requests" - select = relation.select('merge_requests.id', - "#{id} as deployment_id", - "#{environment_id} as environment_id").to_sql + select = relation.select( + 'merge_requests.id', + "#{id} as deployment_id", + "#{environment_id} as environment_id" + ).to_sql # We don't use `ApplicationRecord.legacy_bulk_insert` here so that we don't need to # first pluck lots of IDs into memory. diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb index cb6d4e72c80..505935bb230 100644 --- a/app/models/design_management/design.rb +++ b/app/models/design_management/design.rb @@ -31,8 +31,8 @@ module DesignManagement has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_internal_id :iid, scope: :project, presence: true, - hook_names: %i[create update], # Deal with old records - track_if: -> { !importing? } + hook_names: %i[create update], # Deal with old records + track_if: -> { !importing? } validates :project, :filename, presence: true validates :issue, presence: true, unless: :importing? diff --git a/app/models/design_management/version.rb b/app/models/design_management/version.rb index 5819404efb9..dd6812f0eac 100644 --- a/app/models/design_management/version.rb +++ b/app/models/design_management/version.rb @@ -36,10 +36,10 @@ module DesignManagement belongs_to :author, class_name: 'User' has_many :actions has_many :designs, - through: :actions, - class_name: "DesignManagement::Design", - source: :design, - inverse_of: :versions + through: :actions, + class_name: "DesignManagement::Design", + source: :design, + inverse_of: :versions validates :designs, presence: true, unless: :importing? validates :sha, presence: true diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb index 041ec98ffc9..e2ee951522d 100644 --- a/app/models/diff_discussion.rb +++ b/app/models/diff_discussion.rb @@ -10,13 +10,13 @@ class DiffDiscussion < Discussion DiffNote end - delegate :position, - :original_position, - :change_position, - :diff_note_positions, - :on_text?, - :on_image?, - to: :first_note + delegate :position, + :original_position, + :change_position, + :diff_note_positions, + :on_text?, + :on_image?, + to: :first_note def legacy_diff_discussion? false diff --git a/app/models/diff_viewer/base.rb b/app/models/diff_viewer/base.rb index 75aa51348c8..05552e83700 100644 --- a/app/models/diff_viewer/base.rb +++ b/app/models/diff_viewer/base.rb @@ -101,8 +101,9 @@ module DiffViewer def render_error_options options = [] - blob_url = Gitlab::Routing.url_helpers.project_blob_path(diff_file.repository.project, - File.join(diff_file.content_sha, diff_file.file_path)) + blob_url = Gitlab::Routing.url_helpers.project_blob_path( + diff_file.repository.project, File.join(diff_file.content_sha, diff_file.file_path) + ) options << ActionController::Base.helpers.link_to(_('view the blob'), blob_url) options diff --git a/app/models/integrations/prometheus.rb b/app/models/integrations/prometheus.rb index 2f0995e9ab0..b148539dec6 100644 --- a/app/models/integrations/prometheus.rb +++ b/app/models/integrations/prometheus.rb @@ -99,8 +99,7 @@ module Integrations end def allow_local_api_url? - allow_local_requests_from_web_hooks_and_services? || - (self_monitoring_project? && internal_prometheus_url?) + allow_local_requests_from_web_hooks_and_services? || internal_prometheus_url? end def configured? @@ -127,10 +126,6 @@ module Integrations delegate :allow_local_requests_from_web_hooks_and_services?, to: :current_settings, private: true - def self_monitoring_project? - project && project.id == current_settings.self_monitoring_project_id - end - def internal_prometheus_url? api_url.present? && api_url == ::Gitlab::Prometheus::Internal.uri end diff --git a/app/models/organization.rb b/app/models/organization.rb new file mode 100644 index 00000000000..73a7e84305f --- /dev/null +++ b/app/models/organization.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# rubocop: disable Gitlab/NamespacedClass +class Organization < ApplicationRecord +end +# rubocop: enable Gitlab/NamespacedClass diff --git a/app/models/project.rb b/app/models/project.rb index 8f9b7042a0e..16719316ede 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -173,7 +173,7 @@ class Project < ApplicationRecord has_one :last_event, -> { order 'events.created_at DESC' }, class_name: 'Event' has_many :boards - has_many :application_setting, inverse_of: :self_monitoring_project + has_many :application_setting def self.integration_association_name(name) "#{name}_integration" @@ -2857,10 +2857,6 @@ class Project < ApplicationRecord Feature.enabled?(:group_protected_branches, group) || Feature.enabled?(:allow_protected_branches_for_group, group) end - def self_monitoring? - Gitlab::CurrentSettings.self_monitoring_project_id == id - end - def deploy_token_create_url(opts = {}) Gitlab::Routing.url_helpers.create_deploy_token_project_settings_repository_path(self, opts) end diff --git a/app/models/user.rb b/app/models/user.rb index a5d8541c8ef..f0797e41c12 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1077,6 +1077,13 @@ class User < ApplicationRecord update(otp_backup_codes: nil) end + # Returns true if the user is allowed to sign in with either otp or recovery codes. + def sign_in_with_codes_allowed? + return two_factor_otp_enabled? unless Feature.enabled?(:webauthn_without_totp) + + two_factor_enabled? + end + def two_factor_enabled? two_factor_otp_enabled? || two_factor_webauthn_enabled? end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index dd84b890385..0f195663a61 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -15,18 +15,39 @@ module Members @skip_auth = skip_authorization if a_group_owner?(member) - process_destroy_of_group_owner_member(member, skip_subresources, unassign_issuables) + process_destroy_of_group_owner_member(member, skip_subresources) else destroy_member(member) - destroy_data_related_to_member(member, skip_subresources, unassign_issuables) + destroy_data_related_to_member(member, skip_subresources) end + enqueue_jobs_that_needs_to_be_run_only_once_per_hierarchy(member, unassign_issuables) + member end + # We use this to mark recursive calls made to this service from within the same service. + # We do this so as to help us run some tasks that needs to be run only once per hierarchy, and not recursively. + def mark_as_recursive_call + @recursive_call = true + end + private - def process_destroy_of_group_owner_member(member, skip_subresources, unassign_issuables) + # These actions need to be executed only once per hierarchy because the underlying services + # apply these actions to the entire hierarchy anyway, so there is no need to execute them recursively. + def enqueue_jobs_that_needs_to_be_run_only_once_per_hierarchy(member, unassign_issuables) + return if recursive_call? + + enqueue_delete_todos(member) + enqueue_unassign_issuables(member) if unassign_issuables + end + + def recursive_call? + @recursive_call == true + end + + def process_destroy_of_group_owner_member(member, skip_subresources) # Deleting 2 different group owners via the API in quick succession could lead to # wrong results for the `last_owner?` check due to race conditions. To prevent this # we wrap both the last_owner? check and the deletes of owners within a lock. @@ -40,23 +61,23 @@ module Members end # deletion of related data does not have to be within the lock. - destroy_data_related_to_member(member, skip_subresources, unassign_issuables) unless last_group_owner + destroy_data_related_to_member(member, skip_subresources) unless last_group_owner end def destroy_member(member) member.destroy end - def destroy_data_related_to_member(member, skip_subresources, unassign_issuables) + def destroy_data_related_to_member(member, skip_subresources) member.user&.invalidate_cache_counts - delete_member_associations(member, skip_subresources, unassign_issuables) + delete_member_associations(member, skip_subresources) end def a_group_owner?(member) member.is_a?(GroupMember) && member.owner? end - def delete_member_associations(member, skip_subresources, unassign_issuables) + def delete_member_associations(member, skip_subresources) if member.request? && member.user != current_user notification_service.decline_access_request(member) end @@ -64,8 +85,6 @@ module Members delete_subresources(member) unless skip_subresources delete_project_invitations_by(member) unless skip_subresources resolve_access_request_todos(current_user, member) - enqueue_delete_todos(member) - enqueue_unassign_issuables(member) if unassign_issuables after_execute(member: member) end @@ -110,13 +129,17 @@ module Members def destroy_project_members(members) members.each do |project_member| - self.class.new(current_user).execute(project_member, skip_authorization: @skip_auth) + service = self.class.new(current_user) + service.mark_as_recursive_call + service.execute(project_member, skip_authorization: @skip_auth) end end def destroy_group_members(members) members.each do |group_member| - self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true) + service = self.class.new(current_user) + service.mark_as_recursive_call + service.execute(group_member, skip_authorization: @skip_auth, skip_subresources: true) end end diff --git a/app/views/admin/sessions/_signin_box.html.haml b/app/views/admin/sessions/_signin_box.html.haml index c7382266480..2cd2eabd4b7 100644 --- a/app/views/admin/sessions/_signin_box.html.haml +++ b/app/views/admin/sessions/_signin_box.html.haml @@ -7,7 +7,7 @@ - ldap_servers.each_with_index do |server, i| .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i == 0 && form_based_auth_provider_has_active_class?(:ldapmain)) } .login-body - = render 'devise/sessions/new_ldap', server: server, hide_remember_me: true, submit_message: _('Enter Admin Mode') + = render 'devise/sessions/new_ldap', server: server, render_remember_me: false, submit_message: _('Enter Admin Mode') = render_if_exists 'devise/sessions/new_smartcard' diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml index e9442cf6b53..21b2c6df014 100644 --- a/app/views/admin/sessions/new.html.haml +++ b/app/views/admin/sessions/new.html.haml @@ -20,4 +20,4 @@ - if omniauth_enabled? && button_based_providers_enabled? .clearfix - = render 'devise/shared/omniauth_box', hide_remember_me: true + = render 'devise/shared/omniauth_box', render_remember_me: false diff --git a/app/views/admin/sessions/two_factor.html.haml b/app/views/admin/sessions/two_factor.html.haml index d02090d4880..33d73745138 100644 --- a/app/views/admin/sessions/two_factor.html.haml +++ b/app/views/admin/sessions/two_factor.html.haml @@ -9,7 +9,7 @@ .tab-content .login-box.tab-pane.gl-p-5.active{ id: 'login-pane', role: 'tabpanel' } .login-body - - if current_user.two_factor_otp_enabled? + - if current_user.sign_in_with_codes_allowed? = render 'admin/sessions/two_factor_otp' - if current_user.two_factor_webauthn_enabled? = render 'authentication/authenticate', render_remember_me: false, target_path: admin_session_path diff --git a/app/views/authentication/_authenticate.html.haml b/app/views/authentication/_authenticate.html.haml index 7d7bd395836..9e09bcd6e54 100644 --- a/app/views/authentication/_authenticate.html.haml +++ b/app/views/authentication/_authenticate.html.haml @@ -21,7 +21,7 @@ %div %p= _("We heard back from your device. You have been authenticated.") = form_tag(target_path, method: :post, id: 'js-login-token-2fa-form') do |f| - - if render_remember_me + - if remember_me_enabled? && render_remember_me - resource_params = params[resource_name].presence || params = hidden_field_tag 'user[remember_me]', resource_params.fetch(:remember_me, 0) = hidden_field_tag 'user[device_response]', nil, class: 'form-control', required: true, id: "js-device-response" diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 3aeb89979bb..1c40931c5bb 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -5,15 +5,15 @@ .form-group.gl-px-5 = f.label :password, class: "label-bold #{'gl-mb-1' if Feature.enabled?(:restyle_login_page, @project)}" = f.password_field :password, class: 'form-control gl-form-input bottom', autocomplete: 'current-password', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field', testid: 'password-field' } - - if devise_mapping.rememberable? - .gl-px-5 - .gl-display-inline-block + .gl-px-5 + .gl-display-inline-block + - if remember_me_enabled? = f.gitlab_ui_checkbox_component :remember_me, _('Remember me') - .gl-float-right - - if unconfirmed_email? - = link_to _('Resend confirmation email'), new_user_confirmation_path - - else - = link_to _('Forgot your password?'), new_password_path(:user) + .gl-float-right + - if unconfirmed_email? + = link_to _('Resend confirmation email'), new_user_confirmation_path + - else + = link_to _('Forgot your password?'), new_password_path(:user) %div - if Feature.enabled?(:arkose_labs_login_challenge) = render_if_exists 'devise/sessions/arkose_labs' diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index bdf357c5f74..14038f3c3c7 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -5,7 +5,7 @@ .form-group.gl-px-5 = label_tag :password = password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control bottom", title: _("This field is required."), required: true } - - if devise_mapping.rememberable? + - if remember_me_enabled? .remember-me.gl-px-5 %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 7affbafbdeb..8f9d6ee556e 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,5 +1,5 @@ - server = local_assigns.fetch(:server) -- hide_remember_me = local_assigns.fetch(:hide_remember_me, false) +- render_remember_me = remember_me_enabled? && local_assigns.fetch(:render_remember_me, true) - submit_message = local_assigns.fetch(:submit_message, _('Sign in')) = form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do @@ -9,7 +9,7 @@ .form-group.gl-px-5 = label_tag :password = password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control gl-form-input bottom", title: _("This field is required."), data: { qa_selector: 'password_field' }, required: true } - - if !hide_remember_me && devise_mapping.rememberable? + - if render_remember_me .gl-px-5 = render Pajamas::CheckboxTagComponent.new(name: 'remember_me') do |c| = c.label do diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index 12e5a7263f7..789f6072b68 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -2,10 +2,11 @@ = render 'devise/shared/tab_single', tab_title: _('Two-Factor Authentication') if Feature.disabled?(:restyle_login_page, @project) .login-box.gl-p-5 .login-body - - if @user.two_factor_otp_enabled? || (Feature.enabled?(:webauthn_without_totp) && @user.two_factor_enabled?) + - if @user.sign_in_with_codes_allowed? = gitlab_ui_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: "edit_user gl-show-field-errors js-2fa-form #{'hidden' if @user.two_factor_webauthn_enabled?}" }) do |f| - resource_params = params[resource_name].presence || params - = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) + - if remember_me_enabled? + = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) %div = f.label _('Enter verification code'), name: :otp_attempt, class: Feature.enabled?(:restyle_login_page, @project) ? 'gl-mb-1' : '' = f.text_field :otp_attempt, class: 'form-control gl-form-input', required: true, autofocus: true, autocomplete: 'off', inputmode: 'numeric', title: _('This field is required.'), data: { qa_selector: 'two_fa_code_field' } diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 150f61a97e0..f59bdc67d17 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,4 +1,4 @@ -- hide_remember_me = local_assigns.fetch(:hide_remember_me, false) +- render_remember_me = remember_me_enabled? && local_assigns.fetch(:render_remember_me, true) - restyle_login_page_enabled = Feature.enabled?(:restyle_login_page, @project) %div{ class: restyle_login_page_enabled ? 'omniauth-container gl-mt-5 gl-p-5 gl-text-center gl-w-90p gl-ml-auto gl-mr-auto' : 'omniauth-container gl-mt-5 gl-p-5' } %label{ class: restyle_login_page_enabled ? 'gl-font-weight-normal' : 'gl-font-weight-bold' } @@ -12,7 +12,7 @@ = provider_image_tag(provider) %span.gl-button-text = label_for_provider(provider) - - unless hide_remember_me + - if render_remember_me = render Pajamas::CheckboxTagComponent.new(name: 'remember_me_omniauth', value: nil) do |c| = c.label do = _('Remember me') diff --git a/app/views/projects/_self_monitoring_deprecation_notice.html.haml b/app/views/projects/_self_monitoring_deprecation_notice.html.haml deleted file mode 100644 index b9e32356688..00000000000 --- a/app/views/projects/_self_monitoring_deprecation_notice.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -- return unless project.self_monitoring? - -= content_for :page_level_alert do - .flash-container.flash-container-page.sticky - %div{ class: [container_class, 'limit-container-width', 'gl-pt-5!'] } - = render Pajamas::AlertComponent.new(title: _('Deprecation notice'), - variant: :danger, - alert_options: { class: 'gl-mb-3 gl-sticky' }) do |c| - = c.body do - - deprecation_link = '<a href="%{url}">'.html_safe % { url: help_page_path('update/deprecations', anchor: 'gitlab-self-monitoring-project') } - - removal_link = '<a href="%{url}">'.html_safe % { url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348909' } - - opstrace_link = '<a href="%{url}">'.html_safe % { url: 'https://gitlab.com/groups/gitlab-org/-/epics/6976' } - = _("Self-monitoring was %{deprecation}deprecated%{link_end} in GitLab 14.9, and is %{removal}scheduled for removal%{link_end} in GitLab 16.0. For information on a possible replacement, %{opstrace}learn more about Opstrace%{link_end}.").html_safe % { deprecation: deprecation_link, removal: removal_link, opstrace: opstrace_link, link_end: '</a>'.html_safe } diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index b6c21588193..a51d1080d96 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -7,7 +7,6 @@ = render "home_panel" = render "archived_notice", project: @project -= render "self_monitoring_deprecation_notice", project: @project = render "invite_members_empty_project" if can_admin_project_member?(@project) diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index ab2f6745dfd..e21bf7d318b 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -8,7 +8,6 @@ = render_if_exists 'shared/ultimate_feature_removal_banner', project: @project = render partial: 'flash_messages', locals: { project: @project } -= render "self_monitoring_deprecation_notice", project: @project = render 'clusters_deprecation_alert' diff --git a/db/docs/organizations.yml b/db/docs/organizations.yml new file mode 100644 index 00000000000..68278d0e6e4 --- /dev/null +++ b/db/docs/organizations.yml @@ -0,0 +1,10 @@ +--- +table_name: organizations +classes: +- Organization +feature_categories: +- cell +description: Define ownership of namespaces, projects, and users by organizations +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119421 +milestone: "16.0" +gitlab_schema: gitlab_main diff --git a/db/migrate/20230420012220_create_organizations.rb b/db/migrate/20230420012220_create_organizations.rb new file mode 100644 index 00000000000..ba6e89837d8 --- /dev/null +++ b/db/migrate/20230420012220_create_organizations.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class CreateOrganizations < Gitlab::Database::Migration[2.1] + def change + create_table :organizations do |t| + t.timestamps_with_timezone null: false + end + end +end diff --git a/db/schema_migrations/20230420012220 b/db/schema_migrations/20230420012220 new file mode 100644 index 00000000000..56a78b90585 --- /dev/null +++ b/db/schema_migrations/20230420012220 @@ -0,0 +1 @@ +c6897ef9e8c57b2b0dc8c94c0b2b9311996528b8f88bbf9b6a955de5d5c5120f
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5eb98c14623..2fd0a3e40b3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19253,6 +19253,21 @@ CREATE SEQUENCE operations_user_lists_id_seq ALTER SEQUENCE operations_user_lists_id_seq OWNED BY operations_user_lists.id; +CREATE TABLE organizations ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE organizations_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE organizations_id_seq OWNED BY organizations.id; + CREATE TABLE packages_build_infos ( id bigint NOT NULL, package_id integer NOT NULL, @@ -25413,6 +25428,8 @@ ALTER TABLE ONLY operations_strategies_user_lists ALTER COLUMN id SET DEFAULT ne ALTER TABLE ONLY operations_user_lists ALTER COLUMN id SET DEFAULT nextval('operations_user_lists_id_seq'::regclass); +ALTER TABLE ONLY organizations ALTER COLUMN id SET DEFAULT nextval('organizations_id_seq'::regclass); + ALTER TABLE ONLY p_ci_builds_metadata ALTER COLUMN id SET DEFAULT nextval('ci_builds_metadata_id_seq'::regclass); ALTER TABLE ONLY packages_build_infos ALTER COLUMN id SET DEFAULT nextval('packages_build_infos_id_seq'::regclass); @@ -27639,6 +27656,9 @@ ALTER TABLE ONLY operations_strategies_user_lists ALTER TABLE ONLY operations_user_lists ADD CONSTRAINT operations_user_lists_pkey PRIMARY KEY (id); +ALTER TABLE ONLY organizations + ADD CONSTRAINT organizations_pkey PRIMARY KEY (id); + ALTER TABLE ONLY p_ci_runner_machine_builds ADD CONSTRAINT p_ci_runner_machine_builds_pkey PRIMARY KEY (build_id, partition_id); diff --git a/doc/user/ai_features.md b/doc/user/ai_features.md index 8517ac9d75b..82e8b4cec36 100644 --- a/doc/user/ai_features.md +++ b/doc/user/ai_features.md @@ -7,7 +7,7 @@ type: index, reference # AI/ML powered features -GitLab is creating AI-assisted features across our DevSecOps platform. These features aim to help increase velocity and solve key painpoints across the software development lifecycle. +GitLab is creating AI-assisted features across our DevSecOps platform. These features aim to help increase velocity and solve key pain points across the software development lifecycle. ## Enable AI/ML features @@ -32,13 +32,13 @@ When a feature is [Generally Available](../policy/alpha-beta-support.md#generall ## Beta AI features -[Beta features](../policy/alpha-beta-support.md#beta) do not require the [group-level experiment features setting](group/manage.md#group-experiment-features-setting) to be enabled. +[Beta features](../policy/alpha-beta-support.md#beta) do not require the [group-level experiment features setting](group/manage.md#group-experiment-features-setting) to be enabled. - [Code Suggestions](project/repository/code_suggestions.md) -## Experiment AI features +## Experiment AI features -[Experiment features](../policy/alpha-beta-support.md#experiment) will soon require the [group-level Experiment features setting](group/manage.md#group-experiment-features-setting) to be enabled. +[Experiment features](../policy/alpha-beta-support.md#experiment) will soon require the [group-level Experiment features setting](group/manage.md#group-experiment-features-setting) to be enabled. ## Third-party AI features @@ -50,25 +50,32 @@ Third-party AI features require the [group-level third-party AI features setting This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. -If you spend a lot of time trying to understand pieces of code that others have created, or -are struggling to understand code written in a language that you are not familiar with, GitLab can help you get up to speed faster. By using a large language model, GitLab can explain the code in natural language. +GitLab can help you get up to speed faster if you: + +- Spend a lot of time trying to understand pieces of code that others have created, or +- Struggle to understand code written in a language that you are not familiar with. + +By using a large language model, GitLab can explain the code in natural language. Prerequisites: - The project must be a public project on GitLab.com. -- You must have the Ultimate subscription tier. +- You must have the GitLab Ultimate subscription tier. - You must be a member of the project. To explain your code: -1. Navigate to the file and select the lines that you want to have explained. -1. On the left side, select the question mark. You might have to scroll to the first line of your selection to view it. This sends the selected code together with a prompt to provide an explanation to the large language model referenced above. -1. A drawer is displayed. Wait a moment for the explanation to be generated. -1. Provide feedback about how satisfied you are with the explanation so we can improve the results. +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Merge requests**, then select your merge request. +1. On the secondary menu, select **Changes**. +1. Go to the file, and select the lines that you want to have explained. +1. On the left side, select the question mark (**{question}**). You might have to scroll to the first line of your selection to view it. This sends the selected code, together with a prompt, to provide an explanation to the large language model. +1. A drawer is displayed on the right side of the page. Wait a moment for the explanation to be generated. +1. Provide feedback about how satisfied you are with the explanation, so we can improve the results. ![How to use the Explain Code Experiment](img/explain_code_experiment.png) -Please be aware we cannot guarantee that the large language model will produce results that are correct. Use the explanation with caution. +We cannot guarantee that the large language model produces results that are correct. Use the explanation with caution. ### GitLab Chat **(ULTIMATE SAAS)** @@ -76,14 +83,74 @@ Please be aware we cannot guarantee that the large language model will produce r This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting) to be enabled. -Getting help has never been easier. If you have a question about how the GitLab product works, you can ask product how-to questions and get AI generated support from GitLab Chat. +Getting help has never been easier. If you have a question about how the GitLab product works, you can ask product how-to questions and get AI generated support from GitLab Chat. 1. In the lower-left corner, select the Help icon. 1. Select **Ask in GitLab Chat**. A drawer opens on the right side of your screen. -1. Enter your question in the chat input box and press **Enter** or select **Send**. It may take a few seconds for the interactive AI chat to search the product docs and produce an answer. +1. Enter your question in the chat input box and press **Enter** or select **Send**. It may take a few seconds for the interactive AI chat to search the product documentation and produce an answer. To give feedback, select the **Give Feedback** link. +### Summarize merge request changes **(ULTIMATE SAAS)** + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10400) in GitLab 16.0 as an [Experiment](../policy/alpha-beta-support.md#experiment). + +This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting) to be enabled. + +Merge request summaries can be generated by using the `/summarize_diff` quick action in a merge request comment. This posts a comment from a GitLab bot that provides a summary of the changes and the related SHA for when that summary was generated. + +Feedback on this experimental feature can be provided in [issue 408726](https://gitlab.com/gitlab-org/gitlab/-/issues/408726). + +#### Data usage + +This data is sent to the large language model referenced above when you use the `/summarize_diff` quick action: + +1. The diff of changes between the head of the source branch and the target branch + +### Summarize my merge request review **(ULTIMATE SAAS)** + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10466) in GitLab 16.0 as an [Experiment](../policy/alpha-beta-support.md#experiment). + +This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting) to be enabled. + +When you've completed your review of a merge request and are ready to [submit your review](project/merge_requests/reviews/index.md#submit-a-review) you can choose to have summary generated for you. To generate the summary: + +1. Select the AI Actions dropdown list. +1. Select **Summarize my code review**. + +The summary is generated and entered in to the comment box where you can edit and refine prior to submitting with your review. + +Feedback on this experimental feature can be provided in [issue 408991](https://gitlab.com/gitlab-org/gitlab/-/issues/408991). + +#### Data usage + +This data is sent to the large language model referenced above when you click on **Summarize my code review**: + +1. Draft comment's text +1. File path of the commented file(s) + +### Generate suggested tests in merge requests **(ULTIMATE SAAS)** + +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10366) in GitLab 16.0 as an [Experiment](../policy/alpha-beta-support.md#experiment). + +This feature is an [Experiment](../policy/alpha-beta-support.md) on GitLab.com that is powered by OpenAI's GPT-3. It requires the [group-level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting) to be enabled. + +When in a merge request you can choose to have GitLab suggest tests for the file you are reviewing. This can help to determine if appropriate test coverage has been provided or help with writing tests to provide more coverage for your project. To generate a test suggestion: + +1. Select the menu icon on the header of a file. +1. Select **Generate test with AI**. + +A sidebar opens where the test suggestion is generated. From there you can choose to copy that suggestion in to your editor as the start of your tests. + +Feedback on this experimental feature can be provided in [issue 408995](https://gitlab.com/gitlab-org/gitlab/-/issues/408995). + +#### Data usage + +This data is sent to the large language model referenced above when you click on **Generate test with AI**: + +1. Contents of the file +1. The file name + ## Data Usage GitLab AI features leverage generative AI to help increase velocity and aim to help make you more productive. Each feature operates independently of other features and is not required for other features to function. @@ -102,13 +169,13 @@ These features are in a variety of [feature support levels](../policy/alpha-beta ### Data privacy -Some AI features require the use of third-party AI services models and APIs from: Google AI and OpenAI. The processing of any personal data is in accordance with our [Privacy Statement](https://about.gitlab.com/privacy/). You may also visit the [Sub-Processors page](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors) to see the list of our Sub-Processors that we use in order to provide these features. +Some AI features require the use of third-party AI services models and APIs from: Google AI and OpenAI. The processing of any personal data is in accordance with our [Privacy Statement](https://about.gitlab.com/privacy/). You may also visit the [Sub-Processors page](https://about.gitlab.com/privacy/subprocessors/#third-party-sub-processors) to see the list of our Sub-Processors that we use in order to provide these features. Group owners can control which top-level groups have access to third-party AI features by using the [group level third-party AI features setting](group/manage.md#group-third-party-ai-features-setting). ### Model accuracy and quality -Generative AI may produce unexpected results that may be: +Generative AI may produce unexpected results that may be: - Low-quality - Incoherent diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake index 7d95f744d44..42a34a2d00b 100644 --- a/lib/tasks/gitlab/db.rake +++ b/lib/tasks/gitlab/db.rake @@ -340,11 +340,7 @@ namespace :gitlab do exit 1 end - # A list of projects that GitLab creates automatically on install/upgrade - # gc = Gitlab::CurrentSettings.current_application_settings - seed_projects = [Gitlab::CurrentSettings.current_application_settings.self_monitoring_project] - - if (Project.count - seed_projects.count { |x| !x.nil? }).eql?(0) + if Project.count.eql?(0) puts "No user created projects. Database not active" exit 1 end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 286f3aa4f7f..70d3ddf1f43 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -15146,9 +15146,6 @@ msgstr "" msgid "Deprecated API rate limits" msgstr "" -msgid "Deprecation notice" -msgstr "" - msgid "Deprecations|For information on a possible replacement %{epicStart} learn more about Opstrace %{epicEnd}." msgstr "" @@ -39766,9 +39763,6 @@ msgstr "" msgid "Search users or groups" msgstr "" -msgid "Search your project dependencies for their licenses and apply policies." -msgstr "" - msgid "Search your projects" msgstr "" @@ -40007,9 +40001,6 @@ msgstr "" msgid "SecurityConfiguration|By default, all analyzers are applied in order to cover all languages across your project, and only run if the language is detected in the merge request." msgstr "" -msgid "SecurityConfiguration|Compliance" -msgstr "" - msgid "SecurityConfiguration|Configuration guide" msgstr "" @@ -40076,7 +40067,7 @@ msgstr "" msgid "SecurityConfiguration|Manage profiles for use by DAST scans." msgstr "" -msgid "SecurityConfiguration|More scan types, including DAST, Dependency Scanning, Fuzzing, and Licence Compliance" +msgid "SecurityConfiguration|More scan types, including DAST, Dependency Scanning, Fuzzing" msgstr "" msgid "SecurityConfiguration|Not enabled" @@ -41198,9 +41189,6 @@ msgstr "" msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By %{link_open}@johnsmith%{link_close}\"). It will also associate and/or assign these issues and comments with the selected user." msgstr "" -msgid "Self-monitoring was %{deprecation}deprecated%{link_end} in GitLab 14.9, and is %{removal}scheduled for removal%{link_end} in GitLab 16.0. For information on a possible replacement, %{opstrace}learn more about Opstrace%{link_end}." -msgstr "" - msgid "Send" msgstr "" diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb new file mode 100644 index 00000000000..a6684a8f95f --- /dev/null +++ b/spec/factories/organizations.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :organization +end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index e6e5a1f9894..6f155959adb 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -394,6 +394,24 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ providers: [mock_saml_config_with_upstream_two_factor_authn_contexts]) end + it 'displays the remember me checkbox' do + visit new_user_session_path + + expect(page).to have_field('remember_me_omniauth') + end + + context 'when remember me is not enabled' do + before do + stub_application_setting(remember_me_enabled: false) + end + + it 'does not display the remember me checkbox' do + visit new_user_session_path + + expect(page).not_to have_field('remember_me_omniauth') + end + end + context 'when authn_context is worth two factors' do let(:mock_saml_response) do File.read('spec/fixtures/authentication/saml_response.xml') @@ -444,6 +462,24 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ end describe 'without two-factor authentication' do + it 'displays the remember me checkbox' do + visit new_user_session_path + + expect(page).to have_content(_('Remember me')) + end + + context 'when remember me is not enabled' do + before do + stub_application_setting(remember_me_enabled: false) + end + + it 'does not display the remember me checkbox' do + visit new_user_session_path + + expect(page).not_to have_content(_('Remember me')) + end + end + context 'with correct username and password' do let(:user) { create(:user) } @@ -750,17 +786,37 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ allow(instance).to receive(:"user_#{provider}_omniauth_callback_path") .and_return("/users/auth/#{provider}/callback") end - - visit new_user_session_path end it 'correctly renders tabs and panes' do + visit new_user_session_path + ensure_tab_pane_correctness(['Main LDAP', 'Standard']) end it 'renders link to sign up path' do + visit new_user_session_path + expect(page.body).to have_link('Register now', href: new_user_registration_path) end + + it 'displays the remember me checkbox' do + visit new_user_session_path + + ensure_remember_me_in_tab(ldap_server_config['label']) + end + + context 'when remember me is not enabled' do + before do + stub_application_setting(remember_me_enabled: false) + end + + it 'does not display the remember me checkbox' do + visit new_user_session_path + + ensure_remember_me_not_in_tab(ldap_server_config['label']) + end + end end context 'when crowd is enabled' do @@ -775,13 +831,31 @@ RSpec.describe 'Login', :clean_gitlab_redis_sessions, feature_category: :system_ allow(instance).to receive(:user_crowd_omniauth_authorize_path) .and_return("/users/auth/crowd/callback") end - - visit new_user_session_path end it 'correctly renders tabs and panes' do + visit new_user_session_path + ensure_tab_pane_correctness(%w(Crowd Standard)) end + + it 'displays the remember me checkbox' do + visit new_user_session_path + + ensure_remember_me_in_tab(_('Crowd')) + end + + context 'when remember me is not enabled' do + before do + stub_application_setting(remember_me_enabled: false) + end + + it 'does not display the remember me checkbox' do + visit new_user_session_path + + ensure_remember_me_not_in_tab(_('Crowd')) + end + end end end diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js index 35ae8de1b1f..f03856e5f75 100644 --- a/spec/frontend/graphql_shared/utils_spec.js +++ b/spec/frontend/graphql_shared/utils_spec.js @@ -3,6 +3,7 @@ import Visibility from 'visibilityjs'; import { isGid, getIdFromGraphQLId, + getTypeFromGraphQLId, convertToGraphQLId, convertToGraphQLIds, convertFromGraphQLIds, @@ -26,52 +27,30 @@ describe('isGid', () => { }); }); -describe('getIdFromGraphQLId', () => { - [ - { - input: '', - output: null, - }, - { - input: null, - output: null, - }, - { - input: 2, - output: 2, - }, - { - input: 'gid://', - output: null, - }, - { - input: 'gid://gitlab/', - output: null, - }, - { - input: 'gid://gitlab/Environments', - output: null, - }, - { - input: 'gid://gitlab/Environments/', - output: null, - }, - { - input: 'gid://gitlab/Environments/0', - output: 0, - }, - { - input: 'gid://gitlab/Environments/123', - output: 123, - }, - { - input: 'gid://gitlab/DesignManagement::Version/2', - output: 2, - }, - ].forEach(({ input, output }) => { - it(`getIdFromGraphQLId returns ${output} when passed ${input}`, () => { - expect(getIdFromGraphQLId(input)).toBe(output); - }); +describe.each` + input | id | type + ${''} | ${null} | ${null} + ${null} | ${null} | ${null} + ${0} | ${0} | ${null} + ${'0'} | ${0} | ${null} + ${2} | ${2} | ${null} + ${'2'} | ${2} | ${null} + ${'gid://'} | ${null} | ${null} + ${'gid://gitlab'} | ${null} | ${null} + ${'gid://gitlab/'} | ${null} | ${null} + ${'gid://gitlab/Environments'} | ${null} | ${'Environments'} + ${'gid://gitlab/Environments/'} | ${null} | ${'Environments'} + ${'gid://gitlab/Environments/0'} | ${0} | ${'Environments'} + ${'gid://gitlab/Environments/123'} | ${123} | ${'Environments'} + ${'gid://gitlab/Environments/123/test'} | ${123} | ${'Environments'} + ${'gid://gitlab/DesignManagement::Version/123'} | ${123} | ${'DesignManagement::Version'} +`('parses GraphQL ID `$input`', ({ input, id, type }) => { + it(`getIdFromGraphQLId returns ${id}`, () => { + expect(getIdFromGraphQLId(input)).toBe(id); + }); + + it(`getTypeFromGraphQLId returns ${type}`, () => { + expect(getTypeFromGraphQLId(input)).toBe(type); }); }); diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js index c6869573b2e..9a0cde15b24 100644 --- a/spec/frontend/issues/show/components/description_spec.js +++ b/spec/frontend/issues/show/components/description_spec.js @@ -8,7 +8,6 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createAlert } from '~/alert'; import Description from '~/issues/show/components/description.vue'; import eventHub from '~/issues/show/event_hub'; -import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql'; import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import TaskList from '~/task_list'; @@ -35,13 +34,6 @@ const $toast = { }; const issueDetailsResponse = getIssueDetailsResponse(); -const workItemQueryResponse = { - data: { - workItem: null, - }, -}; - -const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse); const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse); describe('Description component', () => { @@ -72,7 +64,6 @@ describe('Description component', () => { ...provide, }, apolloProvider: createMockApollo([ - [workItemQuery, queryHandler], [workItemTypesQuery, workItemTypesQueryHandler], [getIssueDetailsQuery, issueDetailsQueryHandler], [createWorkItemMutation, createWorkItemMutationHandler], diff --git a/spec/frontend/security_configuration/components/app_spec.js b/spec/frontend/security_configuration/components/app_spec.js index c4e6bef71eb..364fe733a41 100644 --- a/spec/frontend/security_configuration/components/app_spec.js +++ b/spec/frontend/security_configuration/components/app_spec.js @@ -11,7 +11,7 @@ import AutoDevopsEnabledAlert from '~/security_configuration/components/auto_dev import { AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY } from '~/security_configuration/components/constants'; import FeatureCard from '~/security_configuration/components/feature_card.vue'; import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue'; -import { complianceFeaturesMock, securityFeaturesMock, provideMock } from '../mock_data'; +import { securityFeaturesMock, provideMock } from '../mock_data'; const gitlabCiHistoryPath = 'test/historyPath'; const { vulnerabilityTrainingDocsPath, projectFullPath } = provideMock; @@ -29,7 +29,6 @@ describe('~/security_configuration/components/app', () => { wrapper = mountExtended(SecurityConfigurationApp, { propsData: { augmentedSecurityFeatures: securityFeaturesMock, - augmentedComplianceFeatures: complianceFeaturesMock, securityTrainingEnabled: true, ...propsData, }, @@ -72,12 +71,7 @@ describe('~/security_configuration/components/app', () => { text: i18n.configurationHistory, container: findByTestId('security-testing-tab'), }); - const findComplianceViewHistoryLink = () => - findLink({ - href: gitlabCiHistoryPath, - text: i18n.configurationHistory, - container: findByTestId('compliance-testing-tab'), - }); + const findAutoDevopsAlert = () => wrapper.findComponent(AutoDevopsAlert); const findAutoDevopsEnabledAlert = () => wrapper.findComponent(AutoDevopsEnabledAlert); const findVulnerabilityManagementTab = () => wrapper.findByTestId('vulnerability-management-tab'); @@ -94,7 +88,7 @@ describe('~/security_configuration/components/app', () => { }); describe('tabs', () => { - const expectedTabs = ['security-testing', 'compliance-testing', 'vulnerability-management']; + const expectedTabs = ['security-testing', 'vulnerability-management']; it('renders GlTab Component', () => { expect(findTab().exists()).toBe(true); @@ -123,9 +117,8 @@ describe('~/security_configuration/components/app', () => { it('renders right amount of feature cards for given props with correct props', () => { const cards = findFeatureCards(); - expect(cards).toHaveLength(2); + expect(cards).toHaveLength(1); expect(cards.at(0).props()).toEqual({ feature: securityFeaturesMock[0] }); - expect(cards.at(1).props()).toEqual({ feature: complianceFeaturesMock[0] }); }); it('renders a basic description', () => { @@ -137,7 +130,6 @@ describe('~/security_configuration/components/app', () => { }); it('should not show configuration History Link when gitlabCiPresent & gitlabCiHistoryPath are not defined', () => { - expect(findComplianceViewHistoryLink().exists()).toBe(false); expect(findSecurityViewHistoryLink().exists()).toBe(false); }); }); @@ -158,7 +150,7 @@ describe('~/security_configuration/components/app', () => { it('should show Alert with error Message', async () => { expect(findManageViaMRErrorAlert().exists()).toBe(false); - findFeatureCards().at(1).vm.$emit('error', errorMessage); + findFeatureCards().at(0).vm.$emit('error', errorMessage); await nextTick(); expect(findManageViaMRErrorAlert().exists()).toBe(true); @@ -166,7 +158,7 @@ describe('~/security_configuration/components/app', () => { }); it('should hide Alert when it is dismissed', async () => { - findFeatureCards().at(1).vm.$emit('error', errorMessage); + findFeatureCards().at(0).vm.$emit('error', errorMessage); await nextTick(); expect(findManageViaMRErrorAlert().exists()).toBe(true); @@ -257,7 +249,6 @@ describe('~/security_configuration/components/app', () => { createComponent({ augmentedSecurityFeatures: securityFeaturesMock, - augmentedComplianceFeatures: complianceFeaturesMock, autoDevopsEnabled: true, }); @@ -285,24 +276,6 @@ describe('~/security_configuration/components/app', () => { latestPipelinePath: 'test/path', }); }); - - it('should show latest pipeline info on the security tab with correct link when latestPipelinePath is defined', () => { - const latestPipelineInfoSecurity = findByTestId('latest-pipeline-info-security'); - - expect(latestPipelineInfoSecurity.text()).toMatchInterpolatedText( - i18n.latestPipelineDescription, - ); - expect(latestPipelineInfoSecurity.find('a').attributes('href')).toBe('test/path'); - }); - - it('should show latest pipeline info on the compliance tab with correct link when latestPipelinePath is defined', () => { - const latestPipelineInfoCompliance = findByTestId('latest-pipeline-info-compliance'); - - expect(latestPipelineInfoCompliance.text()).toMatchInterpolatedText( - i18n.latestPipelineDescription, - ); - expect(latestPipelineInfoCompliance.find('a').attributes('href')).toBe('test/path'); - }); }); describe('given gitlabCiPresent & gitlabCiHistoryPath props', () => { @@ -314,10 +287,8 @@ describe('~/security_configuration/components/app', () => { }); it('should show configuration History Link', () => { - expect(findComplianceViewHistoryLink().exists()).toBe(true); expect(findSecurityViewHistoryLink().exists()).toBe(true); - expect(findComplianceViewHistoryLink().attributes('href')).toBe('test/historyPath'); expect(findSecurityViewHistoryLink().attributes('href')).toBe('test/historyPath'); }); }); diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index 24ad8024b01..3d4f01d0da1 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -4,14 +4,8 @@ import { SAST_DESCRIPTION, SAST_HELP_PATH, SAST_CONFIG_HELP_PATH, - LICENSE_COMPLIANCE_NAME, - LICENSE_COMPLIANCE_DESCRIPTION, - LICENSE_COMPLIANCE_HELP_PATH, } from '~/security_configuration/components/constants'; -import { - REPORT_TYPE_LICENSE_COMPLIANCE, - REPORT_TYPE_SAST, -} from '~/vue_shared/security_reports/constants'; +import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants'; export const testProjectPath = 'foo/bar'; export const testProviderIds = [101, 102, 103]; @@ -128,16 +122,6 @@ export const securityFeaturesMock = [ }, ]; -export const complianceFeaturesMock = [ - { - name: LICENSE_COMPLIANCE_NAME, - description: LICENSE_COMPLIANCE_DESCRIPTION, - helpPath: LICENSE_COMPLIANCE_HELP_PATH, - type: REPORT_TYPE_LICENSE_COMPLIANCE, - configurationHelpPath: LICENSE_COMPLIANCE_HELP_PATH, - }, -]; - export const provideMock = { upgradePath: '/upgrade', autoDevopsHelpPagePath: '/autoDevopsHelpPagePath', diff --git a/spec/frontend/security_configuration/utils_spec.js b/spec/frontend/security_configuration/utils_spec.js index 241e69204d2..6e731e45da2 100644 --- a/spec/frontend/security_configuration/utils_spec.js +++ b/spec/frontend/security_configuration/utils_spec.js @@ -9,13 +9,6 @@ describe('augmentFeatures', () => { }, ]; - const mockComplianceFeatures = [ - { - name: 'LICENSE_COMPLIANCE', - type: 'LICENSE_COMPLIANCE', - }, - ]; - const mockFeaturesWithSecondary = [ { name: 'DAST', @@ -51,30 +44,25 @@ describe('augmentFeatures', () => { const expectedOutputDefault = { augmentedSecurityFeatures: mockSecurityFeatures, - augmentedComplianceFeatures: mockComplianceFeatures, }; const expectedOutputSecondary = { augmentedSecurityFeatures: mockSecurityFeatures, - augmentedComplianceFeatures: mockFeaturesWithSecondary, }; const expectedOutputCustomFeature = { augmentedSecurityFeatures: mockValidCustomFeature, - augmentedComplianceFeatures: mockComplianceFeatures, }; - describe('returns an object with augmentedSecurityFeatures and augmentedComplianceFeatures when', () => { + describe('returns an object with augmentedSecurityFeatures when', () => { it('given an empty array', () => { - expect(augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, [])).toEqual( - expectedOutputDefault, - ); + expect(augmentFeatures(mockSecurityFeatures, [])).toEqual(expectedOutputDefault); }); it('given an invalid populated array', () => { - expect( - augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockInvalidCustomFeature), - ).toEqual(expectedOutputDefault); + expect(augmentFeatures(mockSecurityFeatures, mockInvalidCustomFeature)).toEqual( + expectedOutputDefault, + ); }); it('features have secondary key', () => { @@ -84,21 +72,17 @@ describe('augmentFeatures', () => { }); it('given a valid populated array', () => { - expect( - augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockValidCustomFeature), - ).toEqual(expectedOutputCustomFeature); + expect(augmentFeatures(mockSecurityFeatures, mockValidCustomFeature)).toEqual( + expectedOutputCustomFeature, + ); }); }); describe('returns an object with camelcased keys', () => { it('given a customfeature in snakecase', () => { - expect( - augmentFeatures( - mockSecurityFeatures, - mockComplianceFeatures, - mockValidCustomFeatureSnakeCase, - ), - ).toEqual(expectedOutputCustomFeature); + expect(augmentFeatures(mockSecurityFeatures, mockValidCustomFeatureSnakeCase)).toEqual( + expectedOutputCustomFeature, + ); }); }); }); diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js index 6e8f6defb17..0045abe50d0 100644 --- a/spec/frontend/work_items/components/work_item_actions_spec.js +++ b/spec/frontend/work_items/components/work_item_actions_spec.js @@ -18,10 +18,10 @@ import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_wor import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql'; import convertWorkItemMutation from '~/work_items/graphql/work_item_convert.mutation.graphql'; import { - workItemResponseFactory, convertWorkItemMutationResponse, projectWorkItemTypesQueryResponse, convertWorkItemMutationErrorResponse, + workItemByIidResponseFactory, } from '../mock_data'; jest.mock('~/lib/utils/common_utils'); @@ -211,45 +211,45 @@ describe('WorkItemActions component', () => { describe('notifications action', () => { const errorMessage = 'Failed to subscribe'; + const id = 'gid://gitlab/WorkItem/1'; const notificationToggledOffMessage = 'Notifications turned off.'; const notificationToggledOnMessage = 'Notifications turned on.'; - const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true }); const inputVariablesOff = { - id: workItemQueryResponse.data.workItem.id, + id, notificationsWidget: { subscribed: false, }, }; const inputVariablesOn = { - id: workItemQueryResponse.data.workItem.id, + id, notificationsWidget: { subscribed: true, }, }; - const notificationsOffExpectedResponse = workItemResponseFactory({ + const notificationsOffExpectedResponse = workItemByIidResponseFactory({ subscribed: false, }); const toggleNotificationsOffHandler = jest.fn().mockResolvedValue({ data: { workItemUpdate: { - workItem: notificationsOffExpectedResponse.data.workItem, + workItem: notificationsOffExpectedResponse.data.workspace.workItems.nodes[0], errors: [], }, }, }); - const notificationsOnExpectedResponse = workItemResponseFactory({ + const notificationsOnExpectedResponse = workItemByIidResponseFactory({ subscribed: true, }); const toggleNotificationsOnHandler = jest.fn().mockResolvedValue({ data: { workItemUpdate: { - workItem: notificationsOnExpectedResponse.data.workItem, + workItem: notificationsOnExpectedResponse.data.workspace.workItems.nodes[0], errors: [], }, }, diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js index af97b3680f9..1e336a928a0 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -8,9 +8,7 @@ import { mockTracking } from 'helpers/tracking_helper'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql'; -import { config } from '~/graphql_shared/issuable_client'; import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue'; -import workItemQuery from '~/work_items/graphql/work_item.query.graphql'; import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue'; import { @@ -22,7 +20,6 @@ import { import { projectMembersResponseWithCurrentUser, mockAssignees, - workItemQueryResponse, currentUserResponse, currentUserNullResponse, projectMembersResponseWithoutCurrentUser, @@ -78,25 +75,11 @@ describe('WorkItemAssignees component', () => { canInviteMembers = false, canUpdate = true, } = {}) => { - const apolloProvider = createMockApollo( - [ - [userSearchQuery, searchQueryHandler], - [currentUserQuery, currentUserQueryHandler], - [updateWorkItemMutation, updateWorkItemMutationHandler], - ], - {}, - { - typePolicies: config.cacheConfig.typePolicies, - }, - ); - - apolloProvider.clients.defaultClient.writeQuery({ - query: workItemQuery, - variables: { - id: workItemId, - }, - data: workItemQueryResponse.data, - }); + const apolloProvider = createMockApollo([ + [userSearchQuery, searchQueryHandler], + [currentUserQuery, currentUserQueryHandler], + [updateWorkItemMutation, updateWorkItemMutationHandler], + ]); wrapper = mountExtended(WorkItemAssignees, { propsData: { diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js index 291dacfd844..630eb78d5c0 100644 --- a/spec/frontend/work_items/components/work_item_detail_spec.js +++ b/spec/frontend/work_items/components/work_item_detail_spec.js @@ -39,7 +39,7 @@ import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_ta import { mockParent, workItemDatesSubscriptionResponse, - workItemByIidResponseFactory as workItemResponseFactory, + workItemByIidResponseFactory, workItemTitleSubscriptionResponse, workItemAssigneesSubscriptionResponse, workItemMilestoneSubscriptionResponse, @@ -52,8 +52,8 @@ describe('WorkItemDetail component', () => { Vue.use(VueApollo); - const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true }); - const workItemQueryResponseWithoutParent = workItemResponseFactory({ + const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true }); + const workItemQueryResponseWithoutParent = workItemByIidResponseFactory({ parent: null, canUpdate: true, canDelete: true, @@ -221,7 +221,7 @@ describe('WorkItemDetail component', () => { describe('confidentiality', () => { const errorMessage = 'Mutation failed'; - const confidentialWorkItem = workItemResponseFactory({ + const confidentialWorkItem = workItemByIidResponseFactory({ confidential: true, }); const workItem = confidentialWorkItem.data.workspace.workItems.nodes[0]; @@ -398,7 +398,7 @@ describe('WorkItemDetail component', () => { describe('with parent', () => { beforeEach(() => { - const parentResponse = workItemResponseFactory(mockParent); + const parentResponse = workItemByIidResponseFactory(mockParent); createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) }); return waitForPromises(); @@ -437,7 +437,7 @@ describe('WorkItemDetail component', () => { }, }, }; - const parentResponse = workItemResponseFactory(mockParentObjective); + const parentResponse = workItemByIidResponseFactory(mockParentObjective); createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) }); await waitForPromises(); @@ -492,7 +492,7 @@ describe('WorkItemDetail component', () => { describe('when the assignees widget does not exist', () => { it('does not call the assignees subscription', async () => { - const response = workItemResponseFactory({ assigneesWidgetPresent: false }); + const response = workItemByIidResponseFactory({ assigneesWidgetPresent: false }); const handler = jest.fn().mockResolvedValue(response); createComponent({ handler }); await waitForPromises(); @@ -514,7 +514,7 @@ describe('WorkItemDetail component', () => { describe('when the due date widget does not exist', () => { it('does not call the dates subscription', async () => { - const response = workItemResponseFactory({ datesWidgetPresent: false }); + const response = workItemByIidResponseFactory({ datesWidgetPresent: false }); const handler = jest.fn().mockResolvedValue(response); createComponent({ handler }); await waitForPromises(); @@ -537,7 +537,7 @@ describe('WorkItemDetail component', () => { createComponent({ handler: jest .fn() - .mockResolvedValue(workItemResponseFactory({ assigneesWidgetPresent: false })), + .mockResolvedValue(workItemByIidResponseFactory({ assigneesWidgetPresent: false })), }); await waitForPromises(); @@ -551,7 +551,7 @@ describe('WorkItemDetail component', () => { ${'renders when widget is returned from API'} | ${true} | ${true} ${'does not render when widget is not returned from API'} | ${false} | ${false} `('$description', async ({ labelsWidgetPresent, exists }) => { - const response = workItemResponseFactory({ labelsWidgetPresent }); + const response = workItemByIidResponseFactory({ labelsWidgetPresent }); const handler = jest.fn().mockResolvedValue(response); createComponent({ handler }); await waitForPromises(); @@ -567,7 +567,7 @@ describe('WorkItemDetail component', () => { ${'when widget is not returned from API'} | ${false} | ${false} `('$description', ({ datesWidgetPresent, exists }) => { it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => { - const response = workItemResponseFactory({ datesWidgetPresent }); + const response = workItemByIidResponseFactory({ datesWidgetPresent }); const handler = jest.fn().mockResolvedValue(response); createComponent({ handler }); await waitForPromises(); @@ -594,7 +594,7 @@ describe('WorkItemDetail component', () => { ${'renders when widget is returned from API'} | ${true} | ${true} ${'does not render when widget is not returned from API'} | ${false} | ${false} `('$description', async ({ milestoneWidgetPresent, exists }) => { - const response = workItemResponseFactory({ milestoneWidgetPresent }); + const response = workItemByIidResponseFactory({ milestoneWidgetPresent }); const handler = jest.fn().mockResolvedValue(response); createComponent({ handler }); await waitForPromises(); @@ -614,7 +614,7 @@ describe('WorkItemDetail component', () => { describe('when the assignees widget does not exist', () => { it('does not call the milestone subscription', async () => { - const response = workItemResponseFactory({ milestoneWidgetPresent: false }); + const response = workItemByIidResponseFactory({ milestoneWidgetPresent: false }); const handler = jest.fn().mockResolvedValue(response); createComponent({ handler }); await waitForPromises(); @@ -632,6 +632,13 @@ describe('WorkItemDetail component', () => { expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' }); }); + it('skips the work item query when there is no workItemIid', async () => { + createComponent({ workItemIid: null }); + await waitForPromises(); + + expect(successHandler).not.toHaveBeenCalled(); + }); + it('calls the work item query when isModal=true', async () => { createComponent({ isModal: true }); await waitForPromises(); @@ -648,7 +655,7 @@ describe('WorkItemDetail component', () => { }); describe('work item has children', () => { - const objectiveWorkItem = workItemResponseFactory({ + const objectiveWorkItem = workItemByIidResponseFactory({ workItemType: objectiveType, confidential: true, }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 2ab31908577..1c319844af3 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -684,87 +684,6 @@ export const createWorkItemMutationErrorResponse = { }, }; -export const createWorkItemFromTaskMutationResponse = { - data: { - workItemCreateFromTask: { - __typename: 'WorkItemCreateFromTaskPayload', - errors: [], - workItem: { - __typename: 'WorkItem', - description: 'New description', - id: 'gid://gitlab/WorkItem/1', - iid: '1', - title: 'Updated title', - state: 'OPEN', - confidential: false, - createdAt: '2022-08-03T12:41:54Z', - closedAt: null, - project: { - __typename: 'Project', - id: '1', - fullPath: 'test-project-path', - archived: false, - }, - workItemType: { - __typename: 'WorkItemType', - id: 'gid://gitlab/WorkItems::Type/5', - name: 'Task', - iconName: 'issue-type-task', - }, - userPermissions: { - deleteWorkItem: false, - updateWorkItem: false, - setWorkItemMetadata: false, - __typename: 'WorkItemPermissions', - }, - widgets: [ - { - __typename: 'WorkItemWidgetDescription', - type: 'DESCRIPTION', - description: 'New description', - descriptionHtml: '<p>New description</p>', - lastEditedAt: '2022-09-21T06:18:42Z', - lastEditedBy: { - name: 'Administrator', - webPath: '/root', - }, - }, - ], - }, - newWorkItem: { - __typename: 'WorkItem', - id: 'gid://gitlab/WorkItem/1000000', - iid: '100', - title: 'Updated title', - state: 'OPEN', - createdAt: '2022-08-03T12:41:54Z', - closedAt: null, - description: '', - confidential: false, - project: { - __typename: 'Project', - id: '1', - fullPath: 'test-project-path', - archived: false, - }, - workItemType: { - __typename: 'WorkItemType', - id: 'gid://gitlab/WorkItems::Type/5', - name: 'Task', - iconName: 'issue-type-task', - }, - userPermissions: { - deleteWorkItem: false, - updateWorkItem: false, - setWorkItemMetadata: false, - __typename: 'WorkItemPermissions', - }, - widgets: [], - }, - }, - }, -}; - export const deleteWorkItemResponse = { data: { workItemDelete: { errors: [], __typename: 'WorkItemDeletePayload' } }, }; @@ -1831,18 +1750,6 @@ export const projectMilestonesResponseWithNoMilestones = { }, }; -export const projectWorkItemResponse = { - data: { - workspace: { - id: 'gid://gitlab/Project/1', - workItems: { - nodes: [workItemQueryResponse.data.workItem], - }, - __typename: 'Project', - }, - }, -}; - export const mockWorkItemNotesResponse = { data: { workItem: { diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js index 86e890ea809..b5d54a7c319 100644 --- a/spec/frontend/work_items/router_spec.js +++ b/spec/frontend/work_items/router_spec.js @@ -6,7 +6,7 @@ import { currentUserResponse, workItemAssigneesSubscriptionResponse, workItemDatesSubscriptionResponse, - workItemByIidResponseFactory as workItemResponseFactory, + workItemByIidResponseFactory, workItemTitleSubscriptionResponse, workItemLabelsSubscriptionResponse, workItemMilestoneSubscriptionResponse, @@ -32,7 +32,7 @@ describe('Work items router', () => { Vue.use(VueApollo); - const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory()); + const workItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory()); const currentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse); const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse); const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse); diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb index b8290fa2337..5a46a20ce1a 100644 --- a/spec/helpers/sessions_helper_spec.rb +++ b/spec/helpers/sessions_helper_spec.rb @@ -87,4 +87,24 @@ RSpec.describe SessionsHelper do expect(subject).to eq('ma**@e******.com') end end + + describe '#remember_me_enabled?' do + subject { helper.remember_me_enabled? } + + context 'when application setting is enabled' do + before do + stub_application_setting(remember_me_enabled: true) + end + + it { is_expected.to be true } + end + + context 'when application setting is disabled' do + before do + stub_application_setting(remember_me_enabled: false) + end + + it { is_expected.to be false } + end + end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index a35c9c5e526..e88c89c74dc 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -639,8 +639,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic create(:alert_management_alert, project: project, created_at: n.days.ago) end - stub_application_setting(self_monitoring_project: project) - for_defined_days_back do create(:product_analytics_event, project: project, se_category: 'epics', se_action: 'promote') end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 9f292b0a294..e0bfe41a3ae 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -27,12 +27,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do describe 'associations' do it do - is_expected.to belong_to(:self_monitoring_project).class_name('Project') - .with_foreign_key(:instance_administration_project_id) - .inverse_of(:application_setting) - end - - it do is_expected.to belong_to(:instance_group).class_name('Group') .with_foreign_key(:instance_administrators_group_id) .inverse_of(:application_setting) diff --git a/spec/models/integrations/prometheus_spec.rb b/spec/models/integrations/prometheus_spec.rb index aa248abd3bb..a533079f906 100644 --- a/spec/models/integrations/prometheus_spec.rb +++ b/spec/models/integrations/prometheus_spec.rb @@ -90,37 +90,6 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching, end end end - - context 'with self-monitoring project and internal Prometheus' do - before do - integration.api_url = 'http://localhost:9090' - - stub_application_setting(self_monitoring_project_id: project.id) - stub_config(prometheus: { enable: true, server_address: 'localhost:9090' }) - end - - it 'allows self-monitoring project to connect to internal Prometheus' do - aggregate_failures do - ['127.0.0.1', '192.168.2.3'].each do |url| - allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)]) - - expect(integration.can_query?).to be true - end - end - end - - it 'does not allow self-monitoring project to connect to other local URLs' do - integration.api_url = 'http://localhost:8000' - - aggregate_failures do - ['127.0.0.1', '192.168.2.3'].each do |url| - allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)]) - - expect(integration.can_query?).to be false - end - end - end - end end end @@ -218,23 +187,6 @@ RSpec.describe Integrations::Prometheus, :use_clean_rails_memory_store_caching, it 'blocks local requests' do expect(integration.prometheus_client).to be_nil end - - context 'with self-monitoring project and internal Prometheus URL' do - before do - stub_application_setting(allow_local_requests_from_web_hooks_and_services: false) - stub_application_setting(self_monitoring_project_id: project.id) - - stub_config(prometheus: { - enable: true, - server_address: api_url - }) - end - - it 'allows local requests' do - expect(integration.prometheus_client).not_to be_nil - expect { integration.prometheus_client.ping }.not_to raise_error - end - end end context 'behind IAP' do diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb new file mode 100644 index 00000000000..9966a7132ce --- /dev/null +++ b/spec/models/organization_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# rubocop: disable Lint/EmptyBlock +# rubocop: disable RSpec/EmptyExampleGroup +RSpec.describe Organization, type: :model, feature_category: :cell do +end +# rubocop: enable RSpec/EmptyExampleGroup +# rubocop: enable Lint/EmptyBlock diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index f25ff0884b9..b5b7a283e39 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -7551,24 +7551,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do end end - describe '#self_monitoring?' do - let_it_be(:project) { create(:project) } - - subject { project.self_monitoring? } - - context 'when the project is instance self-monitoring' do - before do - stub_application_setting(self_monitoring_project_id: project.id) - end - - it { is_expected.to be true } - end - - context 'when the project is not self-monitoring' do - it { is_expected.to be false } - end - end - describe '#add_export_job' do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 60583bc351d..f1d3b17fd6f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2174,6 +2174,20 @@ RSpec.describe User, feature_category: :user_profile do end end + describe '#sign_in_with_codes_allowed?' do + let_it_be(:user) { create(:user, :two_factor_via_webauthn) } + + context 'when `webauthn_without_totp` disabled' do + before do + stub_feature_flags(webauthn_without_totp: false) + end + + it { expect(user.sign_in_with_codes_allowed?).to eq(false) } + end + + it { expect(user.sign_in_with_codes_allowed?).to eq(true) } + end + describe '#two_factor_otp_enabled?' do let_it_be(:user) { create(:user) } diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 2b956bec469..48f59ba596b 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -463,16 +463,26 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do end context 'subresources' do - let(:user) { create(:user) } - let(:member_user) { create(:user) } + let_it_be_with_reload(:user) { create(:user) } + let_it_be_with_reload(:member_user) { create(:user) } + + let_it_be_with_reload(:group) { create(:group, :public) } + let_it_be_with_reload(:subgroup) { create(:group, parent: group) } + let_it_be(:private_subgroup) { create(:group, :private, parent: group, name: 'private_subgroup') } + let_it_be(:private_subgroup_with_direct_membership) { create(:group, :private, parent: group) } + let_it_be_with_reload(:subsubgroup) { create(:group, parent: subgroup) } + + let_it_be_with_reload(:group_project) { create(:project, :public, group: group) } + let_it_be_with_reload(:control_project) { create(:project, :private, group: subsubgroup) } + let_it_be_with_reload(:subsubproject) { create(:project, :public, group: subsubgroup) } - let(:group) { create(:group, :public) } - let(:subgroup) { create(:group, parent: group) } - let(:subsubgroup) { create(:group, parent: subgroup) } - let(:subsubproject) { create(:project, group: subsubgroup) } + let_it_be(:private_subgroup_project) do + create(:project, :private, group: private_subgroup, name: 'private_subgroup_project') + end - let(:group_project) { create(:project, :public, group: group) } - let(:control_project) { create(:project, group: subsubgroup) } + let_it_be(:private_subgroup_with_direct_membership_project) do + create(:project, :private, group: private_subgroup_with_direct_membership, name: 'private_subgroup_project') + end context 'with memberships' do before do @@ -481,14 +491,68 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do subsubproject.add_developer(member_user) group_project.add_developer(member_user) control_project.add_maintainer(user) + private_subgroup_with_direct_membership.add_developer(member_user) group.add_owner(user) @group_member = create(:group_member, :developer, group: group, user: member_user) end + let_it_be(:todo_in_public_group_project) do + create(:todo, :pending, + project: group_project, + user: member_user, + target: create(:issue, project: group_project) + ) + end + + let_it_be(:mr_in_public_group_project) do + create(:merge_request, source_project: group_project, assignees: [member_user]) + end + + let_it_be(:todo_in_private_subgroup_project) do + create(:todo, :pending, + project: private_subgroup_project, + user: member_user, + target: create(:issue, project: private_subgroup_project) + ) + end + + let_it_be(:mr_in_private_subgroup_project) do + create(:merge_request, source_project: private_subgroup_project, assignees: [member_user]) + end + + let_it_be(:todo_in_public_subsubgroup_project) do + create(:todo, :pending, + project: subsubproject, + user: member_user, + target: create(:issue, project: subsubproject) + ) + end + + let_it_be(:mr_in_public_subsubgroup_project) do + create(:merge_request, source_project: subsubproject, assignees: [member_user]) + end + + let_it_be(:todo_in_private_subgroup_with_direct_membership_project) do + create(:todo, :pending, + project: private_subgroup_with_direct_membership_project, + user: member_user, + target: create(:issue, project: private_subgroup_with_direct_membership_project) + ) + end + + let_it_be(:mr_in_private_subgroup_with_direct_membership_project) do + create(:merge_request, + source_project: private_subgroup_with_direct_membership_project, + assignees: [member_user] + ) + end + context 'with skipping of subresources' do + subject(:execute_service) { described_class.new(user).execute(@group_member, skip_subresources: true) } + before do - described_class.new(user).execute(@group_member, skip_subresources: true) + execute_service end it 'removes the group membership' do @@ -514,11 +578,35 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do it 'does not remove the user from the control project' do expect(control_project.members.map(&:user)).to include(user) end + + context 'todos', :sidekiq_inline do + it 'removes todos for which the user no longer has access' do + expect(member_user.todos).to include( + todo_in_public_group_project, + todo_in_public_subsubgroup_project, + todo_in_private_subgroup_with_direct_membership_project + ) + + expect(member_user.todos).not_to include(todo_in_private_subgroup_project) + end + end + + context 'issuables', :sidekiq_inline do + subject(:execute_service) do + described_class.new(user).execute(@group_member, skip_subresources: true, unassign_issuables: true) + end + + it 'removes assigned issuables, even in subresources' do + expect(member_user.assigned_merge_requests).to be_empty + end + end end context 'without skipping of subresources' do + subject(:execute_service) { described_class.new(user).execute(@group_member, skip_subresources: false) } + before do - described_class.new(user).execute(@group_member, skip_subresources: false) + execute_service end it 'removes the project membership' do @@ -544,6 +632,30 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do it 'does not remove the user from the control project' do expect(control_project.members.map(&:user)).to include(user) end + + context 'todos', :sidekiq_inline do + it 'removes todos for which the user no longer has access' do + expect(member_user.todos).to include( + todo_in_public_group_project, + todo_in_public_subsubgroup_project + ) + + expect(member_user.todos).not_to include( + todo_in_private_subgroup_project, + todo_in_private_subgroup_with_direct_membership_project + ) + end + end + + context 'issuables', :sidekiq_inline do + subject(:execute_service) do + described_class.new(user).execute(@group_member, skip_subresources: false, unassign_issuables: true) + end + + it 'removes assigned issuables' do + expect(member_user.assigned_merge_requests).to be_empty + end + end end end @@ -626,4 +738,13 @@ RSpec.describe Members::DestroyService, feature_category: :subgroups do expect(project.members.not_accepted_invitations_by_user(member_user)).to be_empty end end + + describe '#mark_as_recursive_call' do + it 'marks the instance as recursive' do + service = described_class.new(current_user) + service.mark_as_recursive_call + + expect(service.send(:recursive_call?)).to eq(true) + end + end end diff --git a/spec/support/helpers/user_login_helper.rb b/spec/support/helpers/user_login_helper.rb index 47e858cb68c..d8368a94ad7 100644 --- a/spec/support/helpers/user_login_helper.rb +++ b/spec/support/helpers/user_login_helper.rb @@ -30,4 +30,20 @@ module UserLoginHelper def ensure_one_active_pane expect(page).to have_selector('.tab-pane.active', count: 1) end + + def ensure_remember_me_in_tab(tab_name) + find_link(tab_name).click + + within '.tab-pane.active' do + expect(page).to have_content _('Remember me') + end + end + + def ensure_remember_me_not_in_tab(tab_name) + find_link(tab_name).click + + within '.tab-pane.active' do + expect(page).not_to have_content _('Remember me') + end + end end diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb index fb45bc0864d..c2cc6d05630 100644 --- a/spec/tasks/gitlab/db_rake_spec.rb +++ b/spec/tasks/gitlab/db_rake_spec.rb @@ -961,19 +961,17 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_categor using RSpec::Parameterized::TableSyntax let(:task) { 'gitlab:db:active' } - let(:self_monitoring) { double('self_monitoring') } - where(:needs_migration, :self_monitoring_project, :project_count, :exit_status, :exit_code) do - true | nil | nil | 1 | false - false | :self_monitoring | 1 | 1 | false - false | nil | 0 | 1 | false - false | :self_monitoring | 2 | 0 | true + where(:needs_migration, :project_count, :exit_status, :exit_code) do + true | nil | 1 | false + false | 1 | 0 | true + false | 0 | 1 | false + false | 2 | 0 | true end with_them do it 'exits 0 or 1 depending on user modifications to the database' do allow_any_instance_of(ActiveRecord::MigrationContext).to receive(:needs_migration?).and_return(needs_migration) - allow_any_instance_of(ApplicationSetting).to receive(:self_monitoring_project).and_return(self_monitoring_project) allow(Project).to receive(:count).and_return(project_count) expect { run_rake_task(task) }.to raise_error do |error| |