Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
690c904b5e
commit
9c8e8b5ffc
|
@ -4153,7 +4153,6 @@ Layout/LineLength:
|
|||
- 'spec/features/action_cable_logging_spec.rb'
|
||||
- 'spec/features/admin/admin_abuse_reports_spec.rb'
|
||||
- 'spec/features/admin/admin_mode/login_spec.rb'
|
||||
- 'spec/features/admin/admin_runners_spec.rb'
|
||||
- 'spec/features/admin/admin_sees_background_migrations_spec.rb'
|
||||
- 'spec/features/admin/admin_sees_project_statistics_spec.rb'
|
||||
- 'spec/features/admin/admin_settings_spec.rb'
|
||||
|
|
|
@ -185,7 +185,6 @@ Layout/SpaceInsideParens:
|
|||
- 'spec/controllers/projects/runners_controller_spec.rb'
|
||||
- 'spec/dependencies/omniauth_saml_spec.rb'
|
||||
- 'spec/factories/usage_data.rb'
|
||||
- 'spec/features/admin/admin_runners_spec.rb'
|
||||
- 'spec/features/boards/board_filters_spec.rb'
|
||||
- 'spec/features/boards/user_visits_board_spec.rb'
|
||||
- 'spec/features/dashboard/datetime_on_tooltips_spec.rb'
|
||||
|
|
|
@ -1397,7 +1397,6 @@ RSpec/ContextWording:
|
|||
- 'spec/features/admin/admin_mode/logout_spec.rb'
|
||||
- 'spec/features/admin/admin_mode/workers_spec.rb'
|
||||
- 'spec/features/admin/admin_mode_spec.rb'
|
||||
- 'spec/features/admin/admin_runners_spec.rb'
|
||||
- 'spec/features/admin/admin_search_settings_spec.rb'
|
||||
- 'spec/features/admin/admin_settings_spec.rb'
|
||||
- 'spec/features/admin/dashboard_spec.rb'
|
||||
|
@ -1444,7 +1443,6 @@ RSpec/ContextWording:
|
|||
- 'spec/features/groups/dependency_proxy_for_containers_spec.rb'
|
||||
- 'spec/features/groups/dependency_proxy_spec.rb'
|
||||
- 'spec/features/groups/empty_states_spec.rb'
|
||||
- 'spec/features/groups/group_runners_spec.rb'
|
||||
- 'spec/features/groups/group_settings_spec.rb'
|
||||
- 'spec/features/groups/issues_spec.rb'
|
||||
- 'spec/features/groups/labels/subscription_spec.rb'
|
||||
|
|
|
@ -87,7 +87,6 @@ RSpec/InstanceVariable:
|
|||
- spec/controllers/profiles/avatars_controller_spec.rb
|
||||
- spec/controllers/projects/clusters_controller_spec.rb
|
||||
- spec/controllers/sessions_controller_spec.rb
|
||||
- spec/features/admin/admin_runners_spec.rb
|
||||
- spec/features/calendar_spec.rb
|
||||
- spec/features/issues/user_filters_issues_spec.rb
|
||||
- spec/features/markdown/copy_as_gfm_spec.rb
|
||||
|
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -2,6 +2,16 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 15.1.3 (2022-07-19)
|
||||
|
||||
### Added (1 change)
|
||||
|
||||
- [Add praefect list virtual storages subcommand documentation](gitlab-org/gitlab@95689c32e2734831c00ef30de303098485ec095a) ([merge request](gitlab-org/gitlab!92708))
|
||||
|
||||
### Fixed (1 change)
|
||||
|
||||
- [Fix group access dropdown failure if no subgroups are available](gitlab-org/gitlab@518a2f55caddab0c18d0548d0a8f777afe5ae666) ([merge request](gitlab-org/gitlab!92708)) **GitLab Enterprise Edition**
|
||||
|
||||
## 15.1.2 (2022-07-05)
|
||||
|
||||
### Fixed (3 changes)
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
<script>
|
||||
import { GlFormGroup, GlButton, GlFormInput } from '@gitlab/ui';
|
||||
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
|
||||
import { resourceLinksFormI18n } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'AddIssuableResourceLinkForm',
|
||||
components: {
|
||||
GlFormGroup,
|
||||
GlButton,
|
||||
GlFormInput,
|
||||
},
|
||||
i18n: resourceLinksFormI18n,
|
||||
directives: {
|
||||
autofocusonshow,
|
||||
},
|
||||
props: {
|
||||
isSubmitting: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
linkTextValue: '',
|
||||
linkValue: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isSubmitButtonDisabled() {
|
||||
return this.linkValue.length === 0 || this.isSubmitting;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onFormCancel() {
|
||||
this.linkValue = '';
|
||||
this.linkTextValue = '';
|
||||
this.$emit('add-issuable-resource-link-form-cancel');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form @submit.prevent>
|
||||
<gl-form-group :label="$options.i18n.linkTextLabel">
|
||||
<gl-form-input
|
||||
v-model="linkTextValue"
|
||||
v-autofocusonshow
|
||||
data-testid="link-text-input"
|
||||
type="text"
|
||||
/>
|
||||
</gl-form-group>
|
||||
<gl-form-group :label="$options.i18n.linkValueLabel">
|
||||
<gl-form-input v-model="linkValue" data-testid="link-value-input" type="text" />
|
||||
</gl-form-group>
|
||||
<div class="gl-mt-5 gl-clearfix">
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
data-testid="add-button"
|
||||
:disabled="isSubmitButtonDisabled"
|
||||
:loading="isSubmitting"
|
||||
type="submit"
|
||||
class="gl-float-left"
|
||||
>
|
||||
{{ $options.i18n.submitButtonText }}
|
||||
</gl-button>
|
||||
<gl-button class="gl-float-right" @click="onFormCancel">
|
||||
{{ $options.i18n.cancelButtonText }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
|
@ -1,111 +0,0 @@
|
|||
<script>
|
||||
import { GlLink, GlIcon, GlButton } from '@gitlab/ui';
|
||||
import { resourceLinksI18n } from '../constants';
|
||||
import AddIssuableResourceLinkForm from './add_issuable_resource_link_form.vue';
|
||||
|
||||
export default {
|
||||
name: 'ResourceLinksBlock',
|
||||
components: {
|
||||
GlLink,
|
||||
GlButton,
|
||||
GlIcon,
|
||||
AddIssuableResourceLinkForm,
|
||||
},
|
||||
i18n: resourceLinksI18n,
|
||||
props: {
|
||||
helpPath: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
canAddResourceLinks: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFormVisible: false,
|
||||
isSubmitting: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
badgeLabel() {
|
||||
return 0;
|
||||
},
|
||||
hasBody() {
|
||||
return this.isFormVisible;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async toggleResourceLinkForm() {
|
||||
this.isFormVisible = !this.isFormVisible;
|
||||
},
|
||||
hideResourceLinkForm() {
|
||||
this.isFormVisible = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="resource-links" class="gl-mt-5">
|
||||
<div class="card card-slim gl-overflow-hidden">
|
||||
<div
|
||||
:class="{ 'panel-empty-heading border-bottom-0': !hasBody }"
|
||||
class="card-header gl-display-flex gl-justify-content-space-between"
|
||||
>
|
||||
<h3
|
||||
class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
|
||||
>
|
||||
<gl-link
|
||||
id="user-content-resource-links"
|
||||
class="anchor position-absolute gl-text-decoration-none"
|
||||
href="#resource-links"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<slot name="header-text">{{ $options.i18n.headerText }}</slot>
|
||||
<gl-link
|
||||
:href="helpPath"
|
||||
target="_blank"
|
||||
class="gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
|
||||
data-testid="help-link"
|
||||
:aria-label="$options.i18n.helpText"
|
||||
>
|
||||
<gl-icon name="question" :size="12" />
|
||||
</gl-link>
|
||||
|
||||
<div class="gl-display-inline-flex">
|
||||
<div class="gl-display-inline-flex gl-mx-5">
|
||||
<span class="gl-display-inline-flex gl-align-items-center">
|
||||
<gl-icon name="link" class="gl-mr-2 gl-text-gray-500" />
|
||||
{{ badgeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<gl-button
|
||||
v-if="canAddResourceLinks"
|
||||
icon="plus"
|
||||
:aria-label="$options.i18n.addButtonText"
|
||||
@click="toggleResourceLinkForm"
|
||||
/>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
class="linked-issues-card-body bg-gray-light"
|
||||
:class="{
|
||||
'gl-p-5': isFormVisible,
|
||||
}"
|
||||
>
|
||||
<div v-show="isFormVisible" class="card-body bordered-box gl-bg-white">
|
||||
<add-issuable-resource-link-form
|
||||
ref="resourceLinkForm"
|
||||
:is-submitting="isSubmitting"
|
||||
@add-issuable-resource-link-form-cancel="hideResourceLinkForm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,14 +0,0 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const resourceLinksI18n = Object.freeze({
|
||||
headerText: s__('LinkedResources|Linked resources'),
|
||||
helpText: s__('LinkedResources|Read more about linked resources'),
|
||||
addButtonText: s__('LinkedResources|Add a resource link'),
|
||||
});
|
||||
|
||||
export const resourceLinksFormI18n = Object.freeze({
|
||||
linkTextLabel: s__('LinkedResources|Text (Optional)'),
|
||||
linkValueLabel: s__('LinkedResources|Link'),
|
||||
submitButtonText: s__('LinkedResources|Add'),
|
||||
cancelButtonText: s__('LinkedResources|Cancel'),
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import ResourceLinksBlock from 'ee_component/linked_resources/components/resource_links_block.vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import ResourceLinksBlock from './components/resource_links_block.vue';
|
||||
|
||||
export default function initLinkedResources() {
|
||||
const linkedResourcesRootElement = document.querySelector('.js-linked-resources-root');
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { uniqueId } from 'lodash';
|
||||
import { GlIcon, GlPopover, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { timeTilRun } from '../../utils';
|
||||
|
@ -43,6 +44,11 @@ export default {
|
|||
CLEANUP_STATUS_UNFINISHED,
|
||||
PARTIAL_CLEANUP_CONTINUE_MESSAGE,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
iconId: uniqueId('status-info-'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showStatus() {
|
||||
return this.status !== UNSCHEDULED_STATUS;
|
||||
|
@ -85,14 +91,14 @@ export default {
|
|||
</span>
|
||||
<gl-icon
|
||||
v-if="failedDelete"
|
||||
id="status-info"
|
||||
:id="iconId"
|
||||
:size="14"
|
||||
class="gl-text-gray-500"
|
||||
data-testid="extra-info"
|
||||
name="information-o"
|
||||
/>
|
||||
<gl-popover
|
||||
target="status-info"
|
||||
:target="iconId"
|
||||
container="status-popover-container"
|
||||
v-bind="$options.statusPopoverOptions"
|
||||
>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlToast } from '@gitlab/ui';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import WorkItemLinks from './work_item_links.vue';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(GlToast);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from '../../constants';
|
||||
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
|
||||
import WorkItemLinksForm from './work_item_links_form.vue';
|
||||
import WorkItemLinksMenu from './work_item_links_menu.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -19,6 +20,7 @@ export default {
|
|||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
WorkItemLinksForm,
|
||||
WorkItemLinksMenu,
|
||||
},
|
||||
props: {
|
||||
workItemId: {
|
||||
|
@ -156,19 +158,22 @@ export default {
|
|||
<div
|
||||
v-for="child in children"
|
||||
:key="child.id"
|
||||
class="gl-relative gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base"
|
||||
class="gl-relative gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
|
||||
data-testid="links-child"
|
||||
>
|
||||
<div>
|
||||
<gl-icon :name="$options.WIDGET_TYPE_TASK_ICON" class="gl-mr-3 gl-text-gray-700" />
|
||||
<span class="gl-word-break-all">{{ child.title }}</span>
|
||||
</div>
|
||||
<div class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0">
|
||||
<div
|
||||
class="gl-ml-0 gl-sm-ml-auto! gl-mt-3 gl-sm-mt-0 gl-display-inline-flex gl-align-items-center"
|
||||
>
|
||||
<gl-badge :variant="badgeVariant(child.state)">
|
||||
<span class="gl-sm-display-block">{{
|
||||
$options.WORK_ITEM_STATUS_TEXT[child.state]
|
||||
}}</span>
|
||||
</gl-badge>
|
||||
<work-item-links-menu :work-item-id="child.id" :parent-work-item-id="issuableGid" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<script>
|
||||
import { GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { produce } from 'immer';
|
||||
import { s__ } from '~/locale';
|
||||
import changeWorkItemParentMutation from '../../graphql/change_work_item_parent_link.mutation.graphql';
|
||||
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
|
||||
import { WIDGET_TYPE_HIERARCHY } from '../../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDropdownItem,
|
||||
GlDropdown,
|
||||
GlIcon,
|
||||
},
|
||||
props: {
|
||||
workItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
parentWorkItemId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeToast: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleChildFromCache(data, store) {
|
||||
const sourceData = store.readQuery({
|
||||
query: getWorkItemLinksQuery,
|
||||
variables: { id: this.parentWorkItemId },
|
||||
});
|
||||
|
||||
const newData = produce(sourceData, (draftState) => {
|
||||
const widgetHierarchy = draftState.workItem.widgets.find(
|
||||
(widget) => widget.type === WIDGET_TYPE_HIERARCHY,
|
||||
);
|
||||
|
||||
const index = widgetHierarchy.children.nodes.findIndex(
|
||||
(child) => child.id === this.workItemId,
|
||||
);
|
||||
|
||||
if (index >= 0) {
|
||||
widgetHierarchy.children.nodes.splice(index, 1);
|
||||
} else {
|
||||
widgetHierarchy.children.nodes.push(data.workItemUpdate.workItem);
|
||||
}
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query: getWorkItemLinksQuery,
|
||||
variables: { id: this.parentWorkItemId },
|
||||
data: newData,
|
||||
});
|
||||
},
|
||||
async addChild(data) {
|
||||
const { data: resp } = await this.$apollo.mutate({
|
||||
mutation: changeWorkItemParentMutation,
|
||||
variables: { id: this.workItemId, parentId: this.parentWorkItemId },
|
||||
update: this.toggleChildFromCache.bind(this, data),
|
||||
});
|
||||
|
||||
if (resp.workItemUpdate.errors.length === 0) {
|
||||
this.activeToast?.hide();
|
||||
}
|
||||
},
|
||||
async removeChild() {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: changeWorkItemParentMutation,
|
||||
variables: { id: this.workItemId, parentId: null },
|
||||
update: this.toggleChildFromCache.bind(this, null),
|
||||
});
|
||||
|
||||
if (data.workItemUpdate.errors.length === 0) {
|
||||
this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
|
||||
action: {
|
||||
text: s__('WorkItem|Undo'),
|
||||
onClick: this.addChild.bind(this, data),
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="gl-ml-2">
|
||||
<gl-dropdown category="tertiary" toggle-class="btn-icon" :right="true">
|
||||
<template #button-content>
|
||||
<gl-icon name="ellipsis_v" :size="14" />
|
||||
</template>
|
||||
<gl-dropdown-item @click="removeChild">
|
||||
{{ s__('WorkItem|Remove') }}
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</span>
|
||||
</template>
|
|
@ -0,0 +1,13 @@
|
|||
mutation changeWorkItemParentLink($id: WorkItemID!, $parentId: WorkItemID) {
|
||||
workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) {
|
||||
workItem {
|
||||
id
|
||||
workItemType {
|
||||
id
|
||||
}
|
||||
title
|
||||
state
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ module Projects
|
|||
|
||||
def test_suite
|
||||
suite = builds.sum do |build|
|
||||
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
|
||||
build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
|
||||
end
|
||||
|
||||
Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, project).load!
|
||||
|
|
|
@ -28,7 +28,7 @@ module Resolvers
|
|||
|
||||
def load_test_suite_data(builds)
|
||||
suite = builds.sum do |build|
|
||||
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
|
||||
build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
|
||||
end
|
||||
|
||||
Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load!
|
||||
|
|
|
@ -11,6 +11,7 @@ module Users
|
|||
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
|
||||
SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
|
||||
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
|
||||
WEB_HOOK_DISABLED = 'web_hook_disabled'
|
||||
|
||||
def show_gke_cluster_integration_callout?(project)
|
||||
active_nav_link?(controller: sidebar_operations_paths) &&
|
||||
|
@ -60,12 +61,31 @@ module Users
|
|||
!user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
|
||||
end
|
||||
|
||||
def web_hook_disabled_dismissed?(project)
|
||||
return false unless project
|
||||
|
||||
last_failure = Gitlab::Redis::SharedState.with do |redis|
|
||||
key = "web_hooks:last_failure:project-#{project.id}"
|
||||
redis.get(key)
|
||||
end
|
||||
|
||||
last_failure = DateTime.parse(last_failure) if last_failure
|
||||
|
||||
user_dismissed?(WEB_HOOK_DISABLED, last_failure, namespace: project.namespace)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
|
||||
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, namespace: nil)
|
||||
return false unless current_user
|
||||
|
||||
current_user.dismissed_callout?(feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than)
|
||||
query = { feature_name: feature_name, ignore_dismissal_earlier_than: ignore_dismissal_earlier_than }
|
||||
|
||||
if namespace
|
||||
current_user.dismissed_callout_for_namespace?(namespace: namespace, **query)
|
||||
else
|
||||
current_user.dismissed_callout?(**query)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module WebHooks
|
||||
module WebHooksHelper
|
||||
EXPIRY_TTL = 1.hour
|
||||
|
||||
def show_project_hook_failed_callout?(project:)
|
||||
return false unless current_user
|
||||
return false unless Feature.enabled?(:webhooks_failed_callout, project)
|
||||
return false unless Feature.enabled?(:web_hooks_disable_failed, project)
|
||||
return false unless Ability.allowed?(current_user, :read_web_hooks, project)
|
||||
|
||||
# Assumes include of Users::CalloutsHelper
|
||||
return false if web_hook_disabled_dismissed?(project)
|
||||
|
||||
any_project_hook_failed?(project) # Most expensive query last
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def any_project_hook_failed?(project)
|
||||
Rails.cache.fetch("any_web_hook_failed:#{project.id}", expires_in: EXPIRY_TTL) do
|
||||
ProjectHook.for_projects(project).disabled.exists?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1090,7 +1090,7 @@ module Ci
|
|||
end
|
||||
|
||||
def test_reports
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |test_reports|
|
||||
latest_test_report_builds.find_each do |build|
|
||||
build.collect_test_reports!(test_reports)
|
||||
end
|
||||
|
|
|
@ -27,6 +27,8 @@ class ProjectHook < WebHook
|
|||
belongs_to :project
|
||||
validates :project, presence: true
|
||||
|
||||
scope :for_projects, ->(project) { where(project: project) }
|
||||
|
||||
def pluralized_name
|
||||
_('Webhooks')
|
||||
end
|
||||
|
@ -41,6 +43,19 @@ class ProjectHook < WebHook
|
|||
project
|
||||
end
|
||||
|
||||
override :update_last_failure
|
||||
def update_last_failure
|
||||
return if executable?
|
||||
|
||||
key = "web_hooks:last_failure:project-#{project_id}"
|
||||
time = Time.current.utc.iso8601
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
prev = redis.get(key)
|
||||
redis.set(key, time) if !prev || prev < time
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
override :web_hooks_disable_failed?
|
||||
|
|
|
@ -48,6 +48,11 @@ class WebHook < ApplicationRecord
|
|||
where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
|
||||
end
|
||||
|
||||
# Inverse of executable
|
||||
scope :disabled, -> do
|
||||
where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current)
|
||||
end
|
||||
|
||||
def executable?
|
||||
!temporarily_disabled? && !permanently_disabled?
|
||||
end
|
||||
|
@ -181,6 +186,10 @@ class WebHook < ApplicationRecord
|
|||
raise InterpolationError, "Invalid URL template. Missing key #{e.key}"
|
||||
end
|
||||
|
||||
def update_last_failure
|
||||
# Overridden in child classes.
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def web_hooks_disable_failed?
|
||||
|
|
|
@ -111,6 +111,7 @@ class Note < ApplicationRecord
|
|||
end
|
||||
|
||||
validate :does_not_exceed_notes_limit?, on: :create, unless: [:system?, :importing?]
|
||||
validate :validate_created_after
|
||||
|
||||
# @deprecated attachments are handled by the Upload model.
|
||||
#
|
||||
|
@ -748,6 +749,13 @@ class Note < ApplicationRecord
|
|||
errors.add(:base, _('Maximum number of comments exceeded')) if noteable.notes.count >= Noteable::MAX_NOTES_LIMIT
|
||||
end
|
||||
|
||||
def validate_created_after
|
||||
return unless created_at
|
||||
return if created_at >= '1970-01-01'
|
||||
|
||||
errors.add(:created_at, s_('Note|The created date provided is too far in the past.'))
|
||||
end
|
||||
|
||||
def noteable_label_url_method
|
||||
for_merge_request? ? :project_merge_requests_url : :project_issues_url
|
||||
end
|
||||
|
|
|
@ -222,6 +222,7 @@ class User < ApplicationRecord
|
|||
has_many :custom_attributes, class_name: 'UserCustomAttribute'
|
||||
has_many :callouts, class_name: 'Users::Callout'
|
||||
has_many :group_callouts, class_name: 'Users::GroupCallout'
|
||||
has_many :namespace_callouts, class_name: 'Users::NamespaceCallout'
|
||||
has_many :term_agreements
|
||||
belongs_to :accepted_term, class_name: 'ApplicationSetting::Term'
|
||||
|
||||
|
@ -2085,6 +2086,13 @@ class User < ApplicationRecord
|
|||
callout_dismissed?(callout, ignore_dismissal_earlier_than)
|
||||
end
|
||||
|
||||
def dismissed_callout_for_namespace?(feature_name:, namespace:, ignore_dismissal_earlier_than: nil)
|
||||
source_feature_name = "#{feature_name}_#{namespace.id}"
|
||||
callout = namespace_callouts_by_feature_name[source_feature_name]
|
||||
|
||||
callout_dismissed?(callout, ignore_dismissal_earlier_than)
|
||||
end
|
||||
|
||||
# Load the current highest access by looking directly at the user's memberships
|
||||
def current_highest_access_level
|
||||
members.non_request.maximum(:access_level)
|
||||
|
@ -2111,6 +2119,11 @@ class User < ApplicationRecord
|
|||
.find_or_initialize_by(feature_name: ::Users::GroupCallout.feature_names[feature_name], group_id: group_id)
|
||||
end
|
||||
|
||||
def find_or_initialize_namespace_callout(feature_name, namespace_id)
|
||||
namespace_callouts
|
||||
.find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id)
|
||||
end
|
||||
|
||||
def can_trigger_notifications?
|
||||
confirmed? && !blocked? && !ghost?
|
||||
end
|
||||
|
@ -2228,6 +2241,10 @@ class User < ApplicationRecord
|
|||
@group_callouts_by_feature_name ||= group_callouts.index_by(&:source_feature_name)
|
||||
end
|
||||
|
||||
def namespace_callouts_by_feature_name
|
||||
@namespace_callouts_by_feature_name ||= namespace_callouts.index_by(&:source_feature_name)
|
||||
end
|
||||
|
||||
def authorized_groups_without_shared_membership
|
||||
Group.from_union([
|
||||
groups.select(*Namespace.cached_column_list),
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Users
|
||||
class NamespaceCallout < ApplicationRecord
|
||||
include Users::Calloutable
|
||||
|
||||
self.table_name = 'user_namespace_callouts'
|
||||
|
||||
belongs_to :namespace
|
||||
|
||||
enum feature_name: {
|
||||
invite_members_banner: 1,
|
||||
approaching_seat_count_threshold: 2, # EE-only
|
||||
storage_enforcement_banner_first_enforcement_threshold: 3,
|
||||
storage_enforcement_banner_second_enforcement_threshold: 4,
|
||||
storage_enforcement_banner_third_enforcement_threshold: 5,
|
||||
storage_enforcement_banner_fourth_enforcement_threshold: 6,
|
||||
preview_user_over_limit_free_plan_alert: 7, # EE-only
|
||||
user_reached_limit_free_plan_alert: 8, # EE-only
|
||||
web_hook_disabled: 9
|
||||
}
|
||||
|
||||
validates :namespace, presence: true
|
||||
validates :feature_name,
|
||||
presence: true,
|
||||
uniqueness: { scope: [:user_id, :namespace_id] },
|
||||
inclusion: { in: NamespaceCallout.feature_names.keys }
|
||||
|
||||
def source_feature_name
|
||||
"#{feature_name}_#{namespace_id}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -492,6 +492,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :update_runners_registration_token
|
||||
enable :admin_project_google_cloud
|
||||
enable :admin_secure_files
|
||||
enable :read_web_hooks
|
||||
end
|
||||
|
||||
rule { public_project & metrics_dashboard_allowed }.policy do
|
||||
|
|
|
@ -22,7 +22,7 @@ module Ci
|
|||
private
|
||||
|
||||
def generate_test_suite_report(build)
|
||||
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
|
||||
build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
|
||||
end
|
||||
|
||||
def tests_params(test_suite)
|
||||
|
|
|
@ -81,7 +81,7 @@ module Ci
|
|||
|
||||
def generate_test_suite!(build)
|
||||
# Returns an instance of Gitlab::Ci::Reports::TestSuite
|
||||
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
|
||||
build.collect_test_reports!(Gitlab::Ci::Reports::TestReport.new)
|
||||
end
|
||||
|
||||
def ci_unit_test_attrs(batch)
|
||||
|
|
|
@ -44,6 +44,7 @@ module WebHooks
|
|||
end
|
||||
|
||||
log_state_change
|
||||
hook.update_last_failure
|
||||
end
|
||||
rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
|
||||
raise if raise_lock_error?
|
||||
|
|
|
@ -2,14 +2,18 @@
|
|||
|
||||
.container
|
||||
= render partial: "shared/errors/graphic_422", formats: :svg
|
||||
%h3 Sign-in using #{@provider} auth failed
|
||||
%h3
|
||||
= _('Sign-in using %{provider} auth failed') % { provider: @provider }
|
||||
|
||||
%p.light.subtitle Sign-in failed because #{@error}.
|
||||
%p.light.subtitle
|
||||
= _('Sign-in failed because %{error}.') % { error: @error }
|
||||
|
||||
%p Try logging in using your username or email. If you have forgotten your password, try recovering it
|
||||
%p
|
||||
= _('Try logging in using your username or email. If you have forgotten your password, try recovering it')
|
||||
|
||||
= link_to "Sign in", new_session_path(:user), class: 'gl-button btn primary'
|
||||
= link_to "Recover password", new_password_path(:user), class: 'gl-button btn secondary'
|
||||
= link_to _('Sign in'), new_session_path(:user), class: 'gl-button btn primary'
|
||||
= link_to _('Recover password'), new_password_path(:user), class: 'gl-button btn secondary'
|
||||
|
||||
%hr
|
||||
%p.light If none of the options work, try contacting a GitLab administrator.
|
||||
%p.light
|
||||
= _('If none of the options work, try contacting a GitLab administrator.')
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
= preserve(markdown(commit.description, pipeline: :single_line))
|
||||
|
||||
.info-well
|
||||
.well-segment.pipeline-info
|
||||
.icon-container.gl-vertical-align-text-bottom
|
||||
= sprite_icon('clock')
|
||||
.well-segment.pipeline-info{ class: "gl-align-items-baseline!" }
|
||||
.icon-container
|
||||
= sprite_icon('clock', css_class: 'gl-top-0!')
|
||||
= pluralize @pipeline.total_size, "job"
|
||||
= @pipeline.ref_text
|
||||
- if @pipeline.duration
|
||||
|
@ -20,7 +20,7 @@
|
|||
- if has_pipeline_badges?(@pipeline)
|
||||
.well-segment.qa-pipeline-badges
|
||||
.icon-container
|
||||
= sprite_icon('flag')
|
||||
= sprite_icon('flag', css_class: 'gl-top-0!')
|
||||
- if @pipeline.child?
|
||||
- text = sprintf(s_('Pipelines|Child pipeline (%{link_start}parent%{link_end})'), { link_start: "<a href='#{pipeline_path(@pipeline.triggered_by_pipeline)}' class='text-underline'>", link_end: "</a>"}).html_safe
|
||||
= gl_badge_tag text, { variant: :info, size: :sm }, { class: 'js-pipeline-child has-tooltip', title: s_("Pipelines|This is a child pipeline within the parent pipeline") }
|
||||
|
@ -44,13 +44,13 @@
|
|||
|
||||
.well-segment.branch-info
|
||||
.icon-container.commit-icon
|
||||
= custom_icon("icon_commit")
|
||||
= sprite_icon('commit', css_class: 'gl-top-0!')
|
||||
= link_to commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha"
|
||||
= clipboard_button(text: @pipeline.sha, title: _("Copy commit SHA"))
|
||||
|
||||
.well-segment.related-merge-request-info
|
||||
.icon-container
|
||||
= sprite_icon("git-merge")
|
||||
= sprite_icon("git-merge", css_class: 'gl-top-0!')
|
||||
%span.related-merge-requests
|
||||
%span.js-truncated-mr-list
|
||||
= @pipeline.all_related_merge_request_text(limit: 1)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: webhooks_failed_callout
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91092
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/365535
|
||||
milestone: '15.2'
|
||||
type: development
|
||||
group: group::integrations
|
||||
default_enabled: false
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
---
|
||||
table_name: user_namespace_callouts
|
||||
classes:
|
||||
- Users::NamespaceCallout
|
||||
feature_categories:
|
||||
- navigation
|
||||
description: Contains records of which users have dismissed a callout, grouped by namespace.
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91092
|
||||
milestone: '15.2'
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateUserNamespaceCallouts < Gitlab::Database::Migration[2.0]
|
||||
def up
|
||||
create_table :user_namespace_callouts do |t|
|
||||
t.bigint :user_id, null: false
|
||||
t.bigint :namespace_id, null: false, index: true
|
||||
t.datetime_with_timezone :dismissed_at
|
||||
t.integer :feature_name, limit: 2, null: false
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
drop_table :user_namespace_callouts
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddForeignKeysToUserNamespaceCallouts < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_foreign_key :user_namespace_callouts, :users,
|
||||
column: :user_id,
|
||||
on_delete: :cascade
|
||||
|
||||
add_concurrent_foreign_key :user_namespace_callouts, :namespaces,
|
||||
column: :namespace_id,
|
||||
on_delete: :cascade
|
||||
|
||||
add_concurrent_index :user_namespace_callouts, [:user_id, :feature_name, :namespace_id],
|
||||
unique: true,
|
||||
name: 'index_ns_user_callouts_feature'
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :user_namespace_callouts, 'index_ns_user_callouts_feature'
|
||||
|
||||
with_lock_retries do
|
||||
remove_foreign_key :user_namespace_callouts, column: :user_id
|
||||
remove_foreign_key :user_namespace_callouts, column: :namespace_id
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DropTokenIndexFromCiBuilds < Gitlab::Database::Migration[2.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_ci_builds_on_token_partial'
|
||||
|
||||
def up
|
||||
remove_concurrent_index_by_name :ci_builds, INDEX_NAME
|
||||
end
|
||||
|
||||
# rubocop:disable Migration/PreventIndexCreation
|
||||
def down
|
||||
add_concurrent_index :ci_builds, :token, unique: true, where: 'token IS NOT NULL', name: INDEX_NAME
|
||||
end
|
||||
# rubocop:enable Migration/PreventIndexCreation
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
|
||||
# for more information on how to write migrations for GitLab.
|
||||
|
||||
class UpdateNotesInPast < Gitlab::Database::Migration[2.0]
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
loop do
|
||||
update_count = define_batchable_model('notes')
|
||||
.where('created_at < ?', '1970-01-01').limit(100)
|
||||
.update_all(created_at: '1970-01-01 00:00:00')
|
||||
|
||||
break if update_count == 0
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no op
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
29ab69647b53c331aefdd62e8fbcc1567df4424a8e7ae6f8eb7b1e9afa7a6911
|
|
@ -0,0 +1 @@
|
|||
6d65af0d20cd80cf3367f48c5447ff33046e982ac1cfd55aaf52a7cc2330e428
|
|
@ -0,0 +1 @@
|
|||
5a4a6355d1954735a05831e17c97e2879320f2cb313be56fb72e1cd2c20d9090
|
|
@ -0,0 +1 @@
|
|||
ea8182741ce0b30f2de23041d1f6bafaf6e04a7a7d0f50abcd04462683637596
|
|
@ -21669,6 +21669,23 @@ CREATE TABLE user_interacted_projects (
|
|||
project_id integer NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE user_namespace_callouts (
|
||||
id bigint NOT NULL,
|
||||
user_id bigint NOT NULL,
|
||||
namespace_id bigint NOT NULL,
|
||||
dismissed_at timestamp with time zone,
|
||||
feature_name smallint NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE user_namespace_callouts_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE user_namespace_callouts_id_seq OWNED BY user_namespace_callouts.id;
|
||||
|
||||
CREATE TABLE user_permission_export_uploads (
|
||||
id bigint NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
|
@ -23602,6 +23619,8 @@ ALTER TABLE ONLY user_details ALTER COLUMN user_id SET DEFAULT nextval('user_det
|
|||
|
||||
ALTER TABLE ONLY user_group_callouts ALTER COLUMN id SET DEFAULT nextval('user_group_callouts_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY user_namespace_callouts ALTER COLUMN id SET DEFAULT nextval('user_namespace_callouts_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY user_permission_export_uploads ALTER COLUMN id SET DEFAULT nextval('user_permission_export_uploads_id_seq'::regclass);
|
||||
|
||||
ALTER TABLE ONLY user_preferences ALTER COLUMN id SET DEFAULT nextval('user_preferences_id_seq'::regclass);
|
||||
|
@ -25879,6 +25898,9 @@ ALTER TABLE ONLY user_highest_roles
|
|||
ALTER TABLE ONLY user_interacted_projects
|
||||
ADD CONSTRAINT user_interacted_projects_pkey PRIMARY KEY (project_id, user_id);
|
||||
|
||||
ALTER TABLE ONLY user_namespace_callouts
|
||||
ADD CONSTRAINT user_namespace_callouts_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY user_permission_export_uploads
|
||||
ADD CONSTRAINT user_permission_export_uploads_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -27495,8 +27517,6 @@ CREATE INDEX index_ci_builds_on_status_and_type_and_runner_id ON ci_builds USING
|
|||
|
||||
CREATE UNIQUE INDEX index_ci_builds_on_token_encrypted ON ci_builds USING btree (token_encrypted) WHERE (token_encrypted IS NOT NULL);
|
||||
|
||||
CREATE UNIQUE INDEX index_ci_builds_on_token_partial ON ci_builds USING btree (token) WHERE (token IS NOT NULL);
|
||||
|
||||
CREATE INDEX index_ci_builds_on_updated_at ON ci_builds USING btree (updated_at);
|
||||
|
||||
CREATE INDEX index_ci_builds_on_upstream_pipeline_id ON ci_builds USING btree (upstream_pipeline_id) WHERE (upstream_pipeline_id IS NOT NULL);
|
||||
|
@ -28911,6 +28931,8 @@ CREATE INDEX index_notification_settings_on_source_and_level_and_user ON notific
|
|||
|
||||
CREATE UNIQUE INDEX index_notifications_on_user_id_and_source_id_and_source_type ON notification_settings USING btree (user_id, source_id, source_type);
|
||||
|
||||
CREATE UNIQUE INDEX index_ns_user_callouts_feature ON user_namespace_callouts USING btree (user_id, feature_name, namespace_id);
|
||||
|
||||
CREATE INDEX index_oauth_access_grants_on_resource_owner_id ON oauth_access_grants USING btree (resource_owner_id, application_id, created_at);
|
||||
|
||||
CREATE UNIQUE INDEX index_oauth_access_grants_on_token ON oauth_access_grants USING btree (token);
|
||||
|
@ -29913,6 +29935,8 @@ CREATE INDEX index_user_highest_roles_on_user_id_and_highest_access_level ON use
|
|||
|
||||
CREATE INDEX index_user_interacted_projects_on_user_id ON user_interacted_projects USING btree (user_id);
|
||||
|
||||
CREATE INDEX index_user_namespace_callouts_on_namespace_id ON user_namespace_callouts USING btree (namespace_id);
|
||||
|
||||
CREATE INDEX index_user_permission_export_uploads_on_user_id_and_status ON user_permission_export_uploads USING btree (user_id, status);
|
||||
|
||||
CREATE INDEX index_user_preferences_on_gitpod_enabled ON user_preferences USING btree (gitpod_enabled);
|
||||
|
@ -31780,6 +31804,9 @@ ALTER TABLE ONLY ci_pipelines
|
|||
ALTER TABLE ONLY geo_event_log
|
||||
ADD CONSTRAINT fk_27548c6db3 FOREIGN KEY (hashed_storage_migrated_event_id) REFERENCES geo_hashed_storage_migrated_events(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY user_namespace_callouts
|
||||
ADD CONSTRAINT fk_27a69fd1bd FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY merge_requests_compliance_violations
|
||||
ADD CONSTRAINT fk_290ec1ab02 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -31885,6 +31912,9 @@ ALTER TABLE ONLY releases
|
|||
ALTER TABLE ONLY geo_event_log
|
||||
ADD CONSTRAINT fk_4a99ebfd60 FOREIGN KEY (repositories_changed_event_id) REFERENCES geo_repositories_changed_events(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY user_namespace_callouts
|
||||
ADD CONSTRAINT fk_4b1257f385 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY sbom_occurrences
|
||||
ADD CONSTRAINT fk_4b88e5b255 FOREIGN KEY (component_version_id) REFERENCES sbom_component_versions(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -43,6 +43,35 @@ The console is in the toolbox pod. Refer to our [Kubernetes cheat sheet](https:/
|
|||
|
||||
To exit the console, type: `quit`.
|
||||
|
||||
## Enable Active Record logging
|
||||
|
||||
You can enable output of Active Record debug logging in the Rails console
|
||||
session by running:
|
||||
|
||||
```ruby
|
||||
ActiveRecord::Base.logger = Logger.new($stdout)
|
||||
```
|
||||
|
||||
This shows information about database queries triggered by any Ruby code
|
||||
you may run in the console. To turn off logging again, run:
|
||||
|
||||
```ruby
|
||||
ActiveRecord::Base.logger = nil
|
||||
```
|
||||
|
||||
## Disable database statement timeout
|
||||
|
||||
You can disable the PostgreSQL statement timeout for the current Rails console
|
||||
session by running:
|
||||
|
||||
```ruby
|
||||
ActiveRecord::Base.connection.execute('SET statement_timeout TO 0')
|
||||
```
|
||||
|
||||
This change only affects the current Rails console session and is
|
||||
not persisted in the GitLab production environment or in the next Rails
|
||||
console session.
|
||||
|
||||
## Output Rails console session history
|
||||
|
||||
Enter the following command on the rails console to display
|
||||
|
|
|
@ -9,83 +9,6 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
Sometimes things don't work the way they should. Here are some tips on debugging issues out
|
||||
in production.
|
||||
|
||||
## Starting a Rails console session
|
||||
|
||||
Troubleshooting and debugging your GitLab instance often requires a Rails console.
|
||||
|
||||
Your type of GitLab installation determines how
|
||||
[to start a rails console](../operations/rails_console.md).
|
||||
See also:
|
||||
|
||||
- [GitLab Rails Console Cheat Sheet](gitlab_rails_cheat_sheet.md).
|
||||
|
||||
### Enabling Active Record logging
|
||||
|
||||
You can enable output of Active Record debug logging in the Rails console
|
||||
session by running:
|
||||
|
||||
```ruby
|
||||
ActiveRecord::Base.logger = Logger.new($stdout)
|
||||
```
|
||||
|
||||
This shows information about database queries triggered by any Ruby code
|
||||
you may run in the console. To turn off logging again, run:
|
||||
|
||||
```ruby
|
||||
ActiveRecord::Base.logger = nil
|
||||
```
|
||||
|
||||
### Disabling database statement timeout
|
||||
|
||||
You can disable the PostgreSQL statement timeout for the current Rails console
|
||||
session by running:
|
||||
|
||||
```ruby
|
||||
ActiveRecord::Base.connection.execute('SET statement_timeout TO 0')
|
||||
```
|
||||
|
||||
This change only affects the current Rails console session and is
|
||||
not persisted in the GitLab production environment or in the next Rails
|
||||
console session.
|
||||
|
||||
### Output Rails console session history
|
||||
|
||||
If you'd like to output your Rails console command history in a format that's
|
||||
easy to copy and save for future reference, you can run:
|
||||
|
||||
```ruby
|
||||
puts Readline::HISTORY.to_a
|
||||
```
|
||||
|
||||
## Using the Rails runner
|
||||
|
||||
If you need to run some Ruby code in the context of your GitLab production
|
||||
environment, you can do so using the [Rails runner](https://guides.rubyonrails.org/command_line.html#rails-runner). When executing a script file, the script must be accessible by the `git` user.
|
||||
|
||||
**For Omnibus installations**
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails runner "RAILS_COMMAND"
|
||||
|
||||
# Example with a two-line Ruby script
|
||||
sudo gitlab-rails runner "user = User.first; puts user.username"
|
||||
|
||||
# Example with a ruby script file (make sure to use the full path)
|
||||
sudo gitlab-rails runner /path/to/script.rb
|
||||
```
|
||||
|
||||
**For installations from source**
|
||||
|
||||
```shell
|
||||
sudo -u git -H bundle exec rails runner -e production "RAILS_COMMAND"
|
||||
|
||||
# Example with a two-line Ruby script
|
||||
sudo -u git -H bundle exec rails runner -e production "user = User.first; puts user.username"
|
||||
|
||||
# Example with a ruby script file (make sure to use the full path)
|
||||
sudo -u git -H bundle exec rails runner -e production /path/to/script.rb
|
||||
```
|
||||
|
||||
## More information
|
||||
|
||||
- [Debugging Stuck Ruby Processes](https://newrelic.com/blog/best-practices/debugging-stuck-ruby-processes-what-to-do-before-you-kill-9)
|
||||
|
|
|
@ -453,12 +453,14 @@ Keyset-pagination allows for more efficient retrieval of pages and - in contrast
|
|||
to offset-based pagination - runtime is independent of the size of the
|
||||
collection.
|
||||
|
||||
This method is controlled by the following parameters:
|
||||
This method is controlled by the following parameters. `order_by` and `sort` are both mandatory.
|
||||
|
||||
| Parameter | Description |
|
||||
|--------------| ------------|
|
||||
| `pagination` | `keyset` (to enable keyset pagination). |
|
||||
| `per_page` | Number of items to list per page (default: `20`, max: `100`). |
|
||||
| Parameter | Required | Description |
|
||||
|--------------| ------------ | --------- |
|
||||
| `pagination` | yes | `keyset` (to enable keyset pagination). |
|
||||
| `per_page` | no | Number of items to list per page (default: `20`, max: `100`). |
|
||||
| `order_by` | yes | Column by which to order by. |
|
||||
| `sort` | yes | Sort order (`asc` or `desc`) |
|
||||
|
||||
In the following example, we list 50 [projects](projects.md) per page, ordered
|
||||
by `id` ascending.
|
||||
|
|
|
@ -146,7 +146,7 @@ Parameters:
|
|||
| `issue_iid` | integer | yes | The IID of an issue. |
|
||||
| `body` | string | yes | The content of a note. Limited to 1,000,000 characters. |
|
||||
| `confidential` | boolean | no | The confidential flag of a note. Default is false. |
|
||||
| `created_at` | string | no | Date time string, ISO 8601 formatted. Example: `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
|
||||
| `created_at` | string | no | Date time string, ISO 8601 formatted. It must be after 1970-01-01. Example: `2016-03-11T03:45:40Z` (requires administrator or project/group owner rights) |
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/notes?body=note"
|
||||
|
|
|
@ -316,11 +316,12 @@ and [Container Scanning](../../user/application_security/container_scanning/inde
|
|||
|
||||
You can find the schemas for these scanners here:
|
||||
|
||||
- [SAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json)
|
||||
- [DAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dast-report-format.json)
|
||||
- [Dependency Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dependency-scanning-report-format.json)
|
||||
- [Cluster Image Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/cluster-image-scanning-report-format.json)
|
||||
- [Container Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/container-scanning-report-format.json)
|
||||
- [Coverage Fuzzing](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/coverage-fuzzing-report-format.json)
|
||||
- [DAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dast-report-format.json)
|
||||
- [Dependency Scanning](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/dependency-scanning-report-format.json)
|
||||
- [SAST](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/sast-report-format.json)
|
||||
- [Secret Detection](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/master/dist/secret-detection-report-format.json)
|
||||
|
||||
### Retention period for vulnerabilities
|
||||
|
|
|
@ -220,11 +220,12 @@ once it's imported into the database.
|
|||
|
||||
The type of scan. This must be one of the following:
|
||||
|
||||
- `container_scanning`
|
||||
- `dependency_scanning`
|
||||
- `dast`
|
||||
- `sast`
|
||||
- `cluster_image_scanning`
|
||||
- `container_scanning`
|
||||
- `dast`
|
||||
- `dependency_scanning`
|
||||
- `sast`
|
||||
- `secret_detection`
|
||||
|
||||
### Scanner
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ To create a personal access token programmatically:
|
|||
```
|
||||
|
||||
This code can be shortened into a single-line shell command by using the
|
||||
[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
|
||||
[Rails runner](../../administration/operations/rails_console.md#using-the-rails-runner):
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails runner "token = User.find_by_username('automation-bot').personal_access_tokens.create(scopes: [:read_user, :read_repository], name: 'Automation token'); token.set_token('token-string-here123'); token.save!"
|
||||
|
@ -177,7 +177,7 @@ To revoke a token programmatically:
|
|||
```
|
||||
|
||||
This code can be shortened into a single-line shell command using the
|
||||
[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
|
||||
[Rails runner](../../administration/operations/rails_console.md#using-the-rails-runner):
|
||||
|
||||
```shell
|
||||
sudo gitlab-rails runner "PersonalAccessToken.find_by_token('token-string-here123').revoke!"
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 4.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
Before Width: | Height: | Size: 14 KiB |
|
@ -23,7 +23,26 @@ merge requests are merged into an existing branch.
|
|||
This setting is the default. It always creates a separate merge commit,
|
||||
even when using [squash](../squash_and_merge.md). An example commit graph generated using this merge method:
|
||||
|
||||
![Commit graph for merge commits](../img/merge_method_merge_commit_v15_0.png)
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit id: "Init"
|
||||
branch mr-branch-1
|
||||
commit
|
||||
checkout main
|
||||
commit
|
||||
branch mr-branch-2
|
||||
commit
|
||||
checkout mr-branch-1
|
||||
commit
|
||||
checkout main
|
||||
branch squash-mr
|
||||
commit id: "Squashed commits"
|
||||
checkout main
|
||||
merge squash-mr
|
||||
merge mr-branch-1
|
||||
commit
|
||||
merge mr-branch-2
|
||||
```
|
||||
|
||||
- For regular merges, it is equivalent to the command `git merge --no-ff <source-branch>`.
|
||||
- For squash merges, it squashes all commits in the source branch before merging it normally. It performs actions similar to:
|
||||
|
@ -42,7 +61,25 @@ A merge commit is created for every merge, but the branch is only merged if
|
|||
a fast-forward merge is possible. This ensures that if the merge request build
|
||||
succeeded, the target branch build also succeeds after the merge. An example commit graph generated using this merge method:
|
||||
|
||||
![Commit graph for merge commit with semi-linear history](../img/merge_method_merge_commit_with_semi_linear_history_v15_0.png)
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit id: "Init"
|
||||
branch mr-branch-1
|
||||
commit
|
||||
commit
|
||||
checkout main
|
||||
merge mr-branch-1
|
||||
branch mr-branch-2
|
||||
commit
|
||||
commit
|
||||
checkout main
|
||||
merge mr-branch-2
|
||||
commit
|
||||
branch squash-mr
|
||||
commit id: "Squashed commits"
|
||||
checkout main
|
||||
merge squash-mr
|
||||
```
|
||||
|
||||
When you visit the merge request page with `Merge commit with semi-linear history`
|
||||
method selected, you can accept it **only if a fast-forward merge is possible**.
|
||||
|
@ -63,7 +100,14 @@ fast-forward merge requests, you can retain a linear Git history and a way
|
|||
to accept merge requests without creating merge commits. An example commit graph
|
||||
generated using this merge method:
|
||||
|
||||
![Commit graph for fast-forward merge](../img/merge_method_ff_v15_0.png)
|
||||
```mermaid
|
||||
gitGraph
|
||||
commit id: "Init"
|
||||
commit id: "Merge mr-branch-1"
|
||||
commit id: "Merge mr-branch-2"
|
||||
commit id: "Commit on main"
|
||||
commit id: "Merge squash-mr"
|
||||
```
|
||||
|
||||
This method is equivalent to `git merge --ff <source-branch>` for regular merges, and to
|
||||
`git merge -squash <source-branch>` for squash merges.
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Gitlab
|
||||
module Ci
|
||||
module Reports
|
||||
class TestReports
|
||||
class TestReport
|
||||
attr_reader :test_suites
|
||||
|
||||
def initialize
|
|
@ -9,7 +9,7 @@ module Gitlab
|
|||
attr_reader :base_reports, :head_reports
|
||||
|
||||
def initialize(base_reports, head_reports)
|
||||
@base_reports = base_reports || TestReports.new
|
||||
@base_reports = base_reports || TestReport.new
|
||||
@head_reports = head_reports
|
||||
end
|
||||
|
||||
|
|
|
@ -578,3 +578,4 @@ zentao_tracker_data: :gitlab_main
|
|||
dingtalk_tracker_data: :gitlab_main
|
||||
zoom_meetings: :gitlab_main
|
||||
batched_background_migration_job_transition_logs: :gitlab_shared
|
||||
user_namespace_callouts: :gitlab_main
|
||||
|
|
|
@ -14,7 +14,7 @@ module Gitlab
|
|||
def jobs_by_migration_name
|
||||
Gitlab::Database::BackgroundMigration::BatchedMigration
|
||||
.executable
|
||||
.created_after(2.days.ago) # Simple way to exclude migrations already running before migration testing
|
||||
.created_after(3.hours.ago) # Simple way to exclude migrations already running before migration testing
|
||||
.to_h do |migration|
|
||||
batching_strategy = migration.batch_class.new(connection: connection)
|
||||
|
||||
|
|
|
@ -6,9 +6,6 @@ module Gitlab
|
|||
URL_REGEX = %r{https?://[^'" ]+}.freeze
|
||||
GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze
|
||||
REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze
|
||||
VALID_LINK_ATTRIBUTES = %w[href rel target].freeze
|
||||
|
||||
include ActionView::Helpers::SanitizeHelper
|
||||
|
||||
class_attribute :file_type
|
||||
|
||||
|
@ -65,10 +62,9 @@ module Gitlab
|
|||
end
|
||||
|
||||
def link_tag(name, url)
|
||||
sanitize(
|
||||
%{<a href="#{ERB::Util.html_escape_once(url)}" rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>},
|
||||
attributes: VALID_LINK_ATTRIBUTES
|
||||
)
|
||||
href_attribute = %{href="#{ERB::Util.html_escape_once(url)}" } if Gitlab::UrlSanitizer.valid_web?(url)
|
||||
|
||||
%{<a #{href_attribute}rel="nofollow noreferrer noopener" target="_blank">#{ERB::Util.html_escape_once(name)}</a>}.html_safe
|
||||
end
|
||||
|
||||
# Links package names based on regex.
|
||||
|
|
|
@ -128,6 +128,9 @@ module Gitlab
|
|||
@loaded_size = @data.bytesize if @data
|
||||
@loaded_all_data = @loaded_size == size
|
||||
|
||||
# Recalculate binary status if we loaded all data
|
||||
@binary = nil if @loaded_all_data
|
||||
|
||||
record_metric_blob_size
|
||||
record_metric_truncated(truncated?)
|
||||
end
|
||||
|
|
|
@ -5,6 +5,8 @@ module Gitlab
|
|||
module Keyset
|
||||
class CursorBasedRequestContext
|
||||
DEFAULT_SORT_DIRECTION = :desc
|
||||
DEFAULT_SORT_COLUMN = :id
|
||||
|
||||
attr_reader :request_context
|
||||
|
||||
delegate :params, to: :request_context
|
||||
|
@ -28,7 +30,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def order_by
|
||||
{ params[:order_by].to_sym => params[:sort]&.to_sym || DEFAULT_SORT_DIRECTION }
|
||||
{ (params[:order_by]&.to_sym || DEFAULT_SORT_COLUMN) => (params[:sort]&.to_sym || DEFAULT_SORT_DIRECTION) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19590,6 +19590,9 @@ msgstr ""
|
|||
msgid "If no options are selected, only administrators can register runners."
|
||||
msgstr ""
|
||||
|
||||
msgid "If none of the options work, try contacting a GitLab administrator."
|
||||
msgstr ""
|
||||
|
||||
msgid "If the number of active users exceeds the user limit, you will be charged for the number of %{users_over_license_link} at your next license reconciliation."
|
||||
msgstr ""
|
||||
|
||||
|
@ -26491,6 +26494,9 @@ msgstr ""
|
|||
msgid "Notes|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options."
|
||||
msgstr ""
|
||||
|
||||
msgid "Note|The created date provided is too far in the past."
|
||||
msgstr ""
|
||||
|
||||
msgid "Nothing to preview."
|
||||
msgstr ""
|
||||
|
||||
|
@ -31990,6 +31996,9 @@ msgstr ""
|
|||
msgid "Reconfigure"
|
||||
msgstr ""
|
||||
|
||||
msgid "Recover password"
|
||||
msgstr ""
|
||||
|
||||
msgid "Recovery Codes"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36255,6 +36264,9 @@ msgstr ""
|
|||
msgid "Sign-in count:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sign-in failed because %{error}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Sign-in page"
|
||||
msgstr ""
|
||||
|
||||
|
@ -36264,6 +36276,9 @@ msgstr ""
|
|||
msgid "Sign-in text"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sign-in using %{provider} auth failed"
|
||||
msgstr ""
|
||||
|
||||
msgid "Sign-out page URL"
|
||||
msgstr ""
|
||||
|
||||
|
@ -41103,6 +41118,9 @@ msgstr ""
|
|||
msgid "Try grouping with different labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "Try logging in using your username or email. If you have forgotten your password, try recovering it"
|
||||
msgstr ""
|
||||
|
||||
msgid "Try out GitLab Pipelines"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44021,6 +44039,9 @@ msgstr ""
|
|||
msgid "WorkItem|Child items"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Child removed"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Closed"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44051,6 +44072,9 @@ msgstr ""
|
|||
msgid "WorkItem|Open"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Remove"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Select type"
|
||||
msgstr ""
|
||||
|
||||
|
@ -44084,6 +44108,9 @@ msgstr ""
|
|||
msgid "WorkItem|Type"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Undo"
|
||||
msgstr ""
|
||||
|
||||
msgid "WorkItem|Work Items"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -29,5 +29,9 @@ FactoryBot.define do
|
|||
trait :with_push_branch_filter do
|
||||
push_events_branch_filter { 'my-branch-*' }
|
||||
end
|
||||
|
||||
trait :permanently_disabled do
|
||||
recent_failures { WebHook::FAILURE_THRESHOLD + 1 }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :namespace_callout, class: 'Users::NamespaceCallout' do
|
||||
feature_name { :invite_members_banner }
|
||||
|
||||
user
|
||||
namespace
|
||||
end
|
||||
end
|
|
@ -21,7 +21,7 @@ RSpec.describe "Admin Runners" do
|
|||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:project) { create(:project, namespace: namespace, creator: user) }
|
||||
|
||||
context "runners registration" do
|
||||
describe "runners registration" do
|
||||
before do
|
||||
visit admin_runners_path
|
||||
end
|
||||
|
@ -164,7 +164,9 @@ RSpec.describe "Admin Runners" do
|
|||
end
|
||||
|
||||
describe 'filter by status' do
|
||||
let!(:never_contacted) { create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil) }
|
||||
let!(:never_contacted) do
|
||||
create(:ci_runner, :instance, description: 'runner-never-contacted', contacted_at: nil)
|
||||
end
|
||||
|
||||
before do
|
||||
create(:ci_runner, :instance, description: 'runner-1', contacted_at: Time.zone.now)
|
||||
|
@ -326,13 +328,15 @@ RSpec.describe "Admin Runners" do
|
|||
visit admin_runners_path
|
||||
page.within('[data-testid="runner-type-tabs"]') do
|
||||
click_on 'Instance'
|
||||
|
||||
expect(page).to have_link('Instance', class: 'active')
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'shows no runners found'
|
||||
|
||||
it 'shows active tab' do
|
||||
expect(page).to have_link('Instance', class: 'active')
|
||||
end
|
||||
|
||||
it 'shows no runner' do
|
||||
expect(page).not_to have_content 'runner-project'
|
||||
expect(page).not_to have_content 'runner-group'
|
||||
|
@ -402,8 +406,8 @@ RSpec.describe "Admin Runners" do
|
|||
end
|
||||
|
||||
it 'sorts by last contact date' do
|
||||
create(:ci_runner, :instance, description: 'runner-1', created_at: '2018-07-12 15:37', contacted_at: '2018-07-12 15:37')
|
||||
create(:ci_runner, :instance, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37')
|
||||
create(:ci_runner, :instance, description: 'runner-1', contacted_at: '2018-07-12')
|
||||
create(:ci_runner, :instance, description: 'runner-2', contacted_at: '2018-07-13')
|
||||
|
||||
visit admin_runners_path
|
||||
|
||||
|
@ -448,13 +452,13 @@ RSpec.describe "Admin Runners" do
|
|||
it 'updates ACTIVE runner status to paused=false' do
|
||||
visit admin_runners_path('status[]': 'ACTIVE')
|
||||
|
||||
expect(page).to have_current_path(admin_runners_path('paused[]': 'false') )
|
||||
expect(page).to have_current_path(admin_runners_path('paused[]': 'false'))
|
||||
end
|
||||
|
||||
it 'updates PAUSED runner status to paused=true' do
|
||||
visit admin_runners_path('status[]': 'PAUSED')
|
||||
|
||||
expect(page).to have_current_path(admin_runners_path('paused[]': 'true') )
|
||||
expect(page).to have_current_path(admin_runners_path('paused[]': 'true'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -477,7 +481,9 @@ RSpec.describe "Admin Runners" do
|
|||
describe 'runner show page breadcrumbs' do
|
||||
it 'contains the current runner id and token' do
|
||||
page.within '[data-testid="breadcrumb-links"]' do
|
||||
expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_link("##{runner.id} (#{runner.short_sha})")
|
||||
expect(page.find('[data-testid="breadcrumb-current-link"]')).to have_link(
|
||||
"##{runner.id} (#{runner.short_sha})"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -515,16 +521,16 @@ RSpec.describe "Admin Runners" do
|
|||
|
||||
describe "Runner edit page" do
|
||||
let(:runner) { create(:ci_runner, :project) }
|
||||
let!(:project1) { create(:project) }
|
||||
let!(:project2) { create(:project) }
|
||||
|
||||
before do
|
||||
@project1 = create(:project)
|
||||
@project2 = create(:project)
|
||||
visit edit_admin_runner_path(runner)
|
||||
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
describe 'runner edit page breadcrumbs' do
|
||||
describe 'breadcrumbs' do
|
||||
it 'contains the current runner id and token' do
|
||||
page.within '[data-testid="breadcrumb-links"]' do
|
||||
expect(page).to have_link("##{runner.id} (#{runner.short_sha})")
|
||||
|
@ -539,7 +545,7 @@ RSpec.describe "Admin Runners" do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'when a runner is updated', :js do
|
||||
context 'when a runner is updated', :js do
|
||||
before do
|
||||
click_on _('Save changes')
|
||||
wait_for_requests
|
||||
|
@ -556,21 +562,21 @@ RSpec.describe "Admin Runners" do
|
|||
|
||||
describe 'projects' do
|
||||
it 'contains project names' do
|
||||
expect(page).to have_content(@project1.full_name)
|
||||
expect(page).to have_content(@project2.full_name)
|
||||
expect(page).to have_content(project1.full_name)
|
||||
expect(page).to have_content(project2.full_name)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'search' do
|
||||
before do
|
||||
search_form = find('#runner-projects-search')
|
||||
search_form.fill_in 'search', with: @project1.name
|
||||
search_form.fill_in 'search', with: project1.name
|
||||
search_form.click_button 'Search'
|
||||
end
|
||||
|
||||
it 'contains name of correct project' do
|
||||
expect(page).to have_content(@project1.full_name)
|
||||
expect(page).not_to have_content(@project2.full_name)
|
||||
expect(page).to have_content(project1.full_name)
|
||||
expect(page).not_to have_content(project2.full_name)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -584,12 +590,12 @@ RSpec.describe "Admin Runners" do
|
|||
assigned_project = page.find('[data-testid="assigned-projects"]')
|
||||
|
||||
expect(page).to have_content('Runner assigned to project.')
|
||||
expect(assigned_project).to have_content(@project2.path)
|
||||
expect(assigned_project).to have_content(project2.path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with specific runner' do
|
||||
let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
|
||||
let(:runner) { create(:ci_runner, :project, projects: [project1]) }
|
||||
|
||||
before do
|
||||
visit edit_admin_runner_path(runner)
|
||||
|
@ -599,7 +605,7 @@ RSpec.describe "Admin Runners" do
|
|||
end
|
||||
|
||||
context 'with locked runner' do
|
||||
let(:runner) { create(:ci_runner, :project, projects: [@project1], locked: true) }
|
||||
let(:runner) { create(:ci_runner, :project, projects: [project1], locked: true) }
|
||||
|
||||
before do
|
||||
visit edit_admin_runner_path(runner)
|
||||
|
@ -610,7 +616,7 @@ RSpec.describe "Admin Runners" do
|
|||
end
|
||||
|
||||
describe 'disable/destroy' do
|
||||
let(:runner) { create(:ci_runner, :project, projects: [@project1]) }
|
||||
let(:runner) { create(:ci_runner, :project, projects: [project1]) }
|
||||
|
||||
before do
|
||||
visit edit_admin_runner_path(runner)
|
||||
|
@ -624,7 +630,7 @@ RSpec.describe "Admin Runners" do
|
|||
new_runner_project = page.find('[data-testid="unassigned-projects"]')
|
||||
|
||||
expect(page).to have_content('Runner unassigned from project.')
|
||||
expect(new_runner_project).to have_content(@project1.path)
|
||||
expect(new_runner_project).to have_content(project1.path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe "Group Runners" do
|
|||
describe "Group runners page", :js do
|
||||
let!(:group_registration_token) { group.runners_token }
|
||||
|
||||
context "runners registration" do
|
||||
describe "runners registration" do
|
||||
before do
|
||||
visit group_runners_path(group)
|
||||
end
|
||||
|
@ -128,7 +128,7 @@ RSpec.describe "Group Runners" do
|
|||
end
|
||||
end
|
||||
|
||||
context 'filtered search' do
|
||||
describe 'filtered search' do
|
||||
before do
|
||||
visit group_runners_path(group)
|
||||
end
|
||||
|
|
|
@ -591,14 +591,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when a new failures exists' do
|
||||
let(:base_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_success)
|
||||
end
|
||||
end
|
||||
|
||||
let(:head_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_failed)
|
||||
end
|
||||
|
@ -639,14 +639,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when an existing failure exists' do
|
||||
let(:base_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_failed)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_success)
|
||||
end
|
||||
end
|
||||
|
||||
let(:head_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_failed)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_success)
|
||||
end
|
||||
|
@ -686,14 +686,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when a resolved failure exists' do
|
||||
let(:base_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_failed)
|
||||
end
|
||||
end
|
||||
|
||||
let(:head_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_success)
|
||||
end
|
||||
|
@ -732,14 +732,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when a new error exists' do
|
||||
let(:base_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_success)
|
||||
end
|
||||
end
|
||||
|
||||
let(:head_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_error)
|
||||
end
|
||||
|
@ -779,14 +779,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when an existing error exists' do
|
||||
let(:base_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_error)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_success)
|
||||
end
|
||||
end
|
||||
|
||||
let(:head_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_error)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_success)
|
||||
end
|
||||
|
@ -825,14 +825,14 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'when a resolved error exists' do
|
||||
let(:base_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_error)
|
||||
end
|
||||
end
|
||||
|
||||
let(:head_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
reports.get_suite('rspec').add_test_case(create_test_case_rspec_success)
|
||||
reports.get_suite('junit').add_test_case(create_test_case_java_success)
|
||||
end
|
||||
|
@ -871,7 +871,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
|
||||
context 'properly truncates the report' do
|
||||
let(:base_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
10.times do |index|
|
||||
reports.get_suite('rspec').add_test_case(
|
||||
create_test_case_rspec_failed(index))
|
||||
|
@ -882,7 +882,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do
|
|||
end
|
||||
|
||||
let(:head_reports) do
|
||||
Gitlab::Ci::Reports::TestReports.new.tap do |reports|
|
||||
Gitlab::Ci::Reports::TestReport.new.tap do |reports|
|
||||
10.times do |index|
|
||||
reports.get_suite('rspec').add_test_case(
|
||||
create_test_case_rspec_failed(index))
|
||||
|
|
|
@ -464,6 +464,20 @@ RSpec.describe 'File blob', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'binary file that appears to be text in the first 1024 bytes' do
|
||||
before do
|
||||
visit_blob('encoding/binary-1.bin', ref: 'binary-encoding')
|
||||
end
|
||||
|
||||
it 'displays the blob' do
|
||||
expect(page).to have_link('Download (23.81 KiB)')
|
||||
# does not show a viewer switcher
|
||||
expect(page).not_to have_selector('.js-blob-viewer-switcher')
|
||||
expect(page).not_to have_selector('.js-copy-blob-source-btn:not(.disabled)')
|
||||
expect(page).not_to have_link('Open raw')
|
||||
end
|
||||
end
|
||||
|
||||
context 'empty file' do
|
||||
before do
|
||||
project.add_maintainer(project.creator)
|
||||
|
|
|
@ -169,8 +169,8 @@ RSpec.describe 'Diff file viewer', :js, :with_clean_rails_cache do
|
|||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'shows there is no preview' do
|
||||
expect(page).to have_content('No preview for this file type')
|
||||
it 'shows that file was added' do
|
||||
expect(page).to have_content('File added')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,215 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResourceLinksBlock with defaults renders correct component 1`] = `
|
||||
<div
|
||||
class="gl-mt-5"
|
||||
id="resource-links"
|
||||
>
|
||||
<div
|
||||
class="card card-slim gl-overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="card-header gl-display-flex gl-justify-content-space-between panel-empty-heading border-bottom-0"
|
||||
>
|
||||
<h3
|
||||
class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
|
||||
>
|
||||
<a
|
||||
aria-hidden="true"
|
||||
class="gl-link anchor position-absolute gl-text-decoration-none"
|
||||
href="#resource-links"
|
||||
id="user-content-resource-links"
|
||||
/>
|
||||
Linked resources
|
||||
<a
|
||||
aria-label="Read more about linked resources"
|
||||
class="gl-link gl-display-flex gl-align-items-center gl-ml-2 gl-text-gray-500"
|
||||
data-testid="help-link"
|
||||
href="/help/user/project/issues/linked_resources"
|
||||
rel="noopener"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-icon s12"
|
||||
data-testid="question-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#question"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<div
|
||||
class="gl-display-inline-flex"
|
||||
>
|
||||
<div
|
||||
class="gl-display-inline-flex gl-mx-5"
|
||||
>
|
||||
<span
|
||||
class="gl-display-inline-flex gl-align-items-center"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-mr-2 gl-text-gray-500 gl-icon s16"
|
||||
data-testid="link-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#link"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
0
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-label="Add a resource link"
|
||||
class="btn btn-default btn-md gl-button btn-icon"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon gl-icon s16"
|
||||
data-testid="plus-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#plus"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!---->
|
||||
</button>
|
||||
</div>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="linked-issues-card-body bg-gray-light"
|
||||
>
|
||||
<div
|
||||
class="card-body bordered-box gl-bg-white"
|
||||
style="display: none;"
|
||||
>
|
||||
<form>
|
||||
<fieldset
|
||||
aria-describedby=""
|
||||
class="form-group gl-form-group"
|
||||
id="__BVID__14"
|
||||
>
|
||||
<legend
|
||||
class="bv-no-focus-ring col-form-label pt-0 col-form-label"
|
||||
id="__BVID__14__BV_label_"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
||||
Text (Optional)
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</legend>
|
||||
<div
|
||||
aria-labelledby="__BVID__14__BV_label_"
|
||||
class="bv-no-focus-ring"
|
||||
role="group"
|
||||
tabindex="-1"
|
||||
>
|
||||
<input
|
||||
class="gl-form-input form-control"
|
||||
data-testid="link-text-input"
|
||||
id="__BVID__16"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset
|
||||
aria-describedby=""
|
||||
class="form-group gl-form-group"
|
||||
id="__BVID__18"
|
||||
>
|
||||
<legend
|
||||
class="bv-no-focus-ring col-form-label pt-0 col-form-label"
|
||||
id="__BVID__18__BV_label_"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
||||
Link
|
||||
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
</legend>
|
||||
<div
|
||||
aria-labelledby="__BVID__18__BV_label_"
|
||||
class="bv-no-focus-ring"
|
||||
role="group"
|
||||
tabindex="-1"
|
||||
>
|
||||
<input
|
||||
class="gl-form-input form-control"
|
||||
data-testid="link-value-input"
|
||||
id="__BVID__20"
|
||||
type="text"
|
||||
/>
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div
|
||||
class="gl-mt-5 gl-clearfix"
|
||||
>
|
||||
<button
|
||||
class="btn gl-float-left btn-confirm btn-md disabled gl-button"
|
||||
data-testid="add-button"
|
||||
disabled="disabled"
|
||||
type="submit"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Add
|
||||
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn gl-float-right btn-default btn-md gl-button"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<!---->
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
|
||||
Cancel
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -1,61 +0,0 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import AddIssuableResourceLinkForm from '~/linked_resources/components/add_issuable_resource_link_form.vue';
|
||||
|
||||
describe('AddIssuableResourceLinkForm', () => {
|
||||
let wrapper;
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = mountExtended(AddIssuableResourceLinkForm);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
const findAddButton = () => wrapper.findByTestId('add-button');
|
||||
const findCancelButton = () => wrapper.findByText('Cancel');
|
||||
const findLinkTextInput = () => wrapper.findByTestId('link-text-input');
|
||||
const findLinkValueInput = () => wrapper.findByTestId('link-value-input');
|
||||
|
||||
const cancelForm = async () => {
|
||||
await findCancelButton().trigger('click');
|
||||
};
|
||||
|
||||
describe('cancel form button', () => {
|
||||
const closeFormEvent = { 'add-issuable-resource-link-form-cancel': [[]] };
|
||||
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('should close the form on cancel', async () => {
|
||||
await cancelForm();
|
||||
|
||||
expect(wrapper.emitted()).toEqual(closeFormEvent);
|
||||
});
|
||||
|
||||
it('keeps the button disabled without input', () => {
|
||||
expect(findAddButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('keeps the button disabled with only text input', async () => {
|
||||
findLinkTextInput().setValue('link text');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findAddButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('enables add button when link input is provided', async () => {
|
||||
findLinkTextInput().setValue('link text');
|
||||
findLinkValueInput().setValue('https://foo.example.com');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(findAddButton().props('disabled')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,90 +0,0 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ResourceLinksBlock from '~/linked_resources/components/resource_links_block.vue';
|
||||
import AddIssuableResourceLinkForm from '~/linked_resources/components/add_issuable_resource_link_form.vue';
|
||||
|
||||
describe('ResourceLinksBlock', () => {
|
||||
let wrapper;
|
||||
|
||||
const findResourceLinkAddButton = () => wrapper.find(GlButton);
|
||||
const resourceLinkForm = () => wrapper.findComponent(AddIssuableResourceLinkForm);
|
||||
const helpPath = '/help/user/project/issues/linked_resources';
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = mountExtended(ResourceLinksBlock, {
|
||||
propsData: {
|
||||
helpPath,
|
||||
canAddResourceLinks: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFormVisible: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe('with defaults', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('renders correct component', () => {
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should show the form when add button is clicked', async () => {
|
||||
await findResourceLinkAddButton().trigger('click');
|
||||
|
||||
expect(resourceLinkForm().isVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('should hide the form when the hide event is emitted', async () => {
|
||||
// open the form
|
||||
await findResourceLinkAddButton().trigger('click');
|
||||
|
||||
await resourceLinkForm().vm.$emit('add-issuable-resource-link-form-cancel');
|
||||
|
||||
expect(resourceLinkForm().isVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with canAddResourceLinks=false', () => {
|
||||
it('does not show the add button', () => {
|
||||
wrapper = shallowMount(ResourceLinksBlock, {
|
||||
propsData: {
|
||||
canAddResourceLinks: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(findResourceLinkAddButton().exists()).toBe(false);
|
||||
expect(resourceLinkForm().isVisible()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with isFormVisible=true', () => {
|
||||
it('renders the form with correct props', () => {
|
||||
wrapper = shallowMount(ResourceLinksBlock, {
|
||||
propsData: {
|
||||
canAddResourceLinks: true,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFormVisible: true,
|
||||
isSubmitting: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(resourceLinkForm().exists()).toBe(true);
|
||||
expect(resourceLinkForm().props('isSubmitting')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -109,5 +109,17 @@ describe('cleanup_status', () => {
|
|||
expect(findPopover().findComponent(GlLink).exists()).toBe(true);
|
||||
expect(findPopover().findComponent(GlLink).attributes('href')).toBe(cleanupPolicyHelpPage);
|
||||
});
|
||||
|
||||
it('id matches popover target attribute', () => {
|
||||
mountComponent({
|
||||
status: UNFINISHED_STATUS,
|
||||
next_run_at: '2063-04-08T01:44:03Z',
|
||||
});
|
||||
|
||||
const id = findExtraInfoIcon().attributes('id');
|
||||
|
||||
expect(id).toMatch(/status-info-[0-9]+/);
|
||||
expect(findPopover().props('target')).toEqual(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
import Vue from 'vue';
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
|
||||
import changeWorkItemParentMutation from '~/work_items/graphql/change_work_item_parent_link.mutation.graphql';
|
||||
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
|
||||
import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants';
|
||||
import { workItemHierarchyResponse, changeWorkItemParentMutationResponse } from '../../mock_data';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
|
||||
const PARENT_ID = 'gid://gitlab/WorkItem/1';
|
||||
const WORK_ITEM_ID = 'gid://gitlab/WorkItem/3';
|
||||
|
||||
describe('WorkItemLinksMenu', () => {
|
||||
let wrapper;
|
||||
let mockApollo;
|
||||
|
||||
const $toast = {
|
||||
show: jest.fn(),
|
||||
};
|
||||
|
||||
const createComponent = async ({
|
||||
data = {},
|
||||
mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse),
|
||||
} = {}) => {
|
||||
mockApollo = createMockApollo([
|
||||
[getWorkItemLinksQuery, jest.fn().mockResolvedValue(workItemHierarchyResponse)],
|
||||
[changeWorkItemParentMutation, mutationHandler],
|
||||
]);
|
||||
|
||||
mockApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: getWorkItemLinksQuery,
|
||||
variables: {
|
||||
id: PARENT_ID,
|
||||
},
|
||||
data: workItemHierarchyResponse.data,
|
||||
});
|
||||
|
||||
wrapper = shallowMountExtended(WorkItemLinksMenu, {
|
||||
data() {
|
||||
return {
|
||||
...data,
|
||||
};
|
||||
},
|
||||
propsData: {
|
||||
workItemId: WORK_ITEM_ID,
|
||||
parentWorkItemId: PARENT_ID,
|
||||
},
|
||||
apolloProvider: mockApollo,
|
||||
mocks: {
|
||||
$toast,
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
};
|
||||
|
||||
const findDropdown = () => wrapper.find(GlDropdown);
|
||||
const findRemoveDropdownItem = () => wrapper.find(GlDropdownItem);
|
||||
|
||||
beforeEach(async () => {
|
||||
await createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mockApollo = null;
|
||||
});
|
||||
|
||||
it('renders dropdown and dropdown items', () => {
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
expect(findRemoveDropdownItem().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls correct mutation with correct variables', async () => {
|
||||
const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
|
||||
|
||||
createComponent({ mutationHandler });
|
||||
|
||||
findRemoveDropdownItem().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(mutationHandler).toHaveBeenCalledWith({
|
||||
id: WORK_ITEM_ID,
|
||||
parentId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows toast when mutation succeeds', async () => {
|
||||
const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
|
||||
|
||||
createComponent({ mutationHandler });
|
||||
|
||||
findRemoveDropdownItem().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect($toast.show).toHaveBeenCalledWith('Child removed', {
|
||||
action: { onClick: expect.anything(), text: 'Undo' },
|
||||
});
|
||||
});
|
||||
|
||||
it('updates the cache when mutation succeeds', async () => {
|
||||
const mutationHandler = jest.fn().mockResolvedValue(changeWorkItemParentMutationResponse);
|
||||
|
||||
createComponent({ mutationHandler });
|
||||
|
||||
mockApollo.clients.defaultClient.cache.readQuery = jest.fn(
|
||||
() => workItemHierarchyResponse.data,
|
||||
);
|
||||
|
||||
mockApollo.clients.defaultClient.cache.writeQuery = jest.fn();
|
||||
|
||||
findRemoveDropdownItem().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
// Remove the work item from parent's children
|
||||
const resp = cloneDeep(workItemHierarchyResponse);
|
||||
const index = resp.data.workItem.widgets
|
||||
.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
|
||||
.children.nodes.findIndex((child) => child.id === WORK_ITEM_ID);
|
||||
resp.data.workItem.widgets
|
||||
.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
|
||||
.children.nodes.splice(index, 1);
|
||||
|
||||
expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: expect.anything(),
|
||||
variables: { id: PARENT_ID },
|
||||
data: resp.data,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
|
@ -392,6 +392,25 @@ export const workItemHierarchyResponse = {
|
|||
},
|
||||
};
|
||||
|
||||
export const changeWorkItemParentMutationResponse = {
|
||||
data: {
|
||||
workItemUpdate: {
|
||||
workItem: {
|
||||
id: 'gid://gitlab/WorkItem/2',
|
||||
workItemType: {
|
||||
id: 'gid://gitlab/WorkItems::Type/5',
|
||||
__typename: 'WorkItemType',
|
||||
},
|
||||
title: 'Foo',
|
||||
state: 'OPEN',
|
||||
__typename: 'WorkItem',
|
||||
},
|
||||
errors: [],
|
||||
__typename: 'WorkItemUpdatePayload',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const availableWorkItemsResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
|
|
|
@ -222,4 +222,60 @@ RSpec.describe Users::CalloutsHelper do
|
|||
it { is_expected.to be true }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#web_hook_disabled_dismissed?' do
|
||||
context 'without a project' do
|
||||
it 'is false' do
|
||||
expect(helper).not_to be_web_hook_disabled_dismissed(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a project' do
|
||||
let_it_be(:project) { create(:project) }
|
||||
|
||||
context 'the web-hook failure callout has never been dismissed' do
|
||||
it 'is false' do
|
||||
expect(helper).not_to be_web_hook_disabled_dismissed(project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'the web-hook failure callout has been dismissed', :freeze_time do
|
||||
before do
|
||||
create(:namespace_callout,
|
||||
feature_name: described_class::WEB_HOOK_DISABLED,
|
||||
user: user,
|
||||
namespace: project.namespace,
|
||||
dismissed_at: 1.week.ago)
|
||||
end
|
||||
|
||||
it 'is true' do
|
||||
expect(helper).to be_web_hook_disabled_dismissed(project)
|
||||
end
|
||||
|
||||
context 'when there was an older failure', :clean_gitlab_redis_shared_state do
|
||||
let(:key) { "web_hooks:last_failure:project-#{project.id}" }
|
||||
|
||||
before do
|
||||
Gitlab::Redis::SharedState.with { |r| r.set(key, 1.month.ago.iso8601) }
|
||||
end
|
||||
|
||||
it 'is true' do
|
||||
expect(helper).to be_web_hook_disabled_dismissed(project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there has been a more recent failure', :clean_gitlab_redis_shared_state do
|
||||
let(:key) { "web_hooks:last_failure:project-#{project.id}" }
|
||||
|
||||
before do
|
||||
Gitlab::Redis::SharedState.with { |r| r.set(key, 1.day.ago.iso8601) }
|
||||
end
|
||||
|
||||
it 'is false' do
|
||||
expect(helper).not_to be_web_hook_disabled_dismissed(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe WebHooks::WebHooksHelper do
|
||||
let_it_be_with_reload(:project) { create(:project) }
|
||||
|
||||
let(:current_user) { nil }
|
||||
let(:callout_dismissed) { false }
|
||||
let(:web_hooks_disable_failed) { false }
|
||||
let(:webhooks_failed_callout) { false }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:current_user).and_return(current_user)
|
||||
allow(helper).to receive(:web_hook_disabled_dismissed?).with(project).and_return(callout_dismissed)
|
||||
|
||||
stub_feature_flags(
|
||||
webhooks_failed_callout: webhooks_failed_callout,
|
||||
web_hooks_disable_failed: web_hooks_disable_failed
|
||||
)
|
||||
end
|
||||
|
||||
shared_context 'user is logged in' do
|
||||
let(:current_user) { create(:user) }
|
||||
end
|
||||
|
||||
shared_context 'webhooks_failed_callout is enabled' do
|
||||
let(:webhooks_failed_callout) { true }
|
||||
end
|
||||
|
||||
shared_context 'webhooks_failed_callout is enabled for this project' do
|
||||
let(:webhooks_failed_callout) { project }
|
||||
end
|
||||
|
||||
shared_context 'web_hooks_disable_failed is enabled' do
|
||||
let(:web_hooks_disable_failed) { true }
|
||||
end
|
||||
|
||||
shared_context 'web_hooks_disable_failed is enabled for this project' do
|
||||
let(:web_hooks_disable_failed) { project }
|
||||
end
|
||||
|
||||
shared_context 'the user has permission' do
|
||||
before do
|
||||
project.add_maintainer(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
shared_context 'the user dismissed the callout' do
|
||||
let(:callout_dismissed) { true }
|
||||
end
|
||||
|
||||
shared_context 'a hook has failed' do
|
||||
before do
|
||||
create(:project_hook, :permanently_disabled, project: project)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#show_project_hook_failed_callout?' do
|
||||
context 'all conditions are met' do
|
||||
include_context 'user is logged in'
|
||||
include_context 'webhooks_failed_callout is enabled'
|
||||
include_context 'web_hooks_disable_failed is enabled'
|
||||
include_context 'the user has permission'
|
||||
include_context 'a hook has failed'
|
||||
|
||||
it 'is true' do
|
||||
expect(helper).to be_show_project_hook_failed_callout(project: project)
|
||||
end
|
||||
|
||||
it 'caches the DB calls until the TTL', :use_clean_rails_memory_store_caching, :request_store do
|
||||
helper.show_project_hook_failed_callout?(project: project)
|
||||
|
||||
travel_to((described_class::EXPIRY_TTL - 1.second).from_now) do
|
||||
expect do
|
||||
helper.show_project_hook_failed_callout?(project: project)
|
||||
end.not_to exceed_query_limit(0)
|
||||
end
|
||||
|
||||
travel_to((described_class::EXPIRY_TTL + 1.second).from_now) do
|
||||
expect do
|
||||
helper.show_project_hook_failed_callout?(project: project)
|
||||
end.to exceed_query_limit(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'all conditions are met, project scoped flags' do
|
||||
include_context 'user is logged in'
|
||||
include_context 'webhooks_failed_callout is enabled for this project'
|
||||
include_context 'web_hooks_disable_failed is enabled for this project'
|
||||
include_context 'the user has permission'
|
||||
include_context 'a hook has failed'
|
||||
|
||||
it 'is true' do
|
||||
expect(helper).to be_show_project_hook_failed_callout(project: project)
|
||||
end
|
||||
end
|
||||
|
||||
context 'one condition is not met' do
|
||||
contexts = [
|
||||
'user is logged in',
|
||||
'webhooks_failed_callout is enabled',
|
||||
'web_hooks_disable_failed is enabled',
|
||||
'the user has permission',
|
||||
'a hook has failed'
|
||||
]
|
||||
|
||||
contexts.each do |name|
|
||||
context "namely #{name}" do
|
||||
contexts.each { |ctx| include_context(ctx) unless ctx == name }
|
||||
|
||||
it 'is false' do
|
||||
expect(helper).not_to be_show_project_hook_failed_callout(project: project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Ci::Reports::TestReports do
|
||||
RSpec.describe Gitlab::Ci::Reports::TestReport do
|
||||
include TestReportsHelper
|
||||
|
||||
let(:test_reports) { described_class.new }
|
|
@ -6,8 +6,8 @@ RSpec.describe Gitlab::Ci::Reports::TestReportsComparer do
|
|||
include TestReportsHelper
|
||||
|
||||
let(:comparer) { described_class.new(base_reports, head_reports) }
|
||||
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:base_reports) { Gitlab::Ci::Reports::TestReport.new }
|
||||
let(:head_reports) { Gitlab::Ci::Reports::TestReport.new }
|
||||
|
||||
describe '#suite_comparers' do
|
||||
subject { comparer.suite_comparers }
|
||||
|
|
|
@ -68,10 +68,10 @@ RSpec.describe Gitlab::Database::Migrations::TestBatchedBackgroundRunner, :freez
|
|||
end
|
||||
|
||||
context 'with multiple jobs to run' do
|
||||
it 'runs all jobs created within the last 48 hours' do
|
||||
it 'runs all jobs created within the last 3 hours' do
|
||||
old_migration = define_background_migration(migration_name)
|
||||
|
||||
travel 3.days
|
||||
travel 4.hours
|
||||
|
||||
new_migration = define_background_migration('NewMigration') { travel 1.second }
|
||||
migration.queue_batched_background_migration('NewMigration', table_name, :id,
|
||||
|
|
|
@ -33,7 +33,7 @@ RSpec.describe Gitlab::DependencyLinker::BaseLinker do
|
|||
it 'only converts valid links' do
|
||||
expect(subject).to eq(
|
||||
<<~CONTENT
|
||||
<span><span>#{link('http://')}</span><span>#{link('\n', url: '%5Cn')}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span>
|
||||
<span><span>#{link('http://', url: nil)}</span><span>#{link('\n', url: nil)}</span><span>#{link('javascript:alert(1)', url: nil)}</span></span>
|
||||
<span><span>#{link('https://gitlab.com/gitlab-org/gitlab')}</span></span>
|
||||
CONTENT
|
||||
)
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require_migration!
|
||||
|
||||
RSpec.describe UpdateNotesInPast, :migration do
|
||||
let(:notes) { table(:notes) }
|
||||
|
||||
it 'updates created_at when it is too much in the past' do
|
||||
notes.create!(id: 10, note: 'note', created_at: '2009-06-01')
|
||||
notes.create!(id: 11, note: 'note', created_at: '1970-01-01')
|
||||
notes.create!(id: 12, note: 'note', created_at: '1600-06-01')
|
||||
|
||||
migrate!
|
||||
|
||||
expect(notes.all).to contain_exactly(
|
||||
an_object_having_attributes(id: 10, created_at: DateTime.parse('2009-06-01')),
|
||||
an_object_having_attributes(id: 11, created_at: DateTime.parse('1970-01-01')),
|
||||
an_object_having_attributes(id: 12, created_at: DateTime.parse('1970-01-01'))
|
||||
)
|
||||
end
|
||||
end
|
|
@ -4281,7 +4281,7 @@ RSpec.describe Ci::Build do
|
|||
describe '#collect_test_reports!' do
|
||||
subject { build.collect_test_reports!(test_reports) }
|
||||
|
||||
let(:test_reports) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:test_reports) { Gitlab::Ci::Reports::TestReport.new }
|
||||
|
||||
it { expect(test_reports.get_suite(build.name).total_count).to eq(0) }
|
||||
|
||||
|
@ -4332,7 +4332,7 @@ RSpec.describe Ci::Build do
|
|||
|
||||
context 'when build is part of parallel build' do
|
||||
let(:build_1) { create(:ci_build, name: 'build 1/2') }
|
||||
let(:test_report) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:test_report) { Gitlab::Ci::Reports::TestReport.new }
|
||||
|
||||
before do
|
||||
build_1.collect_test_reports!(test_report)
|
||||
|
@ -4356,7 +4356,7 @@ RSpec.describe Ci::Build do
|
|||
end
|
||||
|
||||
context 'when build is part of matrix build' do
|
||||
let(:test_report) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:test_report) { Gitlab::Ci::Reports::TestReport.new }
|
||||
let(:matrix_build_1) { create(:ci_build, :matrix) }
|
||||
|
||||
before do
|
||||
|
|
|
@ -15,6 +15,19 @@ RSpec.describe ProjectHook do
|
|||
subject { build(:project_hook, project: create(:project)) }
|
||||
end
|
||||
|
||||
describe '.for_projects' do
|
||||
it 'finds related project hooks' do
|
||||
hook_a = create(:project_hook)
|
||||
hook_b = create(:project_hook)
|
||||
hook_c = create(:project_hook)
|
||||
|
||||
expect(described_class.for_projects([hook_a.project, hook_b.project]))
|
||||
.to contain_exactly(hook_a, hook_b)
|
||||
expect(described_class.for_projects(hook_c.project))
|
||||
.to contain_exactly(hook_c)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.push_hooks' do
|
||||
it 'returns hooks for push events only' do
|
||||
hook = create(:project_hook, push_events: true)
|
||||
|
@ -50,4 +63,62 @@ RSpec.describe ProjectHook do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_last_failure', :clean_gitlab_redis_shared_state do
|
||||
let_it_be(:hook) { create(:project_hook) }
|
||||
|
||||
it 'is a method of this class' do
|
||||
expect { hook.update_last_failure }.not_to raise_error
|
||||
end
|
||||
|
||||
context 'when the hook is executable' do
|
||||
it 'does not update the state' do
|
||||
expect(Gitlab::Redis::SharedState).not_to receive(:with)
|
||||
|
||||
hook.update_last_failure
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the hook is failed' do
|
||||
before do
|
||||
allow(hook).to receive(:executable?).and_return(false)
|
||||
end
|
||||
|
||||
def last_failure
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.get("web_hooks:last_failure:project-#{hook.project.id}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'there is no prior value', :freeze_time do
|
||||
it 'updates the state' do
|
||||
expect { hook.update_last_failure }.to change { last_failure }.to(Time.current)
|
||||
end
|
||||
end
|
||||
|
||||
context 'there is a prior value, from before now' do
|
||||
it 'updates the state' do
|
||||
the_future = 1.minute.from_now
|
||||
|
||||
hook.update_last_failure
|
||||
|
||||
travel_to(the_future) do
|
||||
expect { hook.update_last_failure }.to change { last_failure }.to(the_future.iso8601)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'there is a prior value, from after now' do
|
||||
it 'does not update the state' do
|
||||
the_past = 1.minute.ago
|
||||
|
||||
hook.update_last_failure
|
||||
|
||||
travel_to(the_past) do
|
||||
expect { hook.update_last_failure }.not_to change { last_failure }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -187,8 +187,8 @@ RSpec.describe WebHook do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.executable' do
|
||||
let(:not_executable) do
|
||||
describe '.executable/.disabled' do
|
||||
let!(:not_executable) do
|
||||
[
|
||||
[0, Time.current],
|
||||
[0, 1.minute.from_now],
|
||||
|
@ -202,7 +202,7 @@ RSpec.describe WebHook do
|
|||
end
|
||||
end
|
||||
|
||||
let(:executables) do
|
||||
let!(:executables) do
|
||||
[
|
||||
[0, nil],
|
||||
[0, 1.day.ago],
|
||||
|
@ -217,6 +217,7 @@ RSpec.describe WebHook do
|
|||
|
||||
it 'finds the correct set of project hooks' do
|
||||
expect(described_class.where(project_id: project.id).executable).to match_array executables
|
||||
expect(described_class.where(project_id: project.id).disabled).to match_array not_executable
|
||||
end
|
||||
|
||||
context 'when the feature flag is not enabled' do
|
||||
|
@ -224,7 +225,7 @@ RSpec.describe WebHook do
|
|||
stub_feature_flags(web_hooks_disable_failed: false)
|
||||
end
|
||||
|
||||
it 'is the same as all' do
|
||||
specify 'enabled is the same as all' do
|
||||
expect(described_class.where(project_id: project.id).executable).to match_array(executables + not_executable)
|
||||
end
|
||||
end
|
||||
|
@ -635,4 +636,10 @@ RSpec.describe WebHook do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_last_failure' do
|
||||
it 'is a method of this class' do
|
||||
expect { described_class.new.update_last_failure }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -106,6 +106,22 @@ RSpec.describe Note do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'created_at in the past' do
|
||||
let_it_be(:noteable) { create(:issue) }
|
||||
|
||||
context 'when creating a note not too much in the past' do
|
||||
subject { build(:note, project: noteable.project, noteable: noteable, created_at: '1990-05-06') }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'when creating a note too much in the past' do
|
||||
subject { build(:note, project: noteable.project, noteable: noteable, created_at: '1600-05-06') }
|
||||
|
||||
it { is_expected.not_to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'confidentiality' do
|
||||
context 'for existing public note' do
|
||||
let_it_be(:existing_note) { create(:note) }
|
||||
|
|
|
@ -136,6 +136,7 @@ RSpec.describe User do
|
|||
it { is_expected.to have_many(:timelogs) }
|
||||
it { is_expected.to have_many(:callouts).class_name('Users::Callout') }
|
||||
it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') }
|
||||
it { is_expected.to have_many(:namespace_callouts).class_name('Users::NamespaceCallout') }
|
||||
|
||||
describe '#user_detail' do
|
||||
it 'does not persist `user_detail` by default' do
|
||||
|
@ -6415,6 +6416,96 @@ RSpec.describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'Users::NamespaceCallout' do
|
||||
describe '#dismissed_callout_for_namespace?' do
|
||||
let_it_be(:user, refind: true) { create(:user) }
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first }
|
||||
|
||||
let(:query) do
|
||||
{ feature_name: feature_name, namespace: namespace }
|
||||
end
|
||||
|
||||
def have_dismissed_callout
|
||||
be_dismissed_callout_for_namespace(**query)
|
||||
end
|
||||
|
||||
context 'when no callout dismissal record exists' do
|
||||
it 'returns false when no ignore_dismissal_earlier_than provided' do
|
||||
expect(user).not_to have_dismissed_callout
|
||||
end
|
||||
end
|
||||
|
||||
context 'when dismissed callout exists' do
|
||||
before_all do
|
||||
create(:namespace_callout,
|
||||
user: user,
|
||||
namespace_id: namespace.id,
|
||||
feature_name: feature_name,
|
||||
dismissed_at: 4.months.ago)
|
||||
end
|
||||
|
||||
it 'returns true when no ignore_dismissal_earlier_than provided' do
|
||||
expect(user).to have_dismissed_callout
|
||||
end
|
||||
|
||||
it 'returns true when ignore_dismissal_earlier_than is earlier than dismissed_at' do
|
||||
query[:ignore_dismissal_earlier_than] = 6.months.ago
|
||||
|
||||
expect(user).to have_dismissed_callout
|
||||
end
|
||||
|
||||
it 'returns false when ignore_dismissal_earlier_than is later than dismissed_at' do
|
||||
query[:ignore_dismissal_earlier_than] = 2.months.ago
|
||||
|
||||
expect(user).not_to have_dismissed_callout
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_or_initialize_namespace_callout' do
|
||||
let_it_be(:user, refind: true) { create(:user) }
|
||||
let_it_be(:namespace) { create(:namespace) }
|
||||
let_it_be(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first }
|
||||
|
||||
subject(:callout_with_source) do
|
||||
user.find_or_initialize_namespace_callout(feature_name, namespace.id)
|
||||
end
|
||||
|
||||
context 'when callout exists' do
|
||||
let!(:callout) do
|
||||
create(:namespace_callout, user: user, feature_name: feature_name, namespace_id: namespace.id)
|
||||
end
|
||||
|
||||
it 'returns existing callout' do
|
||||
expect(callout_with_source).to eq(callout)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when callout does not exist' do
|
||||
context 'when feature name is valid' do
|
||||
it 'initializes a new callout' do
|
||||
expect(callout_with_source)
|
||||
.to be_a_new(Users::NamespaceCallout)
|
||||
.and be_valid
|
||||
end
|
||||
end
|
||||
|
||||
context 'when feature name is not valid' do
|
||||
let(:feature_name) { 'notvalid' }
|
||||
|
||||
it 'initializes a new callout' do
|
||||
expect(callout_with_source).to be_a_new(Users::NamespaceCallout)
|
||||
end
|
||||
|
||||
it 'is not valid' do
|
||||
expect(callout_with_source).not_to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#dismissed_callout_for_group?' do
|
||||
let_it_be(:user, refind: true) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Users::NamespaceCallout do
|
||||
let_it_be(:user) { create_default(:user) }
|
||||
let_it_be(:namespace) { create_default(:namespace) }
|
||||
let_it_be(:callout) { create(:namespace_callout) }
|
||||
|
||||
it_behaves_like 'having unique enum values'
|
||||
|
||||
describe 'relationships' do
|
||||
it { is_expected.to belong_to(:namespace) }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:namespace) }
|
||||
it { is_expected.to validate_presence_of(:user) }
|
||||
it { is_expected.to validate_presence_of(:feature_name) }
|
||||
|
||||
specify do
|
||||
is_expected.to validate_uniqueness_of(:feature_name)
|
||||
.scoped_to(:user_id, :namespace_id)
|
||||
.ignoring_case_sensitivity
|
||||
end
|
||||
|
||||
it { is_expected.to allow_value(:web_hook_disabled).for(:feature_name) }
|
||||
|
||||
it 'rejects invalid feature names' do
|
||||
expect { callout.feature_name = :non_existent_feature }.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#source_feature_name' do
|
||||
it 'provides string based off source and feature' do
|
||||
expect(callout.source_feature_name).to eq "#{callout.feature_name}_#{callout.namespace_id}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,8 +7,8 @@ RSpec.describe TestReportsComparerEntity do
|
|||
|
||||
let(:entity) { described_class.new(comparer) }
|
||||
let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) }
|
||||
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:base_reports) { Gitlab::Ci::Reports::TestReport.new }
|
||||
let(:head_reports) { Gitlab::Ci::Reports::TestReport.new }
|
||||
|
||||
describe '#as_json' do
|
||||
subject { entity.as_json }
|
||||
|
|
|
@ -8,8 +8,8 @@ RSpec.describe TestReportsComparerSerializer do
|
|||
let(:project) { double(:project) }
|
||||
let(:serializer) { described_class.new(project: project).represent(comparer) }
|
||||
let(:comparer) { Gitlab::Ci::Reports::TestReportsComparer.new(base_reports, head_reports) }
|
||||
let(:base_reports) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:head_reports) { Gitlab::Ci::Reports::TestReports.new }
|
||||
let(:base_reports) { Gitlab::Ci::Reports::TestReport.new }
|
||||
let(:head_reports) { Gitlab::Ci::Reports::TestReport.new }
|
||||
|
||||
describe '#to_json' do
|
||||
subject { serializer.to_json }
|
||||
|
|
|
@ -35,6 +35,12 @@ RSpec.describe WebHooks::LogExecutionService do
|
|||
expect(WebHookLog.recent.first).to have_attributes(data)
|
||||
end
|
||||
|
||||
it 'updates the last failure' do
|
||||
expect(project_hook).to receive(:update_last_failure)
|
||||
|
||||
service.execute
|
||||
end
|
||||
|
||||
context 'obtaining an exclusive lease' do
|
||||
let(:lease_key) { "web_hooks:update_hook_failure_state:#{project_hook.id}" }
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'errors/omniauth_error' do
|
||||
let(:provider) { FFaker::Product.brand }
|
||||
let(:error) { FFaker::Lorem.sentence }
|
||||
|
||||
before do
|
||||
assign(:provider, provider)
|
||||
assign(:error, error)
|
||||
end
|
||||
|
||||
it 'renders template' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_content(provider)
|
||||
expect(rendered).to have_content(_('Sign-in failed because %{error}.') % { error: error })
|
||||
expect(rendered).to have_link('Sign in')
|
||||
expect(rendered).to have_content(_('If none of the options work, try contacting a GitLab administrator.'))
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue