Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-13 12:07:54 +00:00
parent 0e65189f85
commit 41e8b05e8d
73 changed files with 1125 additions and 749 deletions

View File

@ -9,6 +9,7 @@ import {
GlTab,
GlButton,
} from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { s__ } from '~/locale';
import query from '../graphql/queries/details.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
@ -24,7 +25,7 @@ export default {
errorMsg: s__(
'AlertManagement|There was an error displaying the alert. Please refresh the page to try again.',
),
fullAlertDetailsTitle: s__('AlertManagement|Full Alert Details'),
fullAlertDetailsTitle: s__('AlertManagement|Full alert details'),
overviewTitle: s__('AlertManagement|Overview'),
},
components: {
@ -32,6 +33,7 @@ export default {
GlLoadingIcon,
GlNewDropdown,
GlNewDropdownItem,
timeAgoTooltip,
GlTab,
GlTabs,
GlButton,
@ -108,7 +110,11 @@ export default {
{{ s__('AlertManagement|Create issue') }}
</gl-button>
</div>
<div v-if="alert" class="gl-display-flex justify-content-end">
<div
v-if="alert"
class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
>
<h2 data-testid="title">{{ alert.title }}</h2>
<gl-new-dropdown right>
<gl-new-dropdown-item
v-for="(label, field) in $options.statuses"
@ -121,15 +127,22 @@ export default {
</div>
<gl-tabs v-if="alert" data-testid="alertDetailsTabs">
<gl-tab data-testid="overviewTab" :title="$options.i18n.overviewTitle">
<ul class="pl-3">
<li data-testid="startTimeItem" class="font-weight-bold mb-3 mt-2">
{{ s__('AlertManagement|Start time:') }}
<ul class="pl-4 mb-n1">
<li v-if="alert.startedAt" class="my-2">
<strong class="bold">{{ s__('AlertManagement|Start time') }}:</strong>
<time-ago-tooltip data-testid="startTimeItem" :time="alert.startedAt" />
</li>
<li class="font-weight-bold my-3">
{{ s__('AlertManagement|End time:') }}
<li v-if="alert.eventCount" class="my-2">
<strong class="bold">{{ s__('AlertManagement|Events') }}:</strong>
<span data-testid="eventCount">{{ alert.eventCount }}</span>
</li>
<li class="font-weight-bold my-3">
{{ s__('AlertManagement|Events:') }}
<li v-if="alert.monitoringTool" class="my-2">
<strong class="bold">{{ s__('AlertManagement|Tool') }}:</strong>
<span data-testid="monitoringTool">{{ alert.monitoringTool }}</span>
</li>
<li v-if="alert.service" class="my-2">
<strong class="bold">{{ s__('AlertManagement|Service') }}:</strong>
<span data-testid="service">{{ alert.service }}</span>
</li>
</ul>
</gl-tab>

View File

@ -127,7 +127,7 @@ export default {
},
methods: {
redirectToSnippets() {
window.location.pathname = 'dashboard/snippets';
window.location.pathname = this.snippet.project?.fullPath || 'dashboard/snippets';
},
closeDeleteModal() {
this.$refs.deleteModal.hide();

View File

@ -54,22 +54,23 @@ export default {
'issuable-info-container': !canReorder,
'card-body': canReorder,
}"
class="item-body d-flex align-items-center p-2 p-lg-3 py-xl-2 px-xl-3"
class="item-body d-flex align-items-center py-2 px-3"
>
<div class="item-contents d-flex align-items-center flex-wrap flex-grow-1 flex-xl-nowrap">
<!-- Title area: Status icon (XL) and title -->
<div class="item-title d-flex align-items-center mb-xl-0">
<span ref="iconElementXL">
<div class="item-title d-flex align-items-xl-center mb-xl-0">
<div ref="iconElementXL">
<icon
v-if="hasState"
ref="iconElementXL"
class="mr-2"
:class="iconClass"
:name="iconName"
:size="16"
:title="stateTitle"
:aria-label="state"
/>
</span>
</div>
<gl-tooltip :target="() => $refs.iconElementXL">
<span v-html="stateTitle"></span>
</gl-tooltip>
@ -97,17 +98,6 @@ export default {
<div
class="item-path-area item-path-id d-flex align-items-center mr-2 mt-2 mt-xl-0 ml-xl-2"
>
<span ref="iconElement">
<icon
v-if="hasState"
:class="iconClass"
:name="iconName"
:title="stateTitle"
:aria-label="state"
data-html="true"
class="d-xl-none"
/>
</span>
<gl-tooltip :target="() => this.$refs.iconElement">
<span v-html="stateTitle"></span>
</gl-tooltip>
@ -159,7 +149,7 @@ export default {
v-gl-tooltip
:disabled="removeDisabled"
type="button"
class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button mr-xl-0 align-self-xl-center"
class="btn btn-default btn-svg btn-item-remove js-issue-item-remove-button"
data-qa-selector="remove_related_issue_button"
:title="__('Remove')"
:aria-label="__('Remove')"

View File

@ -1,9 +1,11 @@
$item-path-max-width: 160px;
$item-milestone-max-width: 120px;
$item-weight-max-width: 48px;
$item-remove-button-space: 42px;
.related-items-list {
padding: $gl-padding-4;
padding-right: $gl-padding-6;
&,
.list-item:last-child {
@ -11,16 +13,16 @@ $item-weight-max-width: 48px;
}
}
.sortable-link {
max-width: 85%;
}
.related-items-tree {
.card-header {
.gl-label {
line-height: $gl-line-height;
}
}
.sortable-link {
white-space: normal;
}
}
.item-body {
@ -48,17 +50,12 @@ $item-weight-max-width: 48px;
cursor: help;
}
.issue-token-state-icon-open,
.issue-token-state-icon-closed {
margin-right: $gl-padding-4;
}
.confidential-icon {
color: $orange-600;
}
.item-title-wrapper {
max-width: 100%;
max-width: calc(100% - #{$item-remove-button-space});
}
.item-title {
@ -88,13 +85,6 @@ $item-weight-max-width: 48px;
.health-label-short {
display: none;
}
@include media-breakpoint-down(lg) {
.issue-count-badge {
padding: 0;
padding-right: 8px;
}
}
}
.item-body,
@ -232,25 +222,28 @@ $item-weight-max-width: 48px;
font-weight: $gl-font-weight-bold;
max-width: $item-path-max-width;
}
.issue-token-state-icon-open,
.issue-token-state-icon-closed {
display: block;
}
}
.btn-item-remove {
position: absolute;
right: 0;
top: $gl-padding-4 / 2;
right: 0;
padding: $gl-padding-4;
margin-right: $gl-padding-4 / 2;
line-height: 0;
border-color: transparent;
color: $gl-text-color-secondary;
.related-items-tree & {
position: relative;
top: initial;
padding: $btn-sm-side-margin;
margin-right: initial;
}
&:hover {
color: $gl-text-color;
border-color: $border-color;
}
}
@ -283,6 +276,15 @@ $item-weight-max-width: 48px;
/* Small devices (landscape phones, 768px and up) */
@include media-breakpoint-up(md) {
.item-body .item-contents {
max-width: 95%;
}
.related-items-tree .item-contents,
.item-body .item-title {
max-width: 100%;
}
.sortable-link {
text-overflow: ellipsis;
overflow: hidden;
@ -294,24 +296,6 @@ $item-weight-max-width: 48px;
.item-contents {
min-width: 0;
}
.item-title {
flex-basis: unset;
// 95% because we compensate
// for remove button which is
// positioned absolutely
width: 95%;
}
.btn-item-remove {
order: 1;
}
}
.item-meta {
.item-meta-child {
flex-basis: unset;
}
}
.card-header {
@ -348,25 +332,10 @@ $item-weight-max-width: 48px;
@include media-breakpoint-up(xl) {
.item-body {
.item-title {
min-width: 0;
width: auto;
flex-basis: auto;
flex-shrink: 1;
font-weight: $gl-font-weight-normal;
.issue-token-state-icon-open,
.issue-token-state-icon-closed {
display: block;
margin-right: $gl-padding-8;
}
}
.item-title-wrapper {
max-width: calc(100% - 500px);
}
.item-info-area {
flex-basis: auto;
}
}
@ -378,16 +347,7 @@ $item-weight-max-width: 48px;
overflow: hidden;
}
.item-meta {
flex: 1;
}
.item-assignees {
.avatar {
height: $gl-padding-24;
width: $gl-padding-24;
}
.avatar-counter {
height: $gl-padding-24;
min-width: $gl-padding-24;
@ -399,12 +359,8 @@ $item-weight-max-width: 48px;
.btn-item-remove {
position: relative;
top: initial;
right: 0;
padding: $btn-sm-side-margin;
&:hover {
border-color: $border-color;
}
margin-right: $gl-padding-4 / 2;
}
.sortable-link {
@ -422,10 +378,4 @@ $item-weight-max-width: 48px;
display: initial;
}
}
.item-body {
.item-title-wrapper {
max-width: calc(100% - 640px);
}
}
}

View File

@ -398,6 +398,7 @@ $tooltip-font-size: 12px;
* Padding
*/
$gl-padding-4: 4px;
$gl-padding-6: 6px;
$gl-padding-8: 8px;
$gl-padding-12: 12px;
$gl-padding: 16px;
@ -447,6 +448,7 @@ $breadcrumb-min-height: 48px;
$home-panel-title-row-height: 64px;
$home-panel-avatar-mobile-size: 24px;
$gl-line-height: 16px;
$gl-line-height-18: 18px;
$gl-line-height-20: 20px;
$gl-line-height-24: 24px;
$gl-line-height-14: 14px;

View File

@ -65,7 +65,7 @@ class Projects::WikisController < Projects::ApplicationController
def update
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
@page = WikiPages::UpdateService.new(container: @project, current_user: current_user, params: wiki_params).execute(@page)
if @page.valid?
redirect_to(
@ -81,7 +81,7 @@ class Projects::WikisController < Projects::ApplicationController
end
def create
@page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute
@page = WikiPages::CreateService.new(container: @project, current_user: current_user, params: wiki_params).execute
if @page.persisted?
redirect_to(
@ -112,7 +112,7 @@ class Projects::WikisController < Projects::ApplicationController
end
def destroy
WikiPages::DestroyService.new(@project, current_user).execute(@page)
WikiPages::DestroyService.new(container: @project, current_user: current_user).execute(@page)
redirect_to project_wiki_path(@project, :home),
status: :found,

View File

@ -483,6 +483,16 @@ class Group < Namespace
false
end
def execute_hooks(data, hooks_scope)
# NOOP
# TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
end
def execute_services(data, hooks_scope)
# NOOP
# TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
end
private
def update_two_factor_requirement

View File

@ -0,0 +1,61 @@
# frozen_string_literal: true
# In order to measure and log execution of our service, we just need to 'prepend Measurable' module
# Example:
# ```
# class DummyService
# prepend Measurable
#
# def execute
# # ...
# end
# end
# DummyService.prepend(Measurable)
# ```
#
# In case when we are prepending a module from the `EE` namespace with EE features
# we need to prepend Measurable after prepending `EE` module.
# This way Measurable will be at the bottom of the ancestor chain,
# in order to measure execution of `EE` features as well
# ```
# class DummyService
# def execute
# # ...
# end
# end
#
# DummyService.prepend_if_ee('EE::DummyService')
# DummyService.prepend(Measurable)
# ```
#
module Measurable
extend ::Gitlab::Utils::Override
override :execute
def execute(*args)
measuring? ? ::Gitlab::Utils::Measuring.new(base_log_data).with_measuring { super(*args) } : super(*args)
end
protected
# You can set extra attributes for performance measurement log.
def extra_attributes_for_measurement
defined?(super) ? super : {}
end
private
def measuring?
Feature.enabled?("gitlab_service_measuring_#{service_class}")
end
# These attributes are always present in log.
def base_log_data
extra_attributes_for_measurement.merge({ class: self.class.name })
end
def service_class
self.class.name.underscore.tr('/', '_')
end
end

View File

@ -52,7 +52,7 @@ module Groups
end
def savers
[tree_exporter, file_saver]
[version_saver, tree_exporter, file_saver]
end
def tree_exporter
@ -65,13 +65,17 @@ module Groups
end
def tree_exporter_class
if ::Feature.enabled?(:group_export_ndjson, @group&.parent)
if ::Feature.enabled?(:group_export_ndjson, @group&.parent, default_enabled: true)
Gitlab::ImportExport::Group::TreeSaver
else
Gitlab::ImportExport::Group::LegacyTreeSaver
end
end
def version_saver
Gitlab::ImportExport::VersionSaver.new(shared: shared)
end
def file_saver
Gitlab::ImportExport::Saver.new(exportable: @group, shared: @shared)
end

View File

@ -53,7 +53,7 @@ module Groups
end
def ndjson?
::Feature.enabled?(:group_import_ndjson, @group&.parent) &&
::Feature.enabled?(:group_import_ndjson, @group&.parent, default_enabled: true) &&
File.exist?(File.join(@shared.export_path, 'tree/groups/_all.ndjson'))
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
# Delete all matching Metrics::UsersStarredDashboard entries for given user based on matched dashboard_path, project
module Metrics
module UsersStarredDashboards
class DeleteService < ::BaseService
def initialize(user, project, dashboard_path = nil)
@user, @project, @dashboard_path = user, project, dashboard_path
end
def execute
ServiceResponse.success(payload: { deleted_rows: starred_dashboards.delete_all })
end
private
attr_reader :user, :project, :dashboard_path
def starred_dashboards
# since deleted records are scoped to their owner there is no need to
# check if that user can delete them, also if user lost access to
# project it shouldn't block that user from removing them
dashboards = user.metrics_users_starred_dashboards
if dashboard_path.present?
dashboards.for_project_dashboard(project, dashboard_path)
else
dashboards.for_project(project)
end
end
end
end
end

View File

@ -202,8 +202,19 @@ module Projects
end
end
def extra_attributes_for_measurement
{
current_user: current_user&.name,
project_full_path: "#{project_namespace&.full_path}/#{@params[:path]}"
}
end
private
def project_namespace
@project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
end
def create_from_template?
@params[:template_name].present? || @params[:template_project_id].present?
end
@ -224,4 +235,9 @@ module Projects
end
end
# rubocop: disable Cop/InjectEnterpriseEditionModule
Projects::CreateService.prepend_if_ee('EE::Projects::CreateService')
# rubocop: enable Cop/InjectEnterpriseEditionModule
# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::CreateService as well
Projects::CreateService.prepend(Measurable)

View File

@ -14,36 +14,16 @@ module Projects
@current_user, @params, @override_params = user, import_params.dup, override_params
end
def execute(options = {})
measurement_enabled = !!options[:measurement_enabled]
measurement_logger = options[:measurement_logger]
def execute
prepare_template_environment(template_file)
::Gitlab::Utils::Measuring.execute_with(measurement_enabled, measurement_logger, base_log_data) do
prepare_template_environment(template_file)
prepare_import_params
prepare_import_params
::Projects::CreateService.new(current_user, params).execute
end
::Projects::CreateService.new(current_user, params).execute
end
private
def base_log_data
base_log_data = {
class: self.class.name,
current_user: current_user.name,
project_full_path: project_path
}
if template_file
base_log_data[:import_type] = 'gitlab_project'
base_log_data[:file_path] = template_file.path
end
base_log_data
end
def overwrite_project?
overwrite? && project_with_same_full_path?
end

View File

@ -3,37 +3,39 @@
module Projects
module ImportExport
class ExportService < BaseService
def execute(after_export_strategy = nil, options = {})
prepend Measurable
def initialize(*args)
super
@shared = project.import_export_shared
end
def execute(after_export_strategy = nil)
unless project.template_source? || can?(current_user, :admin_project, project)
raise ::Gitlab::ImportExport::Error.permission_error(current_user, project)
end
@shared = project.import_export_shared
measurement_enabled = !!options[:measurement_enabled]
measurement_logger = options[:measurement_logger]
::Gitlab::Utils::Measuring.execute_with(measurement_enabled, measurement_logger, base_log_data) do
save_all!
execute_after_export_action(after_export_strategy)
end
save_all!
execute_after_export_action(after_export_strategy)
ensure
cleanup
end
protected
def extra_attributes_for_measurement
{
current_user: current_user&.name,
project_full_path: project&.full_path,
file_path: shared.export_path
}
end
private
attr_accessor :shared
def base_log_data
{
class: self.class.name,
current_user: current_user.name,
project_full_path: project.full_path,
file_path: shared.export_path
}
end
def execute_after_export_action(after_export_strategy)
return unless after_export_strategy

View File

@ -37,6 +37,17 @@ module Projects
error(s_("ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}") % { project_safe_import_url: project.safe_import_url, project_full_path: project.full_path, message: message })
end
protected
def extra_attributes_for_measurement
{
current_user: current_user&.name,
project_full_path: project&.full_path,
import_type: project&.import_type,
file_path: project&.import_source
}
end
private
def after_execute_hook
@ -138,4 +149,9 @@ module Projects
end
end
# rubocop: disable Cop/InjectEnterpriseEditionModule
Projects::ImportService.prepend_if_ee('EE::Projects::ImportService')
# rubocop: enable Cop/InjectEnterpriseEditionModule
# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::ImportService as well
Projects::ImportService.prepend(Measurable)

View File

@ -6,13 +6,13 @@ module WikiPages
# - external_action: the action we report to external clients with webhooks
# - usage_counter_action: the action that we count in out internal counters
# - event_action: what we record as the value of `Event#action`
class BaseService < ::BaseService
class BaseService < ::BaseContainerService
private
def execute_hooks(page)
page_data = payload(page)
@project.execute_hooks(page_data, :wiki_page_hooks)
@project.execute_services(page_data, :wiki_page_hooks)
container.execute_hooks(page_data, :wiki_page_hooks)
container.execute_services(page_data, :wiki_page_hooks)
increment_usage
create_wiki_event(page)
end

View File

@ -3,8 +3,8 @@
module WikiPages
class CreateService < WikiPages::BaseService
def execute
project_wiki = ProjectWiki.new(@project, current_user)
page = WikiPage.new(project_wiki)
wiki = Wiki.for_container(container, current_user)
page = WikiPage.new(wiki)
if page.create(@params)
execute_hooks(page)

View File

@ -9,7 +9,7 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
worker_resource_boundary :memory
urgency :throttled
def perform(current_user_id, project_id, after_export_strategy = {}, params = {}, options = {})
def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
current_user = User.find(current_user_id)
project = Project.find(project_id)
export_job = project.export_jobs.safe_find_or_create_by(jid: self.jid)
@ -17,7 +17,7 @@ class ProjectExportWorker # rubocop:disable Scalability/IdempotentWorker
export_job&.start
::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export, options)
::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
export_job&.finish
rescue ActiveRecord::RecordNotFound, Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError => e

View File

@ -0,0 +1,5 @@
---
title: Fixed redirection when deleting a project snippet
merge_request: 31709
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Remove token attribute from Runners API
merge_request: 31448
author:
type: removed

View File

@ -0,0 +1,5 @@
---
title: Prepare group export feature to use ndjson
merge_request: 31742
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Prepare group import feature to use ndjson
merge_request: 31741
author:
type: changed

View File

@ -0,0 +1,3 @@
# frozen_string_literal: true
Gitlab::Utils::Measuring.logger = Gitlab::Services::Logger.build

View File

@ -339,6 +339,9 @@ only. For example:
## `audit_json.log`
NOTE: **Note:**
Most log entries only exist in [GitLab Starter](https://about.gitlab.com/pricing), however a few exist in GitLab Core.
This file lives in `/var/log/gitlab/gitlab-rails/audit_json.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/audit_json.log` for
installations from source.
@ -584,6 +587,8 @@ This file lives in `/var/log/gitlab/gitlab-rails/importer.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/importer.log` for
installations from source.
It logs the progress of the import process.
## `auth.log`
> Introduced in GitLab 12.0.
@ -738,6 +743,23 @@ Each line contains a JSON line that can be ingested by Elasticsearch. For exampl
}
```
## `service_measurement.log`
> Introduced in GitLab 13.0.
This file lives in `/var/log/gitlab/gitlab-rails/service_measurement.log` for
Omnibus GitLab packages or in `/home/git/gitlab/log/service_measurement.log` for
installations from source.
It contain only a single structured log with measurements for each service execution.
It will contain measurement such as: number of sql calls, execution_time, gc_stats, memory usage, etc...
For example:
```json
{ "severity":"INFO", "time":"2020-04-22T16:04:50.691Z","correlation_id":"04f1366e-57a1-45b8-88c1-b00b23dc3616","class":"Projects::ImportExport::ExportService","current_user":"John Doe","project_full_path":"group1/test-export","file_path":"/path/to/archive","gc_stats":{"count":{"before":127,"after":127,"diff":0},"heap_allocated_pages":{"before":10369,"after":10369,"diff":0},"heap_sorted_length":{"before":10369,"after":10369,"diff":0},"heap_allocatable_pages":{"before":0,"after":0,"diff":0},"heap_available_slots":{"before":4226409,"after":4226409,"diff":0},"heap_live_slots":{"before":2542709,"after":2641420,"diff":98711},"heap_free_slots":{"before":1683700,"after":1584989,"diff":-98711},"heap_final_slots":{"before":0,"after":0,"diff":0},"heap_marked_slots":{"before":2542704,"after":2542704,"diff":0},"heap_eden_pages":{"before":10369,"after":10369,"diff":0},"heap_tomb_pages":{"before":0,"after":0,"diff":0},"total_allocated_pages":{"before":10369,"after":10369,"diff":0},"total_freed_pages":{"before":0,"after":0,"diff":0},"total_allocated_objects":{"before":24896308,"after":24995019,"diff":98711},"total_freed_objects":{"before":22353599,"after":22353599,"diff":0},"malloc_increase_bytes":{"before":140032,"after":6650240,"diff":6510208},"malloc_increase_bytes_limit":{"before":25804104,"after":25804104,"diff":0},"minor_gc_count":{"before":94,"after":94,"diff":0},"major_gc_count":{"before":33,"after":33,"diff":0},"remembered_wb_unprotected_objects":{"before":34284,"after":34284,"diff":0},"remembered_wb_unprotected_objects_limit":{"before":68568,"after":68568,"diff":0},"old_objects":{"before":2404725,"after":2404725,"diff":0},"old_objects_limit":{"before":4809450,"after":4809450,"diff":0},"oldmalloc_increase_bytes":{"before":140032,"after":6650240,"diff":6510208},"oldmalloc_increase_bytes_limit":{"before":68537556,"after":68537556,"diff":0}},"time_to_finish":0.12298400001600385,"number_of_sql_calls":70,"memory_usage":"0.0 MiB","label":"process_48616"}
```
## `geo.log` **(PREMIUM ONLY)**
> Introduced in 9.5.

View File

@ -162,9 +162,9 @@ GET /runners/:id
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6"
```
CAUTION: **Deprecation**
The `token` attribute in the response is deprecated [since GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
It will be removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
NOTE: **Note:**
The `token` attribute in the response was deprecated [in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
and removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
Example response:
@ -190,7 +190,6 @@ Example response:
"path_with_namespace": "gitlab-org/gitlab-foss"
}
],
"token": "205086a8e3b9a2b818ffac9b89d102",
"revision": null,
"tag_list": [
"ruby",
@ -225,9 +224,9 @@ PUT /runners/:id
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
```
CAUTION: **Deprecation**
The `token` attribute in the response is deprecated [since GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
It will be removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
NOTE: **Note:**
The `token` attribute in the response was deprecated [in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/214320).
and removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/issues/214322).
Example response:
@ -253,7 +252,6 @@ Example response:
"path_with_namespace": "gitlab-org/gitlab-foss"
}
],
"token": "205086a8e3b9a2b818ffac9b89d102",
"revision": null,
"tag_list": [
"ruby",

View File

@ -61,10 +61,9 @@ Parameters:
| `namespace_path` | string | yes | Namespace path |
| `project_path` | string | yes | Project name |
| `archive_path` | string | yes | Path to the exported project tarball you want to import |
| `measurement_enabled` | boolean | no | Measure execution time, number of SQL calls and GC count |
```shell
bundle exec rake "gitlab:import_export:import[root, root, testingprojectimport, /path/to/file.tar.gz, true]"
bundle exec rake "gitlab:import_export:import[root, root, testingprojectimport, /path/to/file.tar.gz]"
```
### Importing via the Rails console

View File

@ -370,10 +370,10 @@ use of extensions and concurrent index removal, you need at least PostgreSQL 9.2
## 7. Redis
GitLab requires at least Redis 2.8.
GitLab requires at least Redis 5.0.
If you are using Debian 8 or Ubuntu 14.04 and up, you can simply install
Redis 2.8 with:
If you are using Debian 10 or Ubuntu 20.04 and up, you can install
Redis 5.0 with:
```shell
sudo apt-get install redis-server

View File

@ -72,6 +72,10 @@ a version older than `v10.13.0`, you need to update to a newer version. You
can find instructions to install from community maintained packages or compile
from source at the [Node.js website](https://nodejs.org/en/download/).
## Redis versions
GitLab requires Redis 5.0+. Beginning in GitLab 13.0, lower versions are not supported.
## Hardware requirements
### Storage

View File

@ -83,6 +83,8 @@ The incident detail page shows detailed information about a particular incident.
To publish an Incident, you first need to create an issue in the Project you enabled the Status Page settings in.
Once this issue is created, a background worker will publish the issue onto the Status Page using the credentials you provided during setup.
Since all incidents are published publicly, user and group mentions are anonymized with `Incident Responder`,
and titles of non-public [GitLab references](../../markdown.md#special-gitlab-references) are removed.
NOTE: **Note:**
Confidential issues are not published. If a published issue is made confidential it will be unpublished.

View File

@ -11,9 +11,12 @@ module API
expose :version, :revision, :platform, :architecture
expose :contacted_at
# @deprecated in 12.10 https://gitlab.com/gitlab-org/gitlab/-/issues/214320
# will be removed by 13.0 https://gitlab.com/gitlab-org/gitlab/-/issues/214322
expose :token, if: lambda { |runner, options| options[:current_user].admin? || !runner.instance_type? }
# Will be removed: https://gitlab.com/gitlab-org/gitlab/-/issues/217105
expose(:token, if: ->(runner, options) do
return false if ::Feature.enabled?(:hide_token_from_runners_api, default_enabled: true)
options[:current_user].admin? || !runner.instance_type?
end)
# rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options|

View File

@ -70,7 +70,7 @@ module API
post ':id/wikis' do
authorize! :create_wiki, user_project
page = WikiPages::CreateService.new(user_project, current_user, params).execute
page = WikiPages::CreateService.new(container: user_project, current_user: current_user, params: params).execute
if page.valid?
present page, with: Entities::WikiPage
@ -91,7 +91,7 @@ module API
put ':id/wikis/:slug' do
authorize! :create_wiki, user_project
page = WikiPages::UpdateService.new(user_project, current_user, params).execute(wiki_page)
page = WikiPages::UpdateService.new(container: user_project, current_user: current_user, params: params).execute(wiki_page)
if page.valid?
present page, with: Entities::WikiPage
@ -107,7 +107,7 @@ module API
delete ':id/wikis/:slug' do
authorize! :admin_wiki, user_project
WikiPages::DestroyService.new(user_project, current_user).execute(wiki_page)
WikiPages::DestroyService.new(container: user_project, current_user: current_user).execute(wiki_page)
no_content!
end

View File

@ -6,6 +6,7 @@ module Gitlab
include Gitlab::Routing
CODE_NAVIGATION_JOB_NAME = 'code_navigation'
LATEST_COMMITS_LIMIT = 10
def initialize(project, commit_sha)
@project = project
@ -25,8 +26,11 @@ module Gitlab
def build
strong_memoize(:build) do
latest_commits_shas =
project.repository.commits(commit_sha, limit: LATEST_COMMITS_LIMIT).map(&:sha)
artifact = ::Ci::JobArtifact
.for_sha(commit_sha, project.id)
.for_sha(latest_commits_shas, project.id)
.for_job_name(CODE_NAVIGATION_JOB_NAME)
.last

View File

@ -8,6 +8,9 @@ module Gitlab
def build(wiki_page, user, action)
wiki = wiki_page.wiki
# TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
return {} if wiki.container.is_a?(Group)
{
object_kind: wiki_page.class.name.underscore,
user: user.hook_attrs,

View File

@ -11,20 +11,12 @@ module Gitlab
@file_path = opts.fetch(:file_path)
@namespace = Namespace.find_by_full_path(opts.fetch(:namespace_path))
@current_user = User.find_by_username(opts.fetch(:username))
@measurement_enabled = opts.fetch(:measurement_enabled)
@logger = logger
end
private
attr_reader :project, :namespace, :current_user, :file_path, :project_path, :logger, :measurement_enabled
def measurement_options
{
measurement_enabled: measurement_enabled,
measurement_logger: logger
}
end
attr_reader :project, :namespace, :current_user, :file_path, :project_path, :logger
def disable_upload_object_storage
overwrite_uploads_setting('enabled', false) do

View File

@ -16,7 +16,7 @@ module Gitlab
with_export do
::Projects::ImportExport::ExportService.new(project, current_user)
.execute(Gitlab::ImportExport::AfterExportStrategies::MoveFileStrategy.new(archive_path: file_path), measurement_options)
.execute(Gitlab::ImportExport::AfterExportStrategies::MoveFileStrategy.new(archive_path: file_path))
end
return error(project.import_export_shared.errors.join(', ')) if project.import_export_shared.errors.any?

View File

@ -59,7 +59,7 @@ module Gitlab
import_params
)
service.execute(measurement_options)
service.execute
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Gitlab
module Services
class Logger < ::Gitlab::JsonLogger
def self.file_name_noext
'service_measurement'
end
end
end
end

View File

@ -180,7 +180,8 @@ module Gitlab
reply_by_email_enabled: alt_usage_data { Gitlab::IncomingEmail.enabled? },
signup_enabled: alt_usage_data { Gitlab::CurrentSettings.allow_signup? },
web_ide_clientside_preview_enabled: alt_usage_data { Gitlab::CurrentSettings.web_ide_clientside_preview_enabled? },
ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity)
ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity),
grafana_link_enabled: alt_usage_data { Gitlab::CurrentSettings.grafana_enabled? }
}.merge(features_usage_data_container_expiration_policies)
end

View File

@ -6,19 +6,14 @@ module Gitlab
module Utils
class Measuring
class << self
def execute_with(measurement_enabled, logger, base_log_data)
measurement_enabled ? measuring(logger, base_log_data).with_measuring { yield } : yield
end
attr_writer :logger
private
def measuring(logger, base_log_data)
Gitlab::Utils::Measuring.new(logger: logger, base_log_data: base_log_data)
def logger
@logger ||= Logger.new(STDOUT)
end
end
def initialize(logger: nil, base_log_data: {})
@logger = logger || Logger.new($stdout)
def initialize(base_log_data = {})
@base_log_data = base_log_data
end
@ -45,7 +40,7 @@ module Gitlab
private
attr_reader :gc_stats, :time_to_finish, :sql_calls_count, :logger, :base_log_data
attr_reader :gc_stats, :time_to_finish, :sql_calls_count, :base_log_data
def with_count_queries(&block)
@sql_calls_count = 0
@ -76,21 +71,8 @@ module Gitlab
def log_info(details)
details = base_log_data.merge(details)
details = details.to_yaml if ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
logger.info(details)
end
def duration_in_numbers(duration_in_seconds)
milliseconds = duration_in_seconds.in_milliseconds % 1.second.in_milliseconds
seconds = duration_in_seconds % 1.minute
minutes = (duration_in_seconds / 1.minute) % (1.hour / 1.minute)
hours = duration_in_seconds / 1.hour
if hours == 0
"%02d:%02d:%03d" % [minutes, seconds, milliseconds]
else
"%02d:%02d:%02d:%03d" % [hours, minutes, seconds, milliseconds]
end
details = details.to_yaml if ActiveSupport::Logger.logger_outputs_to?(Measuring.logger, STDOUT)
Measuring.logger.info(details)
end
end
end

View File

@ -3,12 +3,12 @@
# Export project to archive
#
# @example
# bundle exec rake "gitlab:import_export:export[root, root, project_to_export, /path/to/file.tar.gz, true]"
# bundle exec rake "gitlab:import_export:export[root, root, project_to_export, /path/to/file.tar.gz]"
#
namespace :gitlab do
namespace :import_export do
desc 'GitLab | Import/Export | EXPERIMENTAL | Export large project archives'
task :export, [:username, :namespace_path, :project_path, :archive_path, :measurement_enabled] => :gitlab_environment do |_t, args|
task :export, [:username, :namespace_path, :project_path, :archive_path] => :gitlab_environment do |_t, args|
# Load it here to avoid polluting Rake tasks with Sidekiq test warnings
require 'sidekiq/testing'
@ -18,6 +18,7 @@ namespace :gitlab do
warn_user_is_not_gitlab
if ENV['EXPORT_DEBUG'].present?
Gitlab::Utils::Measuring.logger = logger
ActiveRecord::Base.logger = logger
logger.level = Logger::DEBUG
else
@ -29,7 +30,6 @@ namespace :gitlab do
project_path: args.project_path,
username: args.username,
file_path: args.archive_path,
measurement_enabled: Gitlab::Utils.to_boolean(args.measurement_enabled),
logger: logger
)

View File

@ -7,12 +7,12 @@
# 2. Performs Sidekiq job synchronously
#
# @example
# bundle exec rake "gitlab:import_export:import[root, root, imported_project, /path/to/file.tar.gz, true]"
# bundle exec rake "gitlab:import_export:import[root, root, imported_project, /path/to/file.tar.gz]"
#
namespace :gitlab do
namespace :import_export do
desc 'GitLab | Import/Export | EXPERIMENTAL | Import large project archives'
task :import, [:username, :namespace_path, :project_path, :archive_path, :measurement_enabled] => :gitlab_environment do |_t, args|
task :import, [:username, :namespace_path, :project_path, :archive_path] => :gitlab_environment do |_t, args|
# Load it here to avoid polluting Rake tasks with Sidekiq test warnings
require 'sidekiq/testing'
@ -22,6 +22,7 @@ namespace :gitlab do
warn_user_is_not_gitlab
if ENV['IMPORT_DEBUG'].present?
Gitlab::Utils::Measuring.logger = logger
ActiveRecord::Base.logger = logger
logger.level = Logger::DEBUG
else
@ -33,7 +34,6 @@ namespace :gitlab do
project_path: args.project_path,
username: args.username,
file_path: args.archive_path,
measurement_enabled: Gitlab::Utils.to_boolean(args.measurement_enabled),
logger: logger
)

View File

@ -8,8 +8,6 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-24 17:33-0400\n"
"PO-Revision-Date: 2020-04-24 17:33-0400\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@ -1738,16 +1736,10 @@ msgstr ""
msgid "AlertManagement|End time"
msgstr ""
msgid "AlertManagement|End time:"
msgstr ""
msgid "AlertManagement|Events"
msgstr ""
msgid "AlertManagement|Events:"
msgstr ""
msgid "AlertManagement|Full Alert Details"
msgid "AlertManagement|Full alert details"
msgstr ""
msgid "AlertManagement|High"
@ -1780,10 +1772,10 @@ msgstr ""
msgid "AlertManagement|Resolved"
msgstr ""
msgid "AlertManagement|Start time"
msgid "AlertManagement|Service"
msgstr ""
msgid "AlertManagement|Start time:"
msgid "AlertManagement|Start time"
msgstr ""
msgid "AlertManagement|Status"
@ -1798,6 +1790,9 @@ msgstr ""
msgid "AlertManagement|There was an error displaying the alerts. Confirm your endpoint's configuration details to ensure alerts appear."
msgstr ""
msgid "AlertManagement|Tool"
msgstr ""
msgid "AlertManagement|Triggered"
msgstr ""
@ -23712,9 +23707,6 @@ msgstr ""
msgid "Vulnerabilities over time"
msgstr ""
msgid "Vulnerability List"
msgstr ""
msgid "Vulnerability remediated. Review before resolving."
msgstr ""

View File

@ -171,7 +171,12 @@ module QA
end
def runners(tag_list: nil)
response = get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url
response = if tag_list
get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url
else
get Runtime::API::Request.new(api_client, "#{api_runners_path}").url
end
parse_body(response)
end

View File

@ -5,8 +5,8 @@ require 'securerandom'
module QA
module Resource
class Runner < Base
attr_writer :name, :tags, :image
attr_accessor :config, :token
attr_writer :name, :tags, :image, :executor, :executor_image
attr_accessor :config, :token, :run_untagged
attribute :id
attribute :project do
@ -20,35 +20,42 @@ module QA
@name || "qa-runner-#{SecureRandom.hex(4)}"
end
def tags
@tags || %w[qa e2e]
end
def image
@image || 'gitlab/gitlab-runner:alpine'
end
def executor
@executor || :shell
end
def executor_image
@executor_image || 'registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6'
end
def fabricate_via_api!
Service::DockerRun::GitlabRunner.new(name).tap do |runner|
runner.pull
runner.token = @token ||= project.runners_token
runner.address = Runtime::Scenario.gitlab_address
runner.tags = tags
runner.tags = @tags if @tags
runner.image = image
runner.config = config if config
runner.executor = executor
runner.executor_image = executor_image if executor == :docker
runner.run_untagged = run_untagged if run_untagged
runner.register!
end
end
def remove_via_api!
runners = project.runners(tag_list: tags)
runners = project.runners(tag_list: @tags)
unless runners && !runners.empty?
raise "Project #{project.path_with_namespace} has no runners with tags #{tags}."
raise "Project #{project.path_with_namespace} has no runners#{" with tags #{@tags}." if @tags&.any?}"
end
this_runner = runners.find { |runner| runner[:description] == name }
unless this_runner
raise "Project #{project.path_with_namespace} does not have a runner with a description matching #{name} and tags #{tags}. Runners available: #{runners}"
raise "Project #{project.path_with_namespace} does not have a runner with a description matching #{name} #{"or tags #{@tags}" if @tags&.any?}. Runners available: #{runners}"
end
@id = this_runner[:id]

View File

@ -6,14 +6,21 @@ module QA
module Service
module DockerRun
class GitlabRunner < Base
attr_accessor :token, :address, :tags, :image, :run_untagged
attr_writer :config
attr_reader :tags
attr_accessor :token, :address, :image, :run_untagged
attr_writer :config, :executor, :executor_image
CONFLICTING_VARIABLES_MESSAGE = <<~MSG
There are conflicting options preventing the runner from starting.
%s cannot be specified if %s is %s
MSG
def initialize(name)
@image = 'gitlab/gitlab-runner:alpine'
@name = name || "qa-runner-#{SecureRandom.hex(4)}"
@tags = %w[qa test]
@run_untagged = false
@run_untagged = true
@executor = :shell
@executor_image = 'registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6'
super()
end
@ -32,23 +39,49 @@ module QA
shell <<~CMD.tr("\n", ' ')
docker run -d --rm --entrypoint=/bin/sh
--network #{network} --name #{@name}
-p 8093:8093
-e CI_SERVER_URL=#{@address}
-e REGISTER_NON_INTERACTIVE=true
-e REGISTRATION_TOKEN=#{@token}
-e RUNNER_EXECUTOR=shell
-e RUNNER_TAG_LIST=#{@tags.join(',')}
-e RUNNER_NAME=#{@name}
#{'-v /var/run/docker.sock:/var/run/docker.sock' if @executor == :docker}
--privileged
#{@image} -c "#{register_command}"
CMD
end
def tags=(tags)
@tags = tags
@run_untagged = false
end
private
def register_command
<<~CMD
args = []
args << '--non-interactive'
args << "--name #{@name}"
args << "--url #{@address}"
args << "--registration-token #{@token}"
args << if run_untagged
raise CONFLICTING_VARIABLES_MESSAGE % [:tags=, :run_untagged, run_untagged] if @tags&.any?
'--run-untagged=true'
else
raise 'You must specify tags to run!' unless @tags&.any?
"--tag-list #{@tags.join(',')}"
end
args << "--executor #{@executor}"
if @executor == :docker
args << "--docker-image #{@executor_image}"
args << '--docker-tlsverify=false'
args << '--docker-privileged=true'
args << "--docker-network-mode=#{network}"
end
<<~CMD.strip
printf '#{config.chomp.gsub(/\n/, "\\n").gsub('"', '\"')}' > /etc/gitlab-runner/config.toml &&
gitlab-runner register --run-untagged=#{@run_untagged} &&
gitlab-runner register \
#{args.join(' ')} &&
gitlab-runner run
CMD
end

View File

@ -8,6 +8,7 @@ module QA
let(:runner) do
Resource::Runner.fabricate_via_api! do |runner|
runner.name = executor
runner.run_untagged = true
end
end
@ -17,9 +18,6 @@ module QA
mr.file_name = '.gitlab-ci.yml'
mr.file_content = <<~EOF
test:
tags:
- qa
- e2e
script:
- echo '(66.67%) covered'
EOF

View File

@ -0,0 +1,173 @@
# frozen_string_literal: true
module QA
describe Service::DockerRun::GitlabRunner do
let(:runner_name) { 'test-runner' }
let(:address) { 'gitlab.test' }
let(:token) { 'abc123' }
let(:tags) { %w[qa test] }
subject do
described_class.new(runner_name).tap do |runner|
runner.address = address
runner.token = token
end
end
it 'defaults to run untagged' do
expect(subject.run_untagged).to be(true)
end
describe '#register!' do
let(:register) { subject.send(:register!) }
before do
allow(subject).to receive(:shell)
end
context 'defaults' do
before do
register
end
it 'runs non-interactively' do
expect(subject).to have_received(:shell).with(/ --non-interactive /)
end
it 'sets pertinent information' do
expect(subject).to have_received(:shell).with(/--name #{runner_name} /)
expect(subject).to have_received(:shell).with(/--url #{subject.address} /)
expect(subject).to have_received(:shell).with(/--registration-token #{subject.token} /)
end
it 'runs untagged' do
expect(subject).to have_received(:shell).with(/--run-untagged=true /)
end
it 'has no tags' do
expect(subject.tags).to be_falsey
end
it 'runs daemonized' do
expect(subject).to have_received(:shell).with(/ -d /)
end
it 'cleans itself up' do
expect(subject).to have_received(:shell).with(/ --rm /)
end
end
context 'running untagged' do
before do
register
end
it 'passes --run-untagged=true' do
expect(subject).to have_received(:shell).with(/--run-untagged=true /)
end
it 'does not pass tag list' do
expect(subject).not_to have_received(:shell).with(/--tag-list/)
end
end
context 'running tagged' do
context 'with only tags set' do
before do
subject.tags = tags
register
end
it 'does not pass --run-untagged' do
expect(subject).not_to have_received(:shell).with(/--run-untagged=true/)
end
it 'passes the tags with comma-separation' do
expect(subject).to have_received(:shell).with(/--tag-list #{tags.join(',')} /)
end
end
context 'with specifying only run_untagged' do
before do
subject.run_untagged = false
end
it 'raises an error if tags are not specified' do
expect { register }.to raise_error(/must specify tags/i)
end
end
context 'when specifying contradicting variables' do
before do
subject.tags = tags
subject.run_untagged = true
end
it 'raises an error' do
expect { register }.to raise_error(/conflicting options/i)
end
end
end
context 'executors' do
it 'defaults to the shell executor' do
register
expect(subject).to have_received(:shell).with(/--executor shell /)
end
context 'docker' do
before do
subject.executor = :docker
register
end
it 'specifies the docker executor' do
expect(subject).to have_received(:shell).with(/--executor docker /)
end
it 'mounts the docker socket to the host runner' do
expect(subject).to have_received(:shell).with(/-v \/var\/run\/docker.sock:\/var\/run\/docker.sock /)
end
it 'runs in privileged mode' do
expect(subject).to have_received(:shell).with(/--privileged /)
end
it 'has a default image' do
expect(subject).to have_received(:shell).with(/--docker-image \b.+\b /)
end
it 'does not verify TLS' do
expect(subject).to have_received(:shell).with(/--docker-tlsverify=false /)
end
it 'passes privileged mode' do
expect(subject).to have_received(:shell).with(/--docker-privileged=true /)
end
it 'passes the host network' do
expect(subject).to have_received(:shell).with(/--docker-network-mode=#{subject.network}/)
end
end
end
end
describe '#tags=' do
before do
subject.tags = tags
end
it 'sets the tags' do
expect(subject.tags).to eq(tags)
end
it 'sets run_untagged' do
expect(subject.run_untagged).to be(false)
end
end
end
end

View File

@ -2,6 +2,10 @@ import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import AlertDetails from '~/alert_management/components/alert_details.vue';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
describe('AlertDetails', () => {
let wrapper;
const newIssuePath = 'root/alerts/-/issues/new';
@ -56,7 +60,7 @@ describe('AlertDetails', () => {
describe('when alert is present', () => {
beforeEach(() => {
mountComponent();
mountComponent({ data: { alert: mockAlert } });
});
it('renders a tab with overview information', () => {
@ -67,8 +71,39 @@ describe('AlertDetails', () => {
expect(wrapper.find('[data-testid="fullDetailsTab"]').exists()).toBe(true);
});
it('renders alert details', () => {
it('renders a title', () => {
expect(wrapper.find('[data-testid="title"]').text()).toBe(mockAlert.title);
});
it('renders a start time', () => {
expect(wrapper.find('[data-testid="startTimeItem"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="startTimeItem"]').props().time).toBe(
mockAlert.startedAt,
);
});
});
describe('individual alert fields', () => {
describe.each`
field | data | isShown
${'eventCount'} | ${1} | ${true}
${'eventCount'} | ${undefined} | ${false}
${'monitoringTool'} | ${'New Relic'} | ${true}
${'monitoringTool'} | ${undefined} | ${false}
${'service'} | ${'Prometheus'} | ${true}
${'service'} | ${undefined} | ${false}
`(`$desc`, ({ field, data, isShown }) => {
beforeEach(() => {
mountComponent({ data: { alert: { ...mockAlert, [field]: data } } });
});
it(`${field} is ${isShown ? 'displayed' : 'hidden'} correctly`, () => {
if (isShown) {
expect(wrapper.find(`[data-testid="${field}"]`).text()).toBe(data.toString());
} else {
expect(wrapper.find(`[data-testid="${field}"]`).exists()).toBe(false);
}
});
});
});

View File

@ -1,22 +1,38 @@
import Vue from 'vue';
/**
* Deprecated. Please do not use.
* Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
*/
const mountComponent = (Component, props = {}, el = null) =>
new Component({
propsData: props,
}).$mount(el);
/**
* Deprecated. Please do not use.
* Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
*/
export const createComponentWithStore = (Component, store, propsData = {}) =>
new Component({
store,
propsData,
});
/**
* Deprecated. Please do not use.
* Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
*/
export const mountComponentWithStore = (Component, { el, props, store }) =>
new Component({
store,
propsData: props || {},
}).$mount(el);
/**
* Deprecated. Please do not use.
* Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
*/
export const mountComponentWithSlots = (Component, { props, slots }) => {
const component = new Component({
propsData: props || {},
@ -30,9 +46,18 @@ export const mountComponentWithSlots = (Component, { props, slots }) => {
/**
* Mount a component with the given render method.
*
* -----------------------------
* Deprecated. Please do not use.
* Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
* -----------------------------
*
* This helps with inserting slots that need to be compiled.
*/
export const mountComponentWithRender = (render, el = null) =>
mountComponent(Vue.extend({ render }), {}, el);
/**
* Deprecated. Please do not use.
* Please see https://gitlab.com/groups/gitlab-org/-/epics/2445
*/
export default mountComponent;

View File

@ -172,14 +172,34 @@ describe('Snippet header component', () => {
});
});
it('closes modal and redirects to snippets listing in case of successful mutation', () => {
createComponent();
wrapper.vm.closeDeleteModal = jest.fn();
describe('in case of successful mutation, closes modal and redirects to correct listing', () => {
const createDeleteSnippet = (snippetProps = {}) => {
createComponent({
snippetProps,
});
wrapper.vm.closeDeleteModal = jest.fn();
wrapper.vm.deleteSnippet();
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
expect(window.location.pathname).toEqual('dashboard/snippets');
wrapper.vm.deleteSnippet();
return wrapper.vm.$nextTick();
};
it('redirects to dashboard/snippets for personal snippet', () => {
return createDeleteSnippet().then(() => {
expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
expect(window.location.pathname).toBe('dashboard/snippets');
});
});
it('redirects to project snippets for project snippet', () => {
const fullPath = 'foo/bar';
return createDeleteSnippet({
project: {
fullPath,
},
}).then(() => {
expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
expect(window.location.pathname).toBe(fullPath);
});
});
});
});

View File

@ -1,38 +1,2 @@
import Vue from 'vue';
const mountComponent = (Component, props = {}, el = null) =>
new Component({
propsData: props,
}).$mount(el);
export const createComponentWithStore = (Component, store, propsData = {}) =>
new Component({
store,
propsData,
});
export const mountComponentWithStore = (Component, { el, props, store }) =>
new Component({
store,
propsData: props || {},
}).$mount(el);
export const mountComponentWithSlots = (Component, { props, slots }) => {
const component = new Component({
propsData: props || {},
});
component.$slots = slots;
return component.$mount();
};
/**
* Mount a component with the given render method.
*
* This helps with inserting slots that need to be compiled.
*/
export const mountComponentWithRender = (render, el = null) =>
mountComponent(Vue.extend({ render }), {}, el);
export default mountComponent;
export { default } from '../../frontend/helpers/vue_mount_component_helper';
export * from '../../frontend/helpers/vue_mount_component_helper';

View File

@ -4,18 +4,30 @@ require 'spec_helper'
describe Gitlab::CodeNavigationPath do
context 'when there is an artifact with code navigation data' do
let(:project) { create(:project, :repository) }
let(:sha) { project.commit.id }
let(:build_name) { Gitlab::CodeNavigationPath::CODE_NAVIGATION_JOB_NAME }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:sha) { project.repository.commits('master', limit: 5).last.id }
let_it_be(:build_name) { Gitlab::CodeNavigationPath::CODE_NAVIGATION_JOB_NAME }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: sha) }
let_it_be(:job) { create(:ci_build, pipeline: pipeline, name: build_name) }
let_it_be(:artifact) { create(:ci_job_artifact, :lsif, job: job) }
let(:commit_sha) { sha }
let(:path) { 'lib/app.rb' }
let!(:pipeline) { create(:ci_pipeline, project: project, sha: sha) }
let!(:job) { create(:ci_build, pipeline: pipeline, name: build_name) }
let!(:artifact) { create(:ci_job_artifact, :lsif, job: job) }
subject { described_class.new(project, sha).full_json_path_for(path) }
subject { described_class.new(project, commit_sha).full_json_path_for(path) }
it 'assigns code_navigation_build variable' do
expect(subject).to eq("/#{project.full_path}/-/jobs/#{job.id}/artifacts/raw/lsif/#{path}.json")
context 'when a pipeline exist for a sha' do
it 'returns path to a file in the artifact' do
expect(subject).to eq("/#{project.full_path}/-/jobs/#{job.id}/artifacts/raw/lsif/#{path}.json")
end
end
context 'when a pipeline exist for the latest commits' do
let(:commit_sha) { project.commit.id }
it 'returns path to a file in the artifact' do
expect(subject).to eq("/#{project.full_path}/-/jobs/#{job.id}/artifacts/raw/lsif/#{path}.json")
end
end
context 'when code_navigation feature is disabled' do
@ -23,7 +35,7 @@ describe Gitlab::CodeNavigationPath do
stub_feature_flags(code_navigation: false)
end
it 'does not assign code_navigation_build variable' do
it 'returns nil' do
expect(subject).to be_nil
end
end

View File

@ -39,8 +39,6 @@ describe Gitlab::ImportExport::Project::ImportTask, :request_store do
expect(project.milestones.count).to be > 0
expect(project.import_state.status).to eq('finished')
end
it_behaves_like 'measurable'
end
context 'when project import is invalid' do

View File

@ -174,6 +174,21 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:dependency_proxy_enabled]).to eq(Gitlab.config.dependency_proxy.enabled)
expect(subject[:gitlab_shared_runners_enabled]).to eq(Gitlab.config.gitlab_ci.shared_runners_enabled)
expect(subject[:web_ide_clientside_preview_enabled]).to eq(Gitlab::CurrentSettings.web_ide_clientside_preview_enabled?)
expect(subject[:grafana_link_enabled]).to eq(Gitlab::CurrentSettings.grafana_enabled?)
end
context 'with embedded grafana' do
it 'returns true when embedded grafana is enabled' do
stub_application_setting(grafana_enabled: true)
expect(subject[:grafana_link_enabled]).to eq(true)
end
it 'returns false when embedded grafana is disabled' do
stub_application_setting(grafana_enabled: false)
expect(subject[:grafana_link_enabled]).to eq(false)
end
end
context 'with existing container expiration policies' do

View File

@ -3,62 +3,15 @@
require 'fast_spec_helper'
describe Gitlab::Utils::Measuring do
describe '.execute_with' do
let(:measurement_logger) { double(:logger) }
let(:base_log_data) do
{
class: described_class.name
}
end
let(:result_block) { 'result' }
subject { described_class.execute_with(measurement_enabled, measurement_logger, base_log_data) { result_block } }
context 'when measurement is enabled' do
let(:measurement_enabled) { true }
let!(:measurement) { described_class.new(logger: measurement_logger, base_log_data: base_log_data) }
before do
allow(measurement_logger).to receive(:info)
end
it 'measure execution with Gitlab::Utils::Measuring instance', :aggregate_failure do
expect(described_class).to receive(:new).with(logger: measurement_logger, base_log_data: base_log_data) { measurement }
expect(measurement).to receive(:with_measuring)
subject
end
it 'returns result from yielded block' do
is_expected.to eq(result_block)
end
end
context 'when measurement is disabled' do
let(:measurement_enabled) { false }
it 'does not measure service execution' do
expect(Gitlab::Utils::Measuring).not_to receive(:new)
subject
end
it 'returns result from yielded block' do
is_expected.to eq(result_block)
end
end
end
describe '#with_measuring' do
let(:logger) { double(:logger) }
let(:base_log_data) { {} }
let(:result) { "result" }
before do
allow(logger).to receive(:info)
allow(ActiveSupport::Logger).to receive(:logger_outputs_to?).with(Gitlab::Utils::Measuring.logger, STDOUT).and_return(false)
end
let(:measurement) { described_class.new(logger: logger, base_log_data: base_log_data) }
let(:measurement) { described_class.new(base_log_data) }
subject do
measurement.with_measuring { result }
@ -69,7 +22,7 @@ describe Gitlab::Utils::Measuring do
expect(measurement).to receive(:with_count_queries).and_call_original
expect(measurement).to receive(:with_gc_stats).and_call_original
expect(logger).to receive(:info).with(including(:gc_stats, :time_to_finish, :number_of_sql_calls, :memory_usage, :label))
expect(described_class.logger).to receive(:info).with(include(:gc_stats, :time_to_finish, :number_of_sql_calls, :memory_usage, :label))
is_expected.to eq(result)
end
@ -78,7 +31,7 @@ describe Gitlab::Utils::Measuring do
let(:base_log_data) { { test: "data" } }
it 'logs includes base data' do
expect(logger).to receive(:info).with(including(:test, :gc_stats, :time_to_finish, :number_of_sql_calls, :memory_usage, :label))
expect(described_class.logger).to receive(:info).with(include(:test, :gc_stats, :time_to_finish, :number_of_sql_calls, :memory_usage, :label))
subject
end

View File

@ -411,7 +411,9 @@ describe API::ProjectExport, :clean_gitlab_redis_cache do
it 'starts', :sidekiq_might_not_need_inline do
params = { description: "Foo" }
expect_any_instance_of(Projects::ImportExport::ExportService).to receive(:execute)
expect_next_instance_of(Projects::ImportExport::ExportService) do |service|
expect(service).to receive(:execute)
end
post api(path, project.owner), params: params
expect(response).to have_gitlab_http_status(:accepted)

View File

@ -326,6 +326,32 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'FF hide_token_from_runners_api is enabled' do
before do
stub_feature_flags(hide_token_from_runners_api: true)
end
it "does not return runner's token" do
get api("/runners/#{shared_runner.id}", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).not_to have_key('token')
end
end
context 'FF hide_token_from_runners_api is disabled' do
before do
stub_feature_flags(hide_token_from_runners_api: false)
end
it "returns runner's token" do
get api("/runners/#{shared_runner.id}", admin)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('token')
end
end
end
describe 'PUT /runners/:id' do

View File

@ -49,6 +49,12 @@ describe Groups::ImportExport::ExportService do
FileUtils.rm_rf(archive_path)
end
it 'saves the version' do
expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original
service.execute
end
it 'saves the models using ndjson tree saver' do
stub_feature_flags(group_export_ndjson: true)

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'spec_helper'
describe Metrics::UsersStarredDashboards::DeleteService do
subject(:service_instance) { described_class.new(user, project, dashboard_path) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
describe '#execute' do
let_it_be(:user_starred_dashboard_1) { create(:metrics_users_starred_dashboard, user: user, project: project, dashboard_path: 'dashboard_1') }
let_it_be(:user_starred_dashboard_2) { create(:metrics_users_starred_dashboard, user: user, project: project) }
let_it_be(:other_user_starred_dashboard) { create(:metrics_users_starred_dashboard, project: project) }
let_it_be(:other_project_starred_dashboard) { create(:metrics_users_starred_dashboard, user: user) }
context 'without dashboard_path' do
let(:dashboard_path) { nil }
it 'does not scope user starred dashboards by dashboard path' do
result = service_instance.execute
expect(result.success?).to be_truthy
expect(result.payload[:deleted_rows]).to be(2)
expect(Metrics::UsersStarredDashboard.all).to contain_exactly(other_user_starred_dashboard, other_project_starred_dashboard)
end
end
context 'with dashboard_path' do
let(:dashboard_path) { 'dashboard_1' }
it 'does scope user starred dashboards by dashboard path' do
result = service_instance.execute
expect(result.success?).to be_truthy
expect(result.payload[:deleted_rows]).to be(1)
expect(Metrics::UsersStarredDashboard.all).to contain_exactly(user_starred_dashboard_2, other_user_starred_dashboard, other_project_starred_dashboard)
end
end
end
end

View File

@ -489,6 +489,27 @@ describe Projects::CreateService, '#execute' do
end
end
it_behaves_like 'measurable service' do
before do
opts.merge!(
current_user: user,
path: 'foo'
)
end
let(:base_log_data) do
{
class: Projects::CreateService.name,
current_user: user.name,
project_full_path: "#{user.namespace.full_path}/#{opts[:path]}"
}
end
after do
create_project(user, opts)
end
end
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end

View File

@ -7,9 +7,10 @@ describe Projects::ImportExport::ExportService do
let!(:user) { create(:user) }
let(:project) { create(:project) }
let(:shared) { project.import_export_shared }
let(:service) { described_class.new(project, user) }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
subject(:service) { described_class.new(project, user) }
before do
project.add_maintainer(user)
end
@ -184,7 +185,7 @@ describe Projects::ImportExport::ExportService do
end
end
context 'when measurable params are provided' do
it_behaves_like 'measurable service' do
let(:base_log_data) do
{
class: described_class.name,
@ -194,44 +195,8 @@ describe Projects::ImportExport::ExportService do
}
end
subject(:service) { described_class.new(project, user) }
context 'when measurement is enabled' do
let(:logger) { double(:logger) }
let(:measurable_options) do
{
measurement_enabled: true,
measurement_logger: logger
}
end
before do
allow(logger).to receive(:info)
end
it 'measure service execution with Gitlab::Utils::Measuring' do
expect(Gitlab::Utils::Measuring).to receive(:execute_with).with(true, logger, base_log_data).and_call_original
expect_next_instance_of(Gitlab::Utils::Measuring) do |measuring|
expect(measuring).to receive(:with_measuring).and_call_original
end
service.execute(after_export_strategy, measurable_options)
end
end
context 'when measurement is disabled' do
let(:measurable_options) do
{
measurement_enabled: false
}
end
it 'does not measure service execution' do
expect(Gitlab::Utils::Measuring).to receive(:execute_with).with(false, nil, base_log_data).and_call_original
expect(Gitlab::Utils::Measuring).not_to receive(:new)
service.execute(after_export_strategy, measurable_options)
end
after do
service.execute(after_export_strategy)
end
end
end

View File

@ -264,13 +264,33 @@ describe Projects::ImportService do
it 'fails with port 25' do
project.import_url = "https://github.com:25/vim/vim.git"
result = described_class.new(project, user).execute
result = subject.execute
expect(result[:status]).to eq :error
expect(result[:message]).to include('Only allowed ports are 80, 443')
end
end
it_behaves_like 'measurable service' do
let(:base_log_data) do
{
class: described_class.name,
current_user: user.name,
project_full_path: project.full_path,
import_type: project.import_type,
file_path: project.import_source
}
end
before do
project.import_type = 'github'
end
after do
subject.execute
end
end
def stub_github_omniauth_provider
provider = OpenStruct.new(
'name' => 'github',

View File

@ -10,7 +10,7 @@ describe WikiPages::BaseService do
counter = Gitlab::UsageDataCounters::WikiPageCounter
error = counter::UnknownEvent
let(:subject) { bad_service_class.new(project, user, {}) }
let(:subject) { bad_service_class.new(container: project, current_user: user) }
context 'the class implements usage_counter_action incorrectly' do
let(:bad_service_class) do

View File

@ -3,96 +3,5 @@
require 'spec_helper'
describe WikiPages::CreateService do
let(:project) { create(:project, :wiki_repo) }
let(:user) { create(:user) }
let(:page_title) { 'Title' }
let(:opts) do
{
title: page_title,
content: 'Content for wiki page',
format: 'markdown'
}
end
subject(:service) { described_class.new(project, user, opts) }
before do
project.add_developer(user)
end
describe '#execute' do
it 'creates wiki page with valid attributes' do
page = service.execute
expect(page).to be_valid
expect(page.title).to eq(opts[:title])
expect(page.content).to eq(opts[:content])
expect(page.format).to eq(opts[:format].to_sym)
end
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once.with(WikiPage)
service.execute
end
it 'counts wiki page creation' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute }.to change { counter.read(:create) }.by 1
end
shared_examples 'correct event created' do
it 'creates appropriate events' do
expect { service.execute }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::CREATED,
target: have_attributes(canonical_slug: page_title)
)
end
end
context 'the new page is at the top level' do
let(:page_title) { 'root-level-page' }
include_examples 'correct event created'
end
context 'the new page is in a subsection' do
let(:page_title) { 'subsection/page' }
include_examples 'correct event created'
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute }.not_to change(Event, :count)
end
end
context 'when the options are bad' do
let(:page_title) { '' }
it 'does not count a creation event' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute }.not_to change { counter.read(:create) }
end
it 'does not record the activity' do
expect { service.execute }.not_to change(Event, :count)
end
it 'reports the error' do
expect(service.execute).to be_invalid
.and have_attributes(errors: be_present)
end
end
end
it_behaves_like 'WikiPages::CreateService#execute', :project
end

View File

@ -3,52 +3,5 @@
require 'spec_helper'
describe WikiPages::DestroyService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:page) { create(:wiki_page) }
subject(:service) { described_class.new(project, user) }
before do
project.add_developer(user)
end
describe '#execute' do
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once.with(page)
service.execute(page)
end
it 'increments the delete count' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute(page) }.to change { counter.read(:delete) }.by 1
end
it 'creates a new wiki page deletion event' do
expect { service.execute(page) }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::DESTROYED,
target: have_attributes(canonical_slug: page.slug)
)
end
it 'does not increment the delete count if the deletion failed' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute(nil) }.not_to change { counter.read(:delete) }
end
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute(page) }.not_to change(Event, :count)
end
end
it_behaves_like 'WikiPages::DestroyService#execute', :project
end

View File

@ -3,100 +3,5 @@
require 'spec_helper'
describe WikiPages::UpdateService do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:page) { create(:wiki_page) }
let(:page_title) { 'New Title' }
let(:opts) do
{
content: 'New content for wiki page',
format: 'markdown',
message: 'New wiki message',
title: page_title
}
end
subject(:service) { described_class.new(project, user, opts) }
before do
project.add_developer(user)
end
describe '#execute' do
it 'updates the wiki page' do
updated_page = service.execute(page)
expect(updated_page).to be_valid
expect(updated_page.message).to eq(opts[:message])
expect(updated_page.content).to eq(opts[:content])
expect(updated_page.format).to eq(opts[:format].to_sym)
expect(updated_page.title).to eq(page_title)
end
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once.with(WikiPage)
service.execute(page)
end
it 'counts edit events' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute page }.to change { counter.read(:update) }.by 1
end
shared_examples 'adds activity event' do
it 'adds a new wiki page activity event' do
expect { service.execute(page) }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::UPDATED,
wiki_page: page,
target_title: page.title
)
end
end
context 'the page is at the top level' do
let(:page_title) { 'Top level page' }
include_examples 'adds activity event'
end
context 'the page is in a subsection' do
let(:page_title) { 'Subsection / secondary page' }
include_examples 'adds activity event'
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute(page) }.not_to change(Event, :count)
end
end
context 'when the options are bad' do
let(:page_title) { '' }
it 'does not count an edit event' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute page }.not_to change { counter.read(:update) }
end
it 'does not record the activity' do
expect { service.execute page }.not_to change(Event, :count)
end
it 'reports the error' do
expect(service.execute(page)).to be_invalid
.and have_attributes(errors: be_present)
end
end
end
it_behaves_like 'WikiPages::UpdateService#execute', :project
end

View File

@ -53,56 +53,4 @@ RSpec.shared_examples 'gitlab projects import validations' do
end
end
end
context 'when measurable params are provided' do
let(:base_log_data) do
base_log_data = {
class: described_class.name,
current_user: namespace.owner.name,
project_full_path: "#{namespace.full_path}/#{path}"
}
base_log_data.merge!({ import_type: 'gitlab_project', file_path: import_params[:file].path }) if import_params[:file]
base_log_data
end
subject { described_class.new(namespace.owner, import_params) }
context 'when measurement is enabled' do
let(:logger) { double(:logger) }
let(:measurable_options) do
{
measurement_enabled: true,
measurement_logger: logger
}
end
before do
allow(logger).to receive(:info)
end
it 'measure service execution with Gitlab::Utils::Measuring' do
expect(Gitlab::Utils::Measuring).to receive(:execute_with).with(true, logger, base_log_data).and_call_original
expect_next_instance_of(Gitlab::Utils::Measuring) do |measuring|
expect(measuring).to receive(:with_measuring).and_call_original
end
subject.execute(measurable_options)
end
end
context 'when measurement is disabled' do
let(:measurable_options) do
{
measurement_enabled: false
}
end
it 'does not measure service execution' do
expect(Gitlab::Utils::Measuring).to receive(:execute_with).with(false, nil, base_log_data).and_call_original
expect(Gitlab::Utils::Measuring).not_to receive(:new)
subject.execute(measurable_options)
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
RSpec.shared_examples 'measurable service' do
context 'when measurement is enabled' do
let!(:measuring) { Gitlab::Utils::Measuring.new(base_log_data) }
before do
stub_feature_flags(feature_flag => true)
end
it 'measure service execution with Gitlab::Utils::Measuring', :aggregate_failures do
expect(Gitlab::Utils::Measuring).to receive(:new).with(base_log_data).and_return(measuring)
expect(measuring).to receive(:with_measuring).and_call_original
end
end
context 'when measurement is disabled' do
it 'does not measure service execution' do
stub_feature_flags(feature_flag => false)
expect(Gitlab::Utils::Measuring).not_to receive(:new)
end
end
def feature_flag
"gitlab_service_measuring_#{described_class_name}"
end
def described_class_name
described_class.name.underscore.tr('/', '_')
end
end

View File

@ -0,0 +1,94 @@
# frozen_string_literal: true
RSpec.shared_examples 'WikiPages::CreateService#execute' do |container_type|
let(:container) { create(container_type, :wiki_repo) }
let(:user) { create(:user) }
let(:page_title) { 'Title' }
let(:opts) do
{
title: page_title,
content: 'Content for wiki page',
format: 'markdown'
}
end
subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
it 'creates wiki page with valid attributes' do
page = service.execute
expect(page).to be_valid
expect(page).to be_persisted
expect(page.title).to eq(opts[:title])
expect(page.content).to eq(opts[:content])
expect(page.format).to eq(opts[:format].to_sym)
end
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once.with(WikiPage)
service.execute
end
it 'counts wiki page creation' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute }.to change { counter.read(:create) }.by 1
end
shared_examples 'correct event created' do
it 'creates appropriate events' do
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
pending('group wiki support') if container_type == :group
expect { service.execute }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::CREATED,
target: have_attributes(canonical_slug: page_title)
)
end
end
context 'the new page is at the top level' do
let(:page_title) { 'root-level-page' }
include_examples 'correct event created'
end
context 'the new page is in a subsection' do
let(:page_title) { 'subsection/page' }
include_examples 'correct event created'
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute }.not_to change(Event, :count)
end
end
context 'when the options are bad' do
let(:page_title) { '' }
it 'does not count a creation event' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute }.not_to change { counter.read(:create) }
end
it 'does not record the activity' do
expect { service.execute }.not_to change(Event, :count)
end
it 'reports the error' do
expect(service.execute).to be_invalid
.and have_attributes(errors: be_present)
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
RSpec.shared_examples 'WikiPages::DestroyService#execute' do |container_type|
let(:container) { create(container_type) }
let(:user) { create(:user) }
let(:page) { create(:wiki_page) }
subject(:service) { described_class.new(container: container, current_user: user) }
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once.with(page)
service.execute(page)
end
it 'increments the delete count' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute(page) }.to change { counter.read(:delete) }.by 1
end
it 'creates a new wiki page deletion event' do
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
pending('group wiki support') if container_type == :group
expect { service.execute(page) }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::DESTROYED,
target: have_attributes(canonical_slug: page.slug)
)
end
it 'does not increment the delete count if the deletion failed' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute(nil) }.not_to change { counter.read(:delete) }
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute(page) }.not_to change(Event, :count)
end
end
end

View File

@ -0,0 +1,98 @@
# frozen_string_literal: true
RSpec.shared_examples 'WikiPages::UpdateService#execute' do |container_type|
let(:container) { create(container_type, :wiki_repo) }
let(:user) { create(:user) }
let(:page) { create(:wiki_page) }
let(:page_title) { 'New Title' }
let(:opts) do
{
content: 'New content for wiki page',
format: 'markdown',
message: 'New wiki message',
title: page_title
}
end
subject(:service) { described_class.new(container: container, current_user: user, params: opts) }
it 'updates the wiki page' do
updated_page = service.execute(page)
expect(updated_page).to be_valid
expect(updated_page.message).to eq(opts[:message])
expect(updated_page.content).to eq(opts[:content])
expect(updated_page.format).to eq(opts[:format].to_sym)
expect(updated_page.title).to eq(page_title)
end
it 'executes webhooks' do
expect(service).to receive(:execute_hooks).once.with(WikiPage)
service.execute(page)
end
it 'counts edit events' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute page }.to change { counter.read(:update) }.by 1
end
shared_examples 'adds activity event' do
it 'adds a new wiki page activity event' do
# TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/216904
pending('group wiki support') if container_type == :group
expect { service.execute(page) }.to change { Event.count }.by 1
expect(Event.recent.first).to have_attributes(
action: Event::UPDATED,
wiki_page: page,
target_title: page.title
)
end
end
context 'the page is at the top level' do
let(:page_title) { 'Top level page' }
include_examples 'adds activity event'
end
context 'the page is in a subsection' do
let(:page_title) { 'Subsection / secondary page' }
include_examples 'adds activity event'
end
context 'the feature is disabled' do
before do
stub_feature_flags(wiki_events: false)
end
it 'does not record the activity' do
expect { service.execute(page) }.not_to change(Event, :count)
end
end
context 'when the options are bad' do
let(:page_title) { '' }
it 'does not count an edit event' do
counter = Gitlab::UsageDataCounters::WikiPageCounter
expect { service.execute page }.not_to change { counter.read(:update) }
end
it 'does not record the activity' do
expect { service.execute page }.not_to change(Event, :count)
end
it 'reports the error' do
expect(service.execute(page)).to be_invalid
.and have_attributes(errors: be_present)
end
end
end

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
RSpec.shared_examples 'measurable' do
context 'when measurement is enabled' do
let(:measurement_enabled) { true }
it 'prints measurement results' do
expect { subject }.to output(including('time_to_finish')).to_stdout
end
end
context 'when measurement is not enabled' do
let(:measurement_enabled) { false }
it 'does not output measurement results' do
expect { subject }.not_to output(/time_to_finish/).to_stdout
end
end
context 'when measurement is not provided' do
let(:measurement_enabled) { nil }
it 'does not output measurement results' do
expect { subject }.not_to output(/time_to_finish/).to_stdout
end
it 'does not raise any exception' do
expect { subject }.not_to raise_error
end
end
end

View File

@ -24,20 +24,6 @@ describe ProjectExportWorker do
subject.perform(user.id, project.id, { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' })
end
context 'with measurement options provided' do
it 'calls the ExportService with measurement options' do
measurement_options = { measurement_enabled: true }
params = {}
after_export_strategy = { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' }
expect_next_instance_of(::Projects::ImportExport::ExportService) do |service|
expect(service).to receive(:execute).with(instance_of(Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy), measurement_options)
end
subject.perform(user.id, project.id, after_export_strategy, params, measurement_options)
end
end
context 'export job' do
before do
allow_next_instance_of(::Projects::ImportExport::ExportService) do |service|
@ -69,7 +55,7 @@ describe ProjectExportWorker do
context 'when it fails' do
it 'does not raise an exception when strategy is invalid' do
expect_any_instance_of(::Projects::ImportExport::ExportService).not_to receive(:execute)
expect(::Projects::ImportExport::ExportService).not_to receive(:new)
expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.not_to raise_error
end