Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6634288474
commit
bc4cd6ffb9
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -225,44 +218,6 @@ export default {
|
|||
</template>
|
||||
</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"
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -219,10 +219,3 @@
|
|||
width: calc(100% + 24px);
|
||||
margin: -28px -12px 0;
|
||||
}
|
||||
|
||||
.ai-genie-chat-message {
|
||||
pre,
|
||||
code {
|
||||
@include gl-font-sm;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# rubocop: disable Gitlab/NamespacedClass
|
||||
class Organization < ApplicationRecord
|
||||
end
|
||||
# rubocop: enable Gitlab/NamespacedClass
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
c6897ef9e8c57b2b0dc8c94c0b2b9311996528b8f88bbf9b6a955de5d5c5120f
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :organization
|
||||
end
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -26,12 +26,6 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
|
|||
end
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
||||
|
|
|
|||
|
|
@ -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(: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_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(:group_project) { create(:project, :public, group: group) }
|
||||
let(:control_project) { create(:project, group: subsubgroup) }
|
||||
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_it_be(:private_subgroup_project) do
|
||||
create(:project, :private, group: private_subgroup, name: 'private_subgroup_project')
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
Loading…
Reference in New Issue