Commit 4dda2b2b authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 35b61afd fe283d6f
......@@ -15,7 +15,7 @@ export default class GlFieldErrors {
initValidators() {
// register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]']
const validateSelectors = [':text', ':password', '[type=email]', '[type=url]', '[type=number]']
.map(selector => `input${selector}`)
.join(',');
......
......@@ -24,7 +24,13 @@ export default {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapGetters(['currentProject']),
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
...mapState('pipelines', [
'isLoadingPipeline',
'hasLoadedPipeline',
'latestPipeline',
'stages',
'isLoadingJobs',
]),
ciLintText() {
return sprintf(
__('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
......@@ -36,7 +42,7 @@ export default {
);
},
showLoadingIcon() {
return this.isLoadingPipeline && this.latestPipeline === null;
return this.isLoadingPipeline && !this.hasLoadedPipeline;
},
},
created() {
......@@ -51,7 +57,7 @@ export default {
<template>
<div class="ide-pipeline">
<gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
<template v-else-if="latestPipeline !== null">
<template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
<ci-icon :status="latestPipeline.details.status" :size="24" />
<span class="prepend-left-8">
......@@ -62,7 +68,7 @@ export default {
</span>
</header>
<empty-state
v-if="latestPipeline === false"
v-if="!latestPipeline"
:help-page-path="links.ciHelpPagePath"
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true"
......
......@@ -10,6 +10,7 @@ export default {
},
[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) {
state.isLoadingPipeline = false;
state.hasLoadedPipeline = true;
if (pipeline) {
state.latestPipeline = {
......@@ -34,7 +35,7 @@ export default {
};
});
} else {
state.latestPipeline = false;
state.latestPipeline = null;
}
},
[types.REQUEST_JOBS](state, id) {
......
export default () => ({
isLoadingPipeline: true,
hasLoadedPipeline: false,
isLoadingJobs: false,
latestPipeline: null,
stages: [],
......
......@@ -204,8 +204,10 @@ label {
margin-top: #{$grid-size / 2};
}
.gl-field-error {
.gl-field-error,
.invalid-feedback {
color: $red-500;
font-size: $gl-font-size;
}
.gl-show-field-errors {
......
......@@ -46,7 +46,10 @@ class DashboardController < Dashboard::ApplicationController
end
def check_filters_presence!
@no_filters_set = finder_type.scalar_params.none? { |k| params.key?(k) }
no_scalar_filters_set = finder_type.scalar_params.none? { |k| params.key?(k) }
no_array_filters_set = finder_type.array_params.none? { |k, _| params.key?(k) }
@no_filters_set = no_scalar_filters_set && no_array_filters_set
return unless @no_filters_set
......
......@@ -124,8 +124,8 @@ class GroupsController < Groups::ApplicationController
flash[:notice] = "Group '#{@group.name}' was successfully transferred."
redirect_to group_path(@group)
else
flash.now[:alert] = service.error
render :edit
flash[:alert] = service.error
redirect_to edit_group_path(@group)
end
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -117,7 +117,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
terminal = environment.terminals.try(:first)
if terminal
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(terminal)
render json: Gitlab::Workhorse.channel_websocket(terminal)
else
render html: 'Not found', status: :not_found
end
......
......@@ -157,7 +157,7 @@ class Projects::JobsController < Projects::ApplicationController
# GET .../terminal.ws : implemented in gitlab-workhorse
def terminal_websocket_authorize
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.terminal_websocket(@build.terminal_specification)
render json: Gitlab::Workhorse.channel_websocket(@build.terminal_specification)
end
private
......
......@@ -16,7 +16,10 @@ class Projects::WikisController < Projects::ApplicationController
end
def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
@wiki_pages = Kaminari.paginate_array(
@project_wiki.pages(sort: params[:sort], direction: params[:direction])
).page(params[:page])
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end
......
......@@ -53,7 +53,6 @@ class IssuableFinder
assignee_username
author_id
author_username
label_name
milestone_title
my_reaction_emoji
search
......
# frozen_string_literal: true
class GitlabSchema < GraphQL::Schema
# Took our current most complicated query in use, issues.graphql,
# with a complexity of 19, and added a 20 point buffer to it.
# These values will evolve over time.
DEFAULT_MAX_COMPLEXITY = 40
AUTHENTICATED_COMPLEXITY = 50
ADMIN_COMPLEXITY = 60
use BatchLoader::GraphQL
use Gitlab::Graphql::Authorize
use Gitlab::Graphql::Present
use Gitlab::Graphql::Connections
use Gitlab::Graphql::Tracing
query_analyzer Gitlab::Graphql::QueryAnalyzers::LogQueryComplexity.analyzer
query(Types::QueryType)
default_max_page_size 100
max_complexity DEFAULT_MAX_COMPLEXITY
mutation(Types::MutationType)
def self.execute(query_str = nil, **kwargs)
kwargs[:max_complexity] ||= max_query_complexity(kwargs[:context])
super(query_str, **kwargs)
end
def self.max_query_complexity(ctx)
current_user = ctx&.fetch(:current_user, nil)
if current_user&.admin
ADMIN_COMPLEXITY
elsif current_user
AUTHENTICATED_COMPLEXITY
else
DEFAULT_MAX_COMPLEXITY
end
end
end
......@@ -3,5 +3,14 @@
module Types
class BaseField < GraphQL::Schema::Field
prepend Gitlab::Graphql::Authorize
DEFAULT_COMPLEXITY = 1
def initialize(*args, **kwargs, &block)
# complexity is already defaulted to 1, but let's make it explicit
kwargs[:complexity] ||= DEFAULT_COMPLEXITY
super(*args, **kwargs, &block)
end
end
end
......@@ -77,7 +77,7 @@ module BlobHelper
project,
ref,
path,
label: "Replace",
label: _("Replace"),
action: "replace",
btn_class: "default",
modal_type: "upload"
......@@ -89,7 +89,7 @@ module BlobHelper
project,
ref,
path,
label: "Delete",
label: _("Delete"),
action: "delete",
btn_class: "remove",
modal_type: "remove"
......@@ -101,14 +101,14 @@ module BlobHelper
end
def leave_edit_message
"Leave edit mode?\nAll unsaved changes will be lost."
_("Leave edit mode? All unsaved changes will be lost.")
end
def editing_preview_title(filename)
if Gitlab::MarkupHelper.previewable?(filename)
'Preview'
_('Preview')
else
'Preview changes'
_('Preview changes')
end
end
......@@ -201,14 +201,14 @@ module BlobHelper
return if blob.empty?
return if blob.binary? || blob.stored_externally?
title = 'Open raw'
title = _('Open raw')
link_to icon('file-code-o'), blob_raw_path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
def download_blob_button(blob)
return if blob.empty?
title = 'Download'
title = _('Download')
link_to sprite_icon('download'), blob_raw_path(inline: false), download: @path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
......
......@@ -4,12 +4,12 @@ module BuildsHelper
def build_summary(build, skip: false)
if build.has_trace?
if skip
link_to "View job trace", pipeline_job_url(build.pipeline, build)
link_to _("View job trace"), pipeline_job_url(build.pipeline, build)
else
build.trace.html(last_lines: 10).html_safe
end
else
"No job trace"
_("No job trace")
end
end
......@@ -31,7 +31,7 @@ module BuildsHelper
def build_failed_issue_options
{
title: "Job Failed ##{@build.id}",
title: _("Job Failed #%{build_id}") % { build_id: @build.id },
description: project_job_url(@project, @build)
}
end
......
......@@ -21,7 +21,7 @@ module ButtonHelper
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to clipboard'
title = data[:title] || _('Copy to clipboard')
button_text = data[:button_text] || ''
hide_tooltip = data[:hide_tooltip] || false
hide_button_icon = data[:hide_button_icon] || false
......
......@@ -4,8 +4,7 @@ module FormHelper
def form_errors(model, type: 'form')
return unless model.errors.any?
pluralized = 'error'.pluralize(model.errors.count)
headline = "The #{type} contains the following #{pluralized}:"
headline = n_('The %{type} contains the following error:', 'The %{type} contains the following errors:', model.errors.count) % { type: type }
content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
content_tag(:h4, headline) <<
......@@ -24,7 +23,7 @@ module FormHelper
title: 'Select assignee',
filter: true,
dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee',
placeholder: 'Search users',
placeholder: _('Search users'),
data: {
first_user: current_user&.username,
null_user: true,
......
......@@ -181,8 +181,8 @@ module LabelsHelper
def label_deletion_confirm_text(label)
case label
when GroupLabel then 'Remove this label? This will affect all projects within the group. Are you sure?'
when ProjectLabel then 'Remove this label? Are you sure?'
when GroupLabel then _('Remove this label? This will affect all projects within the group. Are you sure?')
when ProjectLabel then _('Remove this label? Are you sure?')
end
end
......
......@@ -30,7 +30,7 @@ module SearchHelper
to = collection.offset_value + collection.to_a.size
count = collection.total_count
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"") % { from: from, to: to, count: count, scope: scope.humanize(capitalize: false), term: term }
end
def find_project_for_result_blob(projects, result)
......@@ -59,31 +59,31 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
{ category: "Settings", label: "User settings", url: profile_path },
{ category: "Settings", label: "SSH Keys", url: profile_keys_path },
{ category: "Settings", label: "Dashboard", url: root_path }
{ category: "Settings", label: _("User settings"), url: profile_path },
{ category: "Settings", label: _("SSH Keys"), url: profile_keys_path },
{ category: "Settings", label: _("Dashboard"), url: root_path }
]
end
# Autocomplete results for settings pages, for admins
def default_autocomplete_admin
[
{ category: "Settings", label: "Admin Section", url: admin_root_path }
{ category: "Settings", label: _("Admin Section"), url: admin_root_path }
]
end
# Autocomplete results for internal help pages
def help_autocomplete
[
{ category: "Help", label: "API Help", url: help_page_path("api/README") },
{ category: "Help", label: "Markdown Help", url: help_page_path("user/markdown") },
{ category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") },
{ category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") },
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
{ category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") },
{ category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") },
{ category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") },
{ category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") }
{ category: "Help", label: _("API Help"), url: help_page_path("api/README") },
{ category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") },
{ category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") },
{ category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") },
{ category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") },
{ category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") },
{ category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") },
{ category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") },
{ category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") }
]
end
......@@ -93,16 +93,16 @@ module SearchHelper
ref = @ref || @project.repository.root_ref
[
{ category: "In this project", label: "Files", url: project_tree_path(@project, ref) },
{ category: "In this project", label: "Commits", url: project_commits_path(@project, ref) },
{ category: "In this project", label: "Network", url: project_network_path(@project, ref) },
{ category: "In this project", label: "Graph", url: project_graph_path(@project, ref) },
{ category: "In this project", label: "Issues", url: project_issues_path(@project) },
{ category: "In this project", label: "Merge Requests", url: project_merge_requests_path(@project) },
{ category: "In this project", label: "Milestones", url: project_milestones_path(@project) },
{ category: "In this project", label: "Snippets", url: project_snippets_path(@project) },
{ category: "In this project", label: "Members", url: project_project_members_path(@project) },
{ category: "In this project", label: "Wiki", url: project_wikis_path(@project) }
{ category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) },
{ category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) },
{ category: "In this project", label: _("Network"), url: project_network_path(@project, ref) },
{ category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) },
{ category: "In this project", label: _("Issues"), url: project_issues_path(@project) },
{ category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) },
{ category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) },
{ category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) },
{ category: "In this project", label: _("Members"), url: project_project_members_path(@project) },
{ category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) }
]
else
[]
......@@ -162,7 +162,7 @@ module SearchHelper
opts =
{
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
placeholder: _('Search or filter results...'),
data: {
'username-params' => UserSerializer.new.represent(@users)
},
......
......@@ -86,17 +86,17 @@ module TreeHelper
end
def edit_in_new_fork_notice_now
"You're not allowed to make changes to this project directly." +
" A fork of this project is being created that you can make changes in, so you can submit a merge request."
_("You're not allowed to make changes to this project directly. "\
"A fork of this project is being created that you can make changes in, so you can submit a merge request.")
end
def edit_in_new_fork_notice
"You're not allowed to make changes to this project directly." +
" A fork of this project has been created that you can make changes in, so you can submit a merge request."
_("You're not allowed to make changes to this project directly. "\
"A fork of this project has been created that you can make changes in, so you can submit a merge request.")
end
def edit_in_new_fork_notice_action(action)
edit_in_new_fork_notice + " Try to #{action} this file again."
edit_in_new_fork_notice + _(" Try to %{action} this file again.") % { action: action }
end
def commit_in_fork_help
......
......@@ -42,11 +42,11 @@ module VisibilityLevelHelper
def group_visibility_level_description(level)
case level
when Gitlab::VisibilityLevel::PRIVATE
"The group and its projects can only be viewed by members."
_("The group and its projects can only be viewed by members.")
when Gitlab::VisibilityLevel::INTERNAL
"The group and any internal projects can be viewed by any logged in user."
_("The group and any internal projects can be viewed by any logged in user.")
when Gitlab::VisibilityLevel::PUBLIC
"The group and any public projects can be viewed without any authentication."
_("The group and any public projects can be viewed without any authentication.")
end
end
......@@ -54,20 +54,20 @@ module VisibilityLevelHelper
case level
when Gitlab::VisibilityLevel::PRIVATE
if snippet.is_a? ProjectSnippet
"The snippet is visible only to project members."
_("The snippet is visible only to project members.")
else
"The snippet is visible only to me."
_("The snippet is visible only to me.")
end
when Gitlab::VisibilityLevel::INTERNAL
"The snippet is visible to any logged in user."
_("The snippet is visible to any logged in user.")
when Gitlab::VisibilityLevel::PUBLIC
"The snippet can be accessed without any authentication."
_("The snippet can be accessed without any authentication.")
end
end
def restricted_visibility_level_description(level)
level_name = Gitlab::VisibilityLevel.level_name(level)
"#{level_name.capitalize} visibility has been restricted by the administrator."
_("%{level_name} visibility has been restricted by the administrator.") % { level_name: level_name.capitalize }
end
def disallowed_visibility_level_description(level, form_model)
......
......@@ -47,4 +47,24 @@ module WikiHelper
def wiki_attachment_upload_url
expose_url(api_v4_projects_wikis_attachments_path(id: @project.id))
end
def wiki_sort_controls(project, sort, direction)
sort ||= ProjectWiki::TITLE_ORDER
link_class = 'btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort'
reversed_direction = direction == 'desc' ? 'asc' : 'desc'
icon_class = direction == 'desc' ? 'highest' : 'lowest'
link_to(project_wikis_pages_path(project, sort: sort, direction: reversed_direction),
type: 'button', class: link_class, title: _('Sort direction')) do
sprite_icon("sort-#{icon_class}", size: 16)
end
end
def wiki_sort_title(key)
if key == ProjectWiki::CREATED_AT_ORDER
s_("Wiki|Created date")
else
s_("Wiki|Title")
end
end
end
......@@ -83,8 +83,13 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) }
scope :with_artifacts_archive, ->() do
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
if Feature.enabled?(:ci_enable_legacy_artifacts)
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)',
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
else
where('EXISTS (?)',
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
end
scope :with_existing_job_artifacts, ->(query) do
......@@ -135,6 +140,8 @@ module Ci
where("EXISTS (?)", matcher)
end
##
# TODO: Remove these mounters when we remove :ci_enable_legacy_artifacts feature flag
mount_uploader :legacy_artifacts_file, LegacyArtifactUploader, mount_on: :artifacts_file
mount_uploader :legacy_artifacts_metadata, LegacyArtifactUploader, mount_on: :artifacts_metadata
......@@ -775,7 +782,7 @@ module Ci
private
def erase_old_artifacts!
# TODO: To be removed once we get rid of
# TODO: To be removed once we get rid of ci_enable_legacy_artifacts feature flag
remove_artifacts_file!
remove_artifacts_metadata!
save
......
......@@ -6,6 +6,8 @@ module Ci
class BuildRunnerSession < ApplicationRecord
extend Gitlab::Ci::Model
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'.freeze
self.table_name = 'ci_builds_runner_session'
belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
......@@ -14,11 +16,21 @@ module Ci
validates :url, url: { protocols: %w(https) }
def terminal_specification
return {} unless url.present?
wss_url = Gitlab::UrlHelpers.as_wss(self.url)
return {} unless wss_url.present?
wss_url = "#{wss_url}/exec"
channel_specification(wss_url, TERMINAL_SUBPROTOCOL)
end
private
def channel_specification(url, subprotocol)
return {} if subprotocol.blank? || url.blank?
{
subprotocols: ['terminal.gitlab.com'].freeze,
url: "#{url}/exec".sub("https://", "wss://"),
subprotocols: Array(subprotocol),
url: url,
headers: { Authorization: [authorization.presence] }.compact,
ca_pem: certificate.presence
}
......
......@@ -13,7 +13,7 @@ module ArtifactMigratable
end
def artifacts?
!artifacts_expired? && artifacts_file.exists?
!artifacts_expired? && artifacts_file&.exists?
end
def artifacts_metadata?
......@@ -43,4 +43,16 @@ module ArtifactMigratable
def artifacts_size
read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i
end
def legacy_artifacts_file
return unless Feature.enabled?(:ci_enable_legacy_artifacts)
super
end
def legacy_artifacts_metadata
return unless Feature.enabled?(:ci_enable_legacy_artifacts)
super
end
end
......@@ -29,6 +29,40 @@
# However, it will enqueue a background worker to call `#calculate_reactive_cache`
# and set an initial cache lifetime of ten minutes.
#
# The background worker needs to find or generate the object on which
# `with_reactive_cache` was called.
# The default behaviour can be overridden by defining a custom
# `reactive_cache_worker_finder`.
# Otherwise the background worker will use the class name and primary key to get
# the object using the ActiveRecord find_by method.
#
# class Bar
# include ReactiveCaching
#
# self.reactive_cache_key = ->() { ["bar", "thing"] }
# self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
#
# def self.from_cache(var1, var2)
# # This method will be called by the background worker with "bar1" and
# # "bar2" as arguments.
# new(var1, var2)
# end
#
# def initialize(var1, var2)
# # ...
# end
#
# def calculate_reactive_cache
# # Expensive operation here. The return value of this method is cached
# end
#
# def result
# with_reactive_cache("bar1", "bar2") do |data|
# # ...
# end
# end
# end
#
# Each time the background job completes, it stores the return value of
# `#calculate_reactive_cache`. It is also re-enqueued to run again after
# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
......@@ -52,6 +86,7 @@ module ReactiveCaching
class_attribute :reactive_cache_key
class_attribute :reactive_cache_lifetime
class_attribute :reactive_cache_refresh_interval
class_attribute :reactive_cache_worker_finder
# defaults
self.reactive_cache_lease_timeout = 2.minutes
......@@ -59,6 +94,10 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes
self.reactive_cache_worker_finder = ->(id, *_args) do
find_by(primary_key => id)
end
def calculate_reactive_cache(*args)
raise NotImplementedError
end
......
......@@ -13,6 +13,11 @@ class ProjectWiki
CouldNotCreateWikiError = Class.new(StandardError)
SIDEBAR = '_sidebar'
TITLE_ORDER = 'title'
CREATED_AT_ORDER = 'created_at'
DIRECTION_DESC = 'desc'
DIRECTION_ASC = 'asc'
# Returns a string describing what went wrong after
# an operation fails.
attr_reader :error_message
......@@ -82,8 +87,15 @@ class ProjectWiki
# Returns an Array of GitLab WikiPage instances or an
# empty Array if this Wiki has no pages.
def pages(limit: 0)
wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) }
def pages(limit: 0, sort: nil, direction: DIRECTION_ASC)
sort ||= TITLE_ORDER
direction_desc = direction == DIRECTION_DESC
wiki.pages(
limit: limit, sort: sort, direction_desc: direction_desc
).map do |page|
WikiPage.new(self, page, true)
end
end
# Finds a page within the repository based on a tile
......
......@@ -28,16 +28,15 @@ class WikiPage
def self.group_by_directory(pages)
return [] if pages.blank?
pages.sort_by { |page| [page.directory, page.slug] }
.group_by(&:directory)
.map do |dir, pages|
if dir.present?
WikiDirectory.new(dir, pages)
else
pages
end
end
.flatten
pages.each_with_object([]) do |page, grouped_pages|
next grouped_pages << page unless page.directory.present?
directory = grouped_pages.find { |dir| dir.slug == page.directory }
next directory.pages << page if directory
grouped_pages << WikiDirectory.new(page.directory, [page])
end
end
def self.unhyphenize(name)
......
# frozen_string_literal: true
##
# TODO: Remove this uploader when we remove :ci_enable_legacy_artifacts feature flag
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/58595
class LegacyArtifactUploader < GitlabUploader
extend Workhorse::UploadPath
include ObjectStorage::Concern
......
......@@ -7,25 +7,27 @@
- help_link_end = ' %{external_link_icon}</a>'.html_safe % { external_link_icon: external_link_icon }
%p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
- link_to_help_page = link_to(s_('ClusterIntegration|help page'),
help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page }
%p= link_to('Select a different Google account', @authorize_url)
= form_for @gcp_cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
= form_errors(@gcp_cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= bootstrap_form_for @gcp_cluster, html: { class: 'gl-show-field-errors js-gke-cluster-creation prepend-top-20',
data: { token: token_in_session } }, url: clusterable.create_gcp_clusters_path, as: :cluster do |field|
= field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
- if has_multiple_clusters?
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
= field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
= field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
class: 'label-bold' } do
= field.text_field :environment_scope, required: true, class: 'form-control',
title: 'Environment scope is required.', wrapper: false
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field|
.form-group
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'), class: 'label-bold'
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project'),
class: 'label-bold'
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.hidden_field :gcp_project_id
.dropdown
......@@ -47,9 +49,9 @@
%p.form-text.text-muted
= s_('ClusterIntegration|Learn more about %{help_link_start}zones%{help_link_end}.').html_safe % { help_link_start: help_link_start % { url: zones_link_url }, help_link_end: help_link_end }
.form-group
= provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes'), class: 'label-bold'
= provider_gcp_field.text_field :num_nodes, class: 'form-control', placeholder: '3'
= provider_gcp_field.number_field :num_nodes, required: true, placeholder: '3',
title: s_('ClusterIntegration|Number of nodes must be a numerical value.'),
label: s_('ClusterIntegration|Number of nodes'), label_class: 'label-bold'
.form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type'), class: 'label-bold'
......@@ -64,13 +66,14 @@
= s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end }
.form-group
.form-check
= provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true
= provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
= provider_gcp_field.check_box :legacy_abac, { label: s_('ClusterIntegration|RBAC-enabled cluster'),
label_class: 'label-bold' }, false, true
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
.form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'),
class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
= form_for @user_cluster, url: clusterable.create_user_clusters_path, as: :cluster do |field|
= form_errors(@user_cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= bootstrap_form_for @user_cluster, html: { class: 'gl-show-field-errors' },
url: clusterable.create_user_clusters_path, as: :cluster do |field|
= field.text_field :name, required: true, title: s_('ClusterIntegration|Cluster name is required.'),
label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold'
- if has_multiple_clusters?
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold'
= field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope')
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.form_group :environment_scope, label: { text: s_('ClusterIntegration|Environment scope'),
class: 'label-bold' } do
= field.text_field :environment_scope, required: true,
title: 'Environment scope is required.', wrapper: false
.form-text.text-muted
= s_("ClusterIntegration|Choose which of your environments will use this cluster.")
= field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL'), class: 'label-bold'
= platform_kubernetes_field.text_field :api_url, class: 'form-control', placeholder: s_('ClusterIntegration|API URL')
.form-group
= platform_kubernetes_field.label :ca_cert, s_('ClusterIntegration|CA Certificate'), class: 'label-bold'
= platform_kubernetes_field.text_area :ca_cert, class: 'form-control', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)')
.form-group
= platform_kubernetes_field.label :token, s_('ClusterIntegration|Token'), class: 'label-bold'
= platform_kubernetes_field.text_field :token, class: 'form-control', placeholder: s_('ClusterIntegration|Service token'), autocomplete: 'off'
= platform_kubernetes_field.url_field :api_url, required: true,
title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
label: s_('ClusterIntegration|API URL'), label_class: 'label-bold'
= platform_kubernetes_field.text_area :ca_cert,
placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold'
= platform_kubernetes_field.text_field :token, required: true,
title: s_('ClusterIntegration|Service token is required.'), label: s_('ClusterIntegration|Service Token'),
autocomplete: 'off', label_class: 'label-bold'
- if @user_cluster.allow_user_defined_namespace?
.form-group
= platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold'
= platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
= platform_kubernetes_field.text_field :namespace,
label: s_('ClusterIntegration|Project namespace (optional, unique)'), label_class: 'label-bold'
.form-group
.form-check
= platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
= platform_kubernetes_field.form_group :authorization_type do
= platform_kubernetes_field.check_box :authorization_type,
{ class: 'qa-rbac-checkbox', label: s_('ClusterIntegration|RBAC-enabled cluster'),
label_class: 'label-bold', inline: true }, 'rbac', 'abac'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= link_to _('More information'), help_page_path('user/project/clusters/index.md',
anchor: 'role-based-access-control-rbac-core-only'), target: '_blank'
.form-group
= field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success'
= form_for cluster, url: update_cluster_url_path, as: :cluster do |field|
= form_errors(cluster)
.form-group
- if cluster.read_only_kubernetes_platform_fields?
%label.append-bottom-10{ for: 'cluster-name' }
= s_('ClusterIntegration|Kubernetes cluster name')
.input-group
%input.form-control.cluster-name.js-select-on-focus{ value: cluster.name, readonly: true }
%span.input-group-append
= clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'input-group-text btn-default')
- else
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold'
.input-group
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
= bootstrap_form_for cluster, url: update_cluster_url_path, html: { class: 'gl-show-field-errors' },
as: :cluster do |field|
- copy_name_btn = clipboard_button(text: cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'),
class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
= field.text_field :name, class: 'js-select-on-focus cluster-name', required: true,
title: s_('ClusterIntegration|Cluster name is required.'),
readonly: cluster.read_only_kubernetes_platform_fields?,
label: s_('ClusterIntegration|Kubernetes cluster name'), label_class: 'label-bold',
input_group_class: 'gl-field-error-anchor', append: copy_name_btn
= field.fields_for :platform_kubernetes, platform do |platform_field|
.form-group
= platform_field.label :api_url, s_('ClusterIntegration|API URL')
.input-group
= platform_field.text_field :api_url, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|API URL'), readonly: cluster.read_only_kubernetes_platform_fields?
- if cluster.read_only_kubernetes_platform_fields?
%span.input-group-append
= clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'), class: 'input-group-text btn-default')
- copy_api_url = clipboard_button(text: platform.api_url, title: s_('ClusterIntegration|Copy API URL'),
class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
= platform_field.text_field :api_url, class: 'js-select-on-focus', required: true,
title: s_('ClusterIntegration|API URL should be a valid http/https url.'),
readonly: cluster.read_only_kubernetes_platform_fields?,
label: s_('ClusterIntegration|API URL'), label_class: 'label-bold',
input_group_class: 'gl-field-error-anchor', append: copy_api_url
- copy_ca_cert_btn = clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'),
class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
= platform_field.text_area :ca_cert, class: 'js-select-on-focus', rows: '5',
readonly: cluster.read_only_kubernetes_platform_fields?,
placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'),
label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold',
input_group_class: 'gl-field-error-anchor', append: copy_ca_cert_btn
.form-group
= platform_field.label :ca_cert, s_('ClusterIntegration|CA Certificate')
.input-group
= platform_field.text_area :ca_cert, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Certificate Authority bundle (PEM format)'), readonly: cluster.read_only_kubernetes_platform_fields?
- if cluster.read_only_kubernetes_platform_fields?
%span.input-group-append.clipboard-addon
= clipboard_button(text: platform.ca_cert, title: s_('ClusterIntegration|Copy CA Certificate'), class: 'input-group-text btn-blank')
- show_token_btn = (platform_field.button s_('ClusterIntegration|Show'),
type: 'button', class: 'js-show-cluster-token btn btn-default')
- copy_token_btn = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Service Token'),
class: 'input-group-text btn-default') unless !cluster.read_only_kubernetes_platform_fields?
.form-group
= platform_field.label :token, s_('ClusterIntegration|Token')
.input-group
= platform_field.text_field :token, class: 'form-control js-cluster-token js-select-on-focus', type: 'password', placeholder: s_('ClusterIntegration|Token'), readonly: cluster.read_only_kubernetes_platform_fields?
%span.input-group-append
%button.btn.btn-default.input-group-text.js-show-cluster-token{ type: 'button' }
= s_('ClusterIntegration|Show')
- if cluster.read_only_kubernetes_platform_fields?
= clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Token'), class: 'btn-default')
= platform_field.text_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token',
required: true, title: s_('ClusterIntegration|Service token is required.'),
readonly: cluster.read_only_kubernetes_platform_fields?,
label: s_('ClusterIntegration|Service Token'), label_class: 'label-bold',
input_group_class: 'gl-field-error-anchor', append: show_token_btn + copy_token_btn
- if cluster.allow_user_defined_namespace?
.form-group
= platform_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)')
= platform_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace')
= platform_field.text_field :namespace, label: s_('ClusterIntegration|Project namespace (optional, unique)'),
label_class: 'label-bold'
.form-group
.form-check
= platform_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac'
= platform_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
= platform_field.form_group :authorization_type do
= platform_field.check_box :authorization_type, { disabled: true, label: s_('ClusterIntegration|RBAC-enabled cluster'),
label_class: 'label-bold', inline: true }, 'rbac', 'abac'
.form-text.text-muted
= s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).')
= s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.')
.form-group
= field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success'
......@@ -6,4 +6,4 @@
This Route Map is invalid:
= viewer.validation_message
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment')
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'going-from-source-files-to-public-pages')
= icon('spinner spin fw')
Validating Route Map…
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment')
= link_to 'Learn more', help_page_path('ci/environments', anchor: 'going-from-source-files-to-public-pages')
......@@ -2,6 +2,7 @@
- add_to_breadcrumbs "Wiki", project_wiki_path(@project, :home)
- breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki")
- sort_title = wiki_sort_title(params[:sort])
%div{ class: container_class }
.wiki-page-header
......@@ -15,6 +16,18 @@
= icon('cloud-download')
= _("Clone repository")
.dropdown.inline.wiki-sort-dropdown
.btn-group{ role: 'group' }
.btn-group{ role: 'group' }
%button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
= sort_title
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(s_("Wiki|Title"), project_wikis_pages_path(@project, sort: ProjectWiki::TITLE_ORDER), sort_title)
= sortable_item(s_("Wiki|Created date"), project_wikis_pages_path(@project, sort: ProjectWiki::CREATED_AT_ORDER), sort_title)
= wiki_sort_controls(@project, params[:sort], params[:direction])
%ul.wiki-pages-list.content-list
= render @wiki_entries, context: 'pages'
......
......@@ -3,7 +3,6 @@
class ReactiveCachingWorker
include ApplicationWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(class_name, id, *args)
klass = begin
class_name.constantize
......@@ -12,7 +11,9 @@ class ReactiveCachingWorker
end
return unless klass
klass.find_by(klass.primary_key => id).try(:exclusively_update_reactive_cache!, *args)
klass
.reactive_cache_worker_finder
.call(id, *args)
.try(:exclusively_update_reactive_cache!, *args)
end
# rubocop: enable CodeReuse/ActiveRecord
end
---
title: Display cluster form validation error messages inline
merge_request: 26502
author:
type: changed
---
title: Allow reactive caching to be used in services
merge_request: 26839
author:
type: added
---
title: Add initial complexity limits to GraphQL queries
merge_request: 26629
author:
type: performance
---
title: Automatically set Prometheus step interval
merge_request: 26441
author:
type: changed
---
title: Allow to use untrusted Regexp via feature flag
merge_request: 26905
author:
type: deprecated
---
title: Drop legacy artifacts usage as there are no leftovers
merge_request: 24294
author:
type: performance
---
title: Fix UI anchor links after docs refactor
merge_request: 26890
author:
type: fixed
---
title: Allow to sort wiki pages by date and title
merge_request: 25365
author:
type: added
---
title: Update GitLab Shell to v9.0.0
merge_request: 27002
author:
type: other
---
title: Group transfer now properly redirects to edit on failure
merge_request: 26837
author:
type: fixed
......@@ -362,6 +362,10 @@ configuring a different storage driver. By default the GitLab Container Registry
is configured to use the filesystem driver, which makes use of [storage path](#container-registry-storage-path)
configuration.
NOTE: **Note:** Enabling a storage driver other than `filesystem` would mean
that your Docker client needs to be able to access the storage backend directly.
In that case, you must use an address that resolves and is accessible outside GitLab server.
The different supported drivers are:
| Driver | Description |
......@@ -369,20 +373,16 @@ The different supported drivers are:
| filesystem | Uses a path on the local filesystem |
| azure | Microsoft Azure Blob Storage |
| gcs | Google Cloud Storage |
| s3 | Amazon Simple Storage Service |
| s3 | Amazon Simple Storage Service. Be sure to configure your storage bucket with the correct [S3 Permission Scopes](https://docs.docker.com/registry/storage-drivers/s3/#s3-permission-scopes). |
| swift | OpenStack Swift Object Storage |
| oss | Aliyun OSS |
Read more about the individual driver's config options in the
[Docker Registry docs][storage-config].
> **Warning** GitLab will not backup Docker images that are not stored on the
CAUTION: **Warning:** GitLab will not backup Docker images that are not stored on the
filesystem. Remember to enable backups with your object storage provider if
desired.
>
> **Important** Enabling storage driver other than `filesystem` would mean
that your Docker client needs to be able to access the storage backend directly.
So you must use an address that resolves and is accessible outside GitLab server.
---
......
......@@ -414,6 +414,27 @@ job:
only: ['branches', 'tags']
```
### Supported `only`/`except` regexp syntax
CAUTION: **Warning:**
This is a breaking change that was introduced with GitLab 11.9.4.
In GitLab 11.9.4, GitLab begun internally converting regexp used
in `only` and `except` parameters to [RE2](https://github.com/google/re2/wiki/Syntax).
This means that only subset of features provided by [Ruby Regexp](https://ruby-doc.org/core/Regexp.html)
is supported. [RE2](https://github.com/google/re2/wiki/Syntax) limits the set of features
provided due to computational complexity, which means some features became unavailable in GitLab 11.9.4.
For example, negative lookaheads.
For GitLab versions from 11.9.7 and up to GitLab 12.0, GitLab provides a feature flag that can be
enabled by administrators that allows users to use unsafe regexp syntax. This brings compatibility
with previously allowed syntax version and allows users to gracefully migrate to the new syntax.
```ruby
Feature.enable(:allow_unsafe_ruby_regexp)
```
### `only`/`except` (advanced)
> - `refs` and `kubernetes` policies introduced in GitLab 10.0.
......
......@@ -130,9 +130,9 @@ secure note named **gitlab-{ce,ee} Review App's root password**.
1. Find and open the `task-runner` Deployment, e.g. `review-29951-issu-id2qax-task-runner`.
1. Click on the Pod in the "Managed pods" section, e.g. `review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz`.
1. Click on the `KUBECTL` dropdown, then `Exec` -> `task-runner`.
1. Replace `-c task-runner -- ls` with `-- /srv/gitlab/bin/rails c` from the
1. Replace `-c task-runner -- ls` with `-it -- gitlab-rails console` from the
default command or
- Run `kubectl exec --namespace review-apps-ce -it review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -- /srv/gitlab/bin/rails c`
- Run `kubectl exec --namespace review-apps-ce review-29951-issu-id2qax-task-runner-d5455cc8-2lsvz -it -- gitlab-rails console`
and
- Replace `review-apps-ce` with `review-apps-ee` if the Review App
is running EE, and
......
......@@ -35,7 +35,7 @@ module Gitlab
# patterns can be matched only when branch or tag is used
# the pattern matching does not work for merge requests pipelines
if pipeline.branch? || pipeline.tag?
if regexp = Gitlab::UntrustedRegexp::RubySyntax.fabricate(pattern)
if regexp = Gitlab::UntrustedRegexp::RubySyntax.fabricate(pattern, fallback: true)
regexp.match?(pipeline.ref)
else
pattern == pipeline.ref
......
......@@ -17,7 +17,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
validations do
validates :config, array_of_strings_or_regexps: true
validates :config, array_of_strings_or_regexps_with_fallback: true
end
def value
......@@ -38,7 +38,7 @@ module Gitlab
validate :variables_expressions_syntax
with_options allow_nil: true do
validates :refs, array_of_strings_or_regexps: true
validates :refs, array_of_strings_or_regexps_with_fallback: true
validates :kubernetes, allowed_values: %w[active]
validates :variables, array_of_strings: true
validates :changes, array_of_strings: true
......
......@@ -129,6 +129,12 @@ module Gitlab
end
end
protected
def fallback
false
end
private
def matches_syntax?(value)
......@@ -137,7 +143,7 @@ module Gitlab
def validate_regexp(value)
matches_syntax?(value) &&
Gitlab::UntrustedRegexp::RubySyntax.valid?(value)
Gitlab::UntrustedRegexp::RubySyntax.valid?(value, fallback: fallback)
end
end
......@@ -162,6 +168,14 @@ module Gitlab
end
end
class ArrayOfStringsOrRegexpsWithFallbackValidator < ArrayOfStringsOrRegexpsValidator
protected
def fallback
true
end
end
class ArrayOfStringsOrStringValidator < RegexpValidator
def validate_each(record, attribute, value)
unless validate_array_of_strings_or_string(value)
......
......@@ -47,7 +47,7 @@ module Gitlab
user: build.user.try(:hook_attrs),
runner: build.runner && runner_hook_attrs(build.runner),
artifacts_file: {
filename: build.artifacts_file.filename,
filename: build.artifacts_file&.filename,
size: build.artifacts_size
}
}
......
......@@ -86,9 +86,9 @@ module Gitlab
end
end
def pages(limit: 0)
def pages(limit: 0, sort: nil, direction_desc: false)
wrapped_gitaly_errors do
gitaly_get_all_pages(limit: limit)
gitaly_get_all_pages(limit: limit, sort: sort, direction_desc: direction_desc)
end
end
......@@ -168,8 +168,10 @@ module Gitlab
Gitlab::Git::WikiFile.new(wiki_file)
end
def gitaly_get_all_pages(limit: 0)
gitaly_wiki_client.get_all_pages(limit: limit).map do |wiki_page, version|
def gitaly_get_all_pages(limit: 0, sort: nil, direction_desc: false)
gitaly_wiki_client.get_all_pages(
limit: limit, sort: sort, direction_desc: direction_desc
).map do |wiki_page, version|
Gitlab::Git::WikiPage.new(wiki_page, version)
end
end
......
......@@ -87,8 +87,13 @@ module Gitlab
wiki_page_from_iterator(response)
end
def get_all_pages(limit: 0)
request = Gitaly::WikiGetAllPagesRequest.new(repository: @gitaly_repo, limit: limit)
def get_all_pages(limit: 0, sort: nil, direction_desc: false)
sort_value = Gitaly::WikiGetAllPagesRequest::SortBy.resolve(sort.to_s.upcase.to_sym)
params = { repository: @gitaly_repo, limit: limit, direction_desc: direction_desc }
params[:sort] = sort_value if sort_value
request = Gitaly::WikiGetAllPagesRequest.new(params)
response = GitalyClient.call(@repository.storage, :wiki_service, :wiki_get_all_pages, request, timeout: GitalyClient.medium_timeout)
pages = []
......
......@@ -14,9 +14,10 @@ module Gitlab
end
def authorized_resolve
proc do |obj, args, ctx|
resolved_obj = @old_resolve_proc.call(obj, args, ctx)
checker = build_checker(ctx[:current_user])
proc do |parent_typed_object, args, ctx|
resolved_obj = @old_resolve_proc.call(parent_typed_object, args, ctx)
authorizing_obj = authorize_against(parent_typed_object)
checker = build_checker(ctx[:current_user], authorizing_obj)
if resolved_obj.respond_to?(:then)
resolved_obj.then(&checker)
......@@ -51,22 +52,28 @@ module Gitlab
Array.wrap(@field.metadata[:authorize])
end
def build_checker(current_user)
lambda do |value|
# If it's a built-in/scalar type, authorize using its parent object.
# nil means authorize using the resolved object
def authorize_against(parent_typed_object)
parent_typed_object.object if built_in_type? && parent_typed_object.respond_to?(:object)
end
def build_checker(current_user, authorizing_obj)
lambda do |resolved_obj|
# Load the elements if they were not loaded by BatchLoader yet
value = value.sync if value.respond_to?(:sync)
resolved_obj = resolved_obj.sync if resolved_obj.respond_to?(:sync)
check = lambda do |object|
authorizations.all? do |ability|
Ability.allowed?(current_user, ability, object)
Ability.allowed?(current_user, ability, authorizing_obj || object)
end
end
case value
case resolved_obj
when Array, ActiveRecord::Relation
value.select(&check)
resolved_obj.select(&check)
else
value if check.call(value)
resolved_obj if check.call(resolved_obj)
end
end
end
......@@ -88,6 +95,10 @@ module Gitlab
def node_type_for_basic_connection(type)
type.unwrap
end
def built_in_type?
GraphQL::Schema::BUILT_IN_TYPES.has_value?(node_type_for_basic_connection(@field.type))
end
end
end
end
......
# frozen_string_literal: true
module Gitlab
module Graphql
module QueryAnalyzers
class LogQueryComplexity
class << self
def analyzer
GraphQL::Analysis::QueryComplexity.new do |query, complexity|
# temporary until https://gitlab.com/gitlab-org/gitlab-ce/issues/59587
Rails.logger.info("[GraphQL Query Complexity] #{complexity} | admin? #{query.context[:current_user]&.admin?}")
end
end
end
end
end
end
end
......@@ -6,6 +6,14 @@ module Gitlab
Error = Class.new(StandardError)
QueryError = Class.new(Gitlab::PrometheusClient::Error)
# Target number of data points for `query_range`.
# Please don't exceed the limit of 11000 data points
# See https://github.com/prometheus/prometheus/blob/91306bdf24f5395e2601773316945a478b4b263d/web/api/v1/api.go#L347
QUERY_RANGE_DATA_POINTS = 600
# Minimal value of the `step` parameter for `query_range` in seconds.
QUERY_RANGE_MIN_STEP = 60
attr_reader :rest_client, :headers
def initialize(rest_client)
......@@ -23,12 +31,18 @@ module Gitlab
end
def query_range(query, start: 8.hours.ago, stop: Time.now)
start = start.to_f
stop = stop.to_f
step = self.class.compute_step(start, stop)
get_result('matrix') do
json_api_get('query_range',
query: query,
start: start.to_f,
end: stop.to_f,
step: 1.minute.to_i)
json_api_get(
'query_range',
query: query,
start: start,
end: stop,
step: step
)
end
end
......@@ -40,6 +54,14 @@ module Gitlab
json_api_get('series', 'match': matches, start: start.to_f, end: stop.to_f)
end
def self.compute_step(start, stop)
diff = stop - start
step = (diff / QUERY_RANGE_DATA_POINTS).ceil
[QUERY_RANGE_MIN_STEP, step].max
end
private
def json_api_get(type, args = {})
......
......@@ -6,7 +6,7 @@ module Gitlab
# and converts that to RE2 representation:
# /<regexp>/<flags>
class RubySyntax
PATTERN = %r{^/(?<regexp>.+)/(?<flags>[ismU]*)$}.freeze
PATTERN = %r{^/(?<regexp>.*)/(?<flags>[ismU]*)$}.freeze
# Checks if pattern matches a regexp pattern
# but does not enforce it's validity
......@@ -16,28 +16,47 @@ module Gitlab
# The regexp can match the pattern `/.../`, but may not be fabricatable:
# it can be invalid or incomplete: `/match ( string/`
def self.valid?(pattern)
!!self.fabricate(pattern)
def self.valid?(pattern, fallback: false)
!!self.fabricate(pattern, fallback: fallback)
end
def self.fabricate(pattern)
self.fabricate!(pattern)
def self.fabricate(pattern, fallback: false)
self.fabricate!(pattern, fallback: fallback)
rescue RegexpError
nil
end
def self.fabricate!(pattern)
def self.fabricate!(pattern, fallback: false)
raise RegexpError, 'Pattern is not string!' unless pattern.is_a?(String)
matches = pattern.match(PATTERN)
raise RegexpError, 'Invalid regular expression!' if matches.nil?
expression = matches[:regexp]
flags = matches[:flags]
expression.prepend("(?#{flags})") if flags.present?
begin
create_untrusted_regexp(matches[:regexp], matches[:flags])
rescue RegexpError
raise unless fallback &&
Feature.enabled?(:allow_unsafe_ruby_regexp, default_enabled: false)
UntrustedRegexp.new(expression, multiline: false)
create_ruby_regexp(matches[:regexp], matches[:flags])
end
end
def self.create_untrusted_regexp(pattern, flags)
pattern.prepend("(?#{flags})") if flags.present?
UntrustedRegexp.new(pattern, multiline: false)
end
private_class_method :create_untrusted_regexp
def self.create_ruby_regexp(pattern, flags)
options = 0
options += Regexp::IGNORECASE if flags&.include?('i')
options += Regexp::MULTILINE if flags&.include?('m')
Regexp.new(pattern, options)
end
private_class_method :create_ruby_regexp
end
end
end
# frozen_string_literal: true
module Gitlab
class UrlHelpers
WSS_PROTOCOL = "wss".freeze
def self.as_wss(url)
return unless url.present?
URI.parse(url).tap do |uri|
uri.scheme = WSS_PROTOCOL
end.to_s
rescue URI::InvalidURIError
nil
end
end
end
......@@ -162,16 +162,16 @@ module Gitlab
]
end
def terminal_websocket(terminal)
def channel_websocket(channel)
details = {
'Terminal' => {
'Subprotocols' => terminal[:subprotocols],
'Url' => terminal[:url],
'Header' => terminal[:headers],
'MaxSessionTime' => terminal[:max_session_time]
'Channel' => {
'Subprotocols' => channel[:subprotocols],
'Url' => channel[:url],
'Header' => channel[:headers],
'MaxSessionTime' => channel[:max_session_time]
}
}
details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.key?(:ca_pem)
details['Channel']['CAPem'] = channel[:ca_pem] if channel.key?(:ca_pem)
details
end
......
......@@ -19,6 +19,9 @@ msgstr ""
msgid " Status"
msgstr ""
msgid " Try to %{action} this file again."
msgstr ""
msgid " You need to do this before %{grace_period_deadline}."
msgstr ""
......@@ -126,6 +129,9 @@ msgstr ""
msgid "%{label_for_message} unavailable"
msgstr ""
msgid "%{level_name} visibility has been restricted by the administrator."
msgstr ""
msgid "%{link_start}Read more%{link_end} about role permissions"
msgstr ""
......@@ -351,6 +357,9 @@ msgstr ""
msgid "A user with write access to the source branch selected this option"
msgstr ""
msgid "API Help"
msgstr ""
msgid "About GitLab"
msgstr ""
......@@ -480,6 +489,9 @@ msgstr ""
msgid "Admin Overview"
msgstr ""
msgid "Admin Section"
msgstr ""
msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr ""
......@@ -1719,6 +1731,9 @@ msgstr ""
msgid "ClusterIntegration|API URL"
msgstr ""
msgid "ClusterIntegration|API URL should be a valid http/https url."
msgstr ""
msgid "ClusterIntegration|Add Kubernetes cluster"
msgstr ""
......@@ -1779,6 +1794,9 @@ msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr ""
msgid "ClusterIntegration|Cluster name is required."
msgstr ""
msgid "ClusterIntegration|Clusters are utilized by selecting the nearest ancestor with a matching environment scope. For example, project clusters will override group clusters."
msgstr ""
......@@ -1800,7 +1818,7 @@ msgstr ""
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
msgid "ClusterIntegration|Copy Token"
msgid "ClusterIntegration|Copy Service Token"
msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster"
......@@ -1992,6 +2010,9 @@ msgstr ""
msgid "ClusterIntegration|Number of nodes"
msgstr ""
msgid "ClusterIntegration|Number of nodes must be a numerical value."
msgstr ""
msgid "ClusterIntegration|Please enter access information for your Kubernetes cluster. If you need help, you can read our %{link_to_help_page} on Kubernetes"
msgstr ""
......@@ -2004,9 +2025,6 @@ msgstr ""
msgid "ClusterIntegration|Project cluster"
msgstr ""
msgid "ClusterIntegration|Project namespace"
msgstr ""
msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr ""
......@@ -2073,7 +2091,10 @@ msgstr ""
msgid "ClusterIntegration|Select zone to choose machine type"
msgstr ""
msgid "ClusterIntegration|Service token"
msgid "ClusterIntegration|Service Token"
msgstr ""
msgid "ClusterIntegration|Service token is required."
msgstr ""
msgid "ClusterIntegration|Show"
......@@ -2106,9 +2127,6 @@ msgstr ""
msgid "ClusterIntegration|Toggle Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|Token"
msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
......@@ -4529,6 +4547,9 @@ msgstr ""
msgid "Job"
msgstr ""
msgid "Job Failed #%{build_id}"
msgstr ""
msgid "Job ID"
msgstr ""
......@@ -4774,6 +4795,9 @@ msgstr ""
msgid "Leave"
msgstr ""
msgid "Leave edit mode? All unsaved changes will be lost."
msgstr ""
msgid "Leave group"
msgstr ""
......@@ -4923,6 +4947,9 @@ msgstr ""
msgid "Markdown"
msgstr ""
msgid "Markdown Help"
msgstr ""
msgid "Markdown enabled"
msgstr ""
......@@ -5393,6 +5420,9 @@ msgstr ""
msgid "No files found."
msgstr ""
msgid "No job trace"
msgstr ""
msgid "No labels with such name or description"
msgstr ""
......@@ -5617,6 +5647,9 @@ msgstr ""
msgid "Open in Xcode"
msgstr ""
msgid "Open raw"
msgstr ""
msgid "Open sidebar"
msgstr ""
......@@ -5740,6 +5773,9 @@ msgstr ""
msgid "Permissions"
msgstr ""
msgid "Permissions Help"
msgstr ""
msgid "Permissions, LFS, 2FA"
msgstr ""
......@@ -6016,6 +6052,9 @@ msgstr ""
msgid "Preview"
msgstr ""
msgid "Preview changes"
msgstr ""
msgid "Preview payload"
msgstr ""
......@@ -6547,6 +6586,9 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication."
msgstr ""
msgid "Public Access Help"
msgstr ""
msgid "Public deploy keys (%{deploy_keys_count})"
msgstr ""
......@@ -6577,6 +6619,9 @@ msgstr ""
msgid "README"
msgstr ""
msgid "Rake Tasks Help"
msgstr ""
msgid "Read more"
msgstr ""
......@@ -6672,6 +6717,12 @@ msgstr ""
msgid "Remove project"
msgstr ""
msgid "Remove this label? Are you sure?"
msgstr ""
msgid "Remove this label? This will affect all projects within the group. Are you sure?"
msgstr ""
msgid "Removed group can not be restored!"
msgstr ""
......@@ -6690,6 +6741,9 @@ msgstr ""
msgid "Reopen milestone"
msgstr ""
msgid "Replace"
msgstr ""
msgid "Reply to comment"
msgstr ""
......@@ -6914,6 +6968,9 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
msgid "SSH Keys Help"
msgstr ""
msgid "SSH host keys"
msgstr ""
......@@ -7040,6 +7097,9 @@ msgstr ""
msgid "SearchAutocomplete|in this project"
msgstr ""
msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\""
msgstr ""
msgid "Secret"
msgstr ""
......@@ -7417,6 +7477,9 @@ msgstr ""
msgid "Sort by"
msgstr ""
msgid "Sort direction"
msgstr ""
msgid "SortOptions|Access level, ascending"
msgstr ""
......@@ -7735,6 +7798,9 @@ msgstr ""
msgid "System Hooks"
msgstr ""
msgid "System Hooks Help"
msgstr ""
msgid "System Info"
msgstr ""
......@@ -7867,6 +7933,11 @@ msgstr ""
msgid "Test failed."
msgstr ""
msgid "The %{type} contains the following error:"
msgid_plural "The %{type} contains the following errors:"
msgstr[0] ""
msgstr[1] ""
msgid "The Git LFS objects will <strong>not</strong> be synced."
msgstr ""
......@@ -7909,6 +7980,15 @@ msgstr ""
msgid "The global settings require you to enable Two-Factor Authentication for your account."
msgstr ""
msgid "The group and any internal projects can be viewed by any logged in user."
msgstr ""
msgid "The group and any public projects can be viewed without any authentication."
msgstr ""
msgid "The group and its projects can only be viewed by members."
msgstr ""
msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}."
msgstr ""
......@@ -7987,6 +8067,18 @@ msgstr ""
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
msgid "The snippet can be accessed without any authentication."
msgstr ""
msgid "The snippet is visible only to me."
msgstr ""
msgid "The snippet is visible only to project members."
msgstr ""
msgid "The snippet is visible to any logged in user."
msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr ""
......@@ -8837,6 +8929,9 @@ msgstr ""
msgid "User map"
msgstr ""
msgid "User settings"
msgstr ""
msgid "User was successfully created."
msgstr ""
......@@ -8990,6 +9085,9 @@ msgstr ""
msgid "View it on GitLab"
msgstr ""
msgid "View job trace"
msgstr ""
msgid "View jobs"
msgstr ""
......@@ -9065,6 +9163,9 @@ msgstr ""
msgid "Web terminal"
msgstr ""
msgid "Webhooks Help"
msgstr ""
msgid "When a runner is locked, it cannot be assigned to other projects"
msgstr ""
......@@ -9197,6 +9298,9 @@ msgstr ""
msgid "Wiki|Create page"
msgstr ""
msgid "Wiki|Created date"
msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
......@@ -9215,6 +9319,9 @@ msgstr ""
msgid "Wiki|Pages"
msgstr ""
msgid "Wiki|Title"
msgstr ""
msgid "Wiki|Wiki Pages"
msgstr ""
......@@ -9224,6 +9331,9 @@ msgstr ""
msgid "Withdraw Access Request"
msgstr ""
msgid "Workflow Help"
msgstr ""
msgid "Write"
msgstr ""
......@@ -9428,6 +9538,12 @@ msgstr ""
msgid "You'll need to use different branch names to get a valid comparison."
msgstr ""
msgid "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
msgstr ""
msgid "You're not allowed to make changes to this project directly. A fork of this project is being created that you can make changes in, so you can submit a merge request."
msgstr ""
msgid "You're only seeing %{startTag}other activity%{endTag} in the feed. To add a comment, switch to one of the following options."
msgstr ""
......
......@@ -6,7 +6,7 @@ module QA
view 'app/helpers/blob_helper.rb' do
element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern
element :delete_button, /label:\s+"Delete"/ # rubocop:disable QA/ElementWithPattern
element :delete_button, '_("Delete")' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/projects/blob/_remove.html.haml' do
......
......@@ -6,7 +6,7 @@ module QA
class AddExisting < Page::Base
view 'app/views/clusters/clusters/user/_form.html.haml' do
element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern
element :api_url, 'url_field :api_url' # rubocop:disable QA/ElementWithPattern
element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern
element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern
......
......@@ -8,11 +8,11 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
deploy_token_name = 'deploy token name'
deploy_token_expires_at = Date.today + 7 # 1 Week from now
one_week_from_now = Date.today + 7
deploy_token = Resource::DeployToken.fabricate! do |resource|
resource.name = deploy_token_name
resource.expires_at = deploy_token_expires_at
resource.expires_at = one_week_from_now
end
expect(deploy_token.username.length).to be > 0
......
......@@ -117,7 +117,7 @@ describe IssuableCollections do
due_date: '2017-01-01',
group_id: '3',
iids: '4',
label_name: 'foo',
label_name: ['foo'],
milestone_title: 'bar',
my_reaction_emoji: 'thumbsup',
non_archived: 'true',
......@@ -142,7 +142,7 @@ describe IssuableCollections do
'author_id' => '2',
'author_username' => 'user2',
'confidential' => true,
'label_name' => 'foo',
'label_name' => ['foo'],
'milestone_title' => 'bar',
'my_reaction_emoji' => 'thumbsup',
'due_date' => '2017-01-01',
......
......@@ -23,4 +23,37 @@ describe DashboardController do
it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
describe "#check_filters_presence!" do
let(:user) { create(:user) }
before do
sign_in(user)
get :merge_requests, params: params
end
context "no filters" do
let(:params) { {} }
it 'sets @no_filters_set to false' do
expect(assigns[:no_filters_set]).to eq(true)
end
end
context "scalar filters" do
let(:params) { { author_id: user.id } }
it 'sets @no_filters_set to false' do
expect(assigns[:no_filters_set]).to eq(false)
end
end
context "array filters" do
let(:params) { { label_name: ['bug'] } }
it 'sets @no_filters_set to false' do
expect(assigns[:no_filters_set]).to eq(false)
end
end
end
end
......@@ -616,7 +616,7 @@ describe GroupsController do
end
it 'should redirect to the current path' do
expect(response).to render_template(:edit)
expect(response).to redirect_to(edit_group_path(group))
end
end
......
......@@ -283,7 +283,7 @@ describe Projects::EnvironmentsController do
.and_return([:fake_terminal])
expect(Gitlab::Workhorse)
.to receive(:terminal_websocket)
.to receive(:channel_websocket)
.with(:fake_terminal)
.and_return(workhorse: :response)
......
......@@ -989,7 +989,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'and valid id' do
it 'returns the terminal for the job' do
expect(Gitlab::Workhorse)
.to receive(:terminal_websocket)
.to receive(:channel_websocket)
.and_return(workhorse: :response)
get_terminal_websocket(id: job.id)
......
......@@ -44,6 +44,8 @@ describe 'Dashboard Merge Requests' do
end
context 'merge requests exist' do
let(:label) { create(:label) }
let!(:assigned_merge_request) do
create(:merge_request,
assignee: current_user,
......@@ -72,6 +74,14 @@ describe 'Dashboard Merge Requests' do
target_project: public_project, source_project: forked_project)
end
let!(:labeled_merge_request) do
create(:labeled_merge_request,
source_branch: 'labeled',
labels: [label],
author: current_user,
source_project: project)
end
let!(:other_merge_request) do
create(:merge_request,
source_branch: 'fix',
......@@ -90,6 +100,7 @@ describe 'Dashboard Merge Requests' do
expect(page).not_to have_content(authored_merge_request.title)
expect(page).not_to have_content(authored_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title)
expect(page).not_to have_content(labeled_merge_request.title)
end
it 'shows authored merge requests', :js do
......@@ -98,7 +109,21 @@ describe 'Dashboard Merge Requests' do
expect(page).to have_content(authored_merge_request.title)
expect(page).to have_content(authored_merge_request_from_fork.title)
expect(page).to have_content(labeled_merge_request.title)
expect(page).not_to have_content(assigned_merge_request.title)
expect(page).not_to have_content(assigned_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title)
end
it 'shows labeled merge requests', :js do
reset_filters
input_filtered_search("label:#{label.name}")
expect(page).to have_content(labeled_merge_request.title)
expect(page).not_to have_content(authored_merge_request.title)
expect(page).not_to have_content(authored_merge_request_from_fork.title)
expect(page).not_to have_content(assigned_merge_request.title)
expect(page).not_to have_content(assigned_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title)
......
......@@ -69,7 +69,7 @@ describe 'User Cluster', :js do
end
it 'user sees a validation error' do
expect(page).to have_css('#error_explanation')
expect(page).to have_css('.gl-field-error')
end
end
end
......
......@@ -43,6 +43,7 @@ describe 'Issues > User uses quick actions', :js do
describe 'issue-only commands' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
before do
project.add_maintainer(user)
......@@ -55,6 +56,8 @@ describe 'Issues > User uses quick actions', :js do
wait_for_requests
end
it_behaves_like 'confidential quick action'
describe 'adding a due date from note' do
let(:issue) { create(:issue, project: project) }
......@@ -137,42 +140,6 @@ describe 'Issues > User uses quick actions', :js do
end
end
describe 'make issue confidential' do
let(:issue) { create(:issue, project: project) }
let(:original_issue) { create(:issue, project: project) }
context 'when the current user can update issues' do
it 'does not create a note, and marks the issue as confidential' do
add_note("/confidential")
expect(page).not_to have_content "/confidential"
expect(page).to have_content 'Commands applied'
expect(page).to have_content "made the issue confidential"
expect(issue.reload).to be_confidential
end
end
context 'when the current user cannot update the issue' do
let(:guest) { create(:user) }
before do
project.add_guest(guest)
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
end
it 'does not create a note, and does not mark the issue as confidential' do
add_note("/confidential")
expect(page).not_to have_content 'Commands applied'
expect(page).not_to have_content "made the issue confidential"
expect(issue.reload).not_to be_confidential
end
end
end
describe 'move the issue to another project' do
let(:issue) { create(:issue, project: project) }
......
......@@ -92,7 +92,7 @@ describe 'Gcp Cluster', :js do
end
it 'user sees a validation error' do
expect(page).to have_css('#error_explanation')
expect(page).to have_css('.gl-field-error')
end
end
end
......
......@@ -53,7 +53,7 @@ describe 'User Cluster', :js do
end
it 'user sees a validation error' do
expect(page).to have_css('#error_explanation')
expect(page).to have_css('.gl-field-error')
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'User views wiki pages' do
include WikiHelpers
let(:user) { create(:user) }
let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page1) do
create(:wiki_page, wiki: project.wiki, attrs: { title: '3 home', content: '3' })
end
let!(:wiki_page2) do
create(:wiki_page, wiki: project.wiki, attrs: { title: '1 home', content: '1' })
end
let!(:wiki_page3) do
create(:wiki_page, wiki: project.wiki, attrs: { title: '2 home', content: '2' })
end
let(:pages) do
page.find('.wiki-pages-list').all('li').map { |li| li.find('a') }
end
before do
project.add_maintainer(user)
sign_in(user)
visit(project_wikis_pages_path(project))
end
context 'ordered by title' do
let(:pages_ordered_by_title) { [wiki_page2, wiki_page3, wiki_page1] }
context 'asc' do
it 'pages are displayed in direct order' do
pages.each.with_index do |page_title, index|
expect(page_title.text).to eq(pages_ordered_by_title[index].title)
end
end
end
context 'desc' do
before do
page.within('.wiki-sort-dropdown') do
page.find('.qa-reverse-sort').click
end
end
it 'pages are displayed in reversed order' do
pages.reverse_each.with_index do |page_title, index|
expect(page_title.text).to eq(pages_ordered_by_title[index].title)
end
end
end
end
context 'ordered by created_at' do
let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] }
before do
page.within('.wiki-sort-dropdown') do
click_button('Title')
click_link('Created date')
end
end
context 'asc' do
it 'pages are displayed in direct order' do
pages.each.with_index do |page_title, index|
expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
end
end
end
context 'desc' do
before do
page.within('.wiki-sort-dropdown') do
page.find('.qa-reverse-sort').click
end
end
it 'pages are displayed in reversed order' do
pages.reverse_each.with_index do |page_title, index|
expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
end
end
end
end
end
......@@ -75,6 +75,59 @@ describe 'Gitlab::Graphql::Authorization' do
end
end
describe 'Field authorizations when field is a built in type' do
let(:query_type) do
query_factory do |query|
query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
end
end
describe 'with a single permission' do
let(:type) do
type_factory do |type|
type.field :name, GraphQL::STRING_TYPE, null: true, authorize: permission_single
end
end
it 'returns the protected field when user has permission' do
permit(permission_single)
expect(subject).to eq('name' => test_object.name)
end
it 'returns nil when user is not authorized' do
expect(subject).to eq('name' => nil)
end
end
describe 'with a collection of permissions' do
let(:type) do
permissions = permission_collection
type_factory do |type|
type.field :name, GraphQL::STRING_TYPE, null: true do
authorize permissions
end
end
end
it 'returns the protected field when user has all permissions' do
permit(*permission_collection)
expect(subject).to eq('name' => test_object.name)
end
it 'returns nil when user only has one of the permissions' do
permit(permission_collection.first)
expect(subject).to eq('name' => nil)
end
it 'returns nil when user only has none of the permissions' do
expect(subject).to eq('name' => nil)
end
end
end
describe 'Type authorizations' do
let(:query_type) do
query_factory do |query|
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema do
......@@ -31,6 +33,46 @@ describe GitlabSchema do
expect(connection).to eq(Gitlab::Graphql::Connections::KeysetConnection)
end
context 'for different types of users' do
it 'returns DEFAULT_MAX_COMPLEXITY for no context' do
expect(GraphQL::Schema)
.to receive(:execute)
.with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY))
described_class.execute('query')
end
it 'returns DEFAULT_MAX_COMPLEXITY for no user' do
expect(GraphQL::Schema)
.to receive(:execute)
.with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY))
described_class.execute('query', context: {})
end
it 'returns AUTHENTICATED_COMPLEXITY for a logged in user' do
user = build :user
expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::AUTHENTICATED_COMPLEXITY))
described_class.execute('query', context: { current_user: user })
end
it 'returns ADMIN_COMPLEXITY for an admin user' do
user = build :user, :admin
expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::ADMIN_COMPLEXITY))
described_class.execute('query', context: { current_user: user })
end
it 'returns what was passed on the query' do
expect(GraphQL::Schema).to receive(:execute).with('query', { max_complexity: 1234 })
described_class.execute('query', max_complexity: 1234)
end
end
def field_instrumenters
described_class.instrumenters[:field]
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Types::BaseField do
context 'when considering complexity' do
it 'defaults to 1' do
field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true)
expect(field.to_graphql.complexity).to eq 1
end
it 'has specified value' do
field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12)
expect(field.to_graphql.complexity).to eq 12
end
end
end
......@@ -18,4 +18,56 @@ describe WikiHelper do
end
end
end
describe '#wiki_sort_controls' do
let(:project) { create(:project) }
let(:wiki_link) { helper.wiki_sort_controls(project, sort, direction) }
let(:classes) { "btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort" }
def expected_link(sort, direction, icon_class)
path = "/#{project.full_path}/wikis/pages?direction=#{direction}&sort=#{sort}"
helper.link_to(path, type: 'button', class: classes, title: 'Sort direction') do
helper.sprite_icon("sort-#{icon_class}", size: 16)
end
end
context 'initial call' do
let(:sort) { nil }
let(:direction) { nil }
it 'renders with default values' do
expect(wiki_link).to eq(expected_link('title', 'desc', 'lowest'))
end
end
context 'sort by title' do
let(:sort) { 'title' }
let(:direction) { 'asc' }
it 'renders a link with opposite direction' do
expect(wiki_link).to eq(expected_link('title', 'desc', 'lowest'))
end
end
context 'sort by created_at' do
let(:sort) { 'created_at' }
let(:direction) { 'desc' }
it 'renders a link with opposite direction' do
expect(wiki_link).to eq(expected_link('created_at', 'asc', 'highest'))
end
end
end
describe '#wiki_sort_title' do
it 'returns a title corresponding to a key' do
expect(helper.wiki_sort_title('created_at')).to eq('Created date')
expect(helper.wiki_sort_title('title')).to eq('Title')
end
it 'defaults to Title if a key is unknown' do
expect(helper.wiki_sort_title('unknown')).to eq('Title')
end
end
end
......@@ -11,6 +11,8 @@ describe('IDE pipelines list', () => {
let vm;
let mock;
const findLoadingState = () => vm.$el.querySelector('.loading-container');
beforeEach(done => {
const store = createStore();
......@@ -95,7 +97,7 @@ describe('IDE pipelines list', () => {
describe('empty state', () => {
it('renders pipelines empty state', done => {
vm.$store.state.pipelines.latestPipeline = false;
vm.$store.state.pipelines.latestPipeline = null;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.empty-state')).not.toBe(null);
......@@ -106,15 +108,30 @@ describe('IDE pipelines list', () => {
});
describe('loading state', () => {
it('renders loading state when there is no latest pipeline', done => {
vm.$store.state.pipelines.latestPipeline = null;
beforeEach(() => {
vm.$store.state.pipelines.isLoadingPipeline = true;
});
vm.$nextTick(() => {
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
it('does not render when pipeline has loaded before', done => {
vm.$store.state.pipelines.hasLoadedPipeline = true;
done();
});
vm.$nextTick()
.then(() => {
expect(findLoadingState()).toBe(null);
})
.then(done)
.catch(done.fail);
});
it('renders loading state when there is no latest pipeline', done => {
vm.$store.state.pipelines.hasLoadedPipeline = false;
vm.$nextTick()
.then(() => {
expect(findLoadingState()).not.toBe(null);
})
.then(done)
.catch(done.fail);
});
});
});
......@@ -27,63 +27,71 @@ describe('IDE pipelines mutations', () => {
});
describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
it('sets loading to false on success', () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
mockedState,
fullPipelinesResponse.data.pipelines[0],
);
const itSetsPipelineLoadingStates = () => {
it('sets has loaded to true', () => {
expect(mockedState.hasLoadedPipeline).toBe(true);
});
expect(mockedState.isLoadingPipeline).toBe(false);
});
it('sets loading to false on success', () => {
expect(mockedState.isLoadingPipeline).toBe(false);
});
};
describe('with pipeline', () => {
beforeEach(() => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
mockedState,
fullPipelinesResponse.data.pipelines[0],
);
});
it('sets latestPipeline', () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
mockedState,
fullPipelinesResponse.data.pipelines[0],
);
itSetsPipelineLoadingStates();
expect(mockedState.latestPipeline).toEqual({
id: '51',
path: 'test',
commit: { id: '123' },
details: { status: jasmine.any(Object) },
yamlError: undefined,
it('sets latestPipeline', () => {
expect(mockedState.latestPipeline).toEqual({
id: '51',
path: 'test',
commit: { id: '123' },
details: { status: jasmine.any(Object) },
yamlError: undefined,
});
});
});
it('does not set latest pipeline if pipeline is null', () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
expect(mockedState.latestPipeline).toEqual(false);
it('sets stages', () => {
expect(mockedState.stages.length).toBe(2);
expect(mockedState.stages).toEqual([
{
id: 0,
dropdownPath: stages[0].dropdown_path,
name: stages[0].name,
status: stages[0].status,
isCollapsed: false,
isLoading: false,
jobs: [],
},
{
id: 1,
dropdownPath: stages[1].dropdown_path,
name: stages[1].name,
status: stages[1].status,
isCollapsed: false,
isLoading: false,
jobs: [],
},
]);
});
});
it('sets stages', () => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
mockedState,
fullPipelinesResponse.data.pipelines[0],
);
describe('with null', () => {
beforeEach(() => {
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
});
expect(mockedState.stages.length).toBe(2);
expect(mockedState.stages).toEqual([
{
id: 0,
dropdownPath: stages[0].dropdown_path,
name: stages[0].name,
status: stages[0].status,
isCollapsed: false,
isLoading: false,
jobs: [],
},
{
id: 1,
dropdownPath: stages[1].dropdown_path,
name: stages[1].name,
status: stages[1].status,
isCollapsed: false,
isLoading: false,
jobs: [],
},
]);
itSetsPipelineLoadingStates();
it('does not set latest pipeline if pipeline is null', () => {
expect(mockedState.latestPipeline).toEqual(null);
});
});
});
......
......@@ -101,6 +101,32 @@ describe Gitlab::Ci::Build::Policy::Refs do
expect(described_class.new(['/fix-.*/']))
.not_to be_satisfied_by(pipeline)
end
context 'when unsafe regexp is used' do
let(:subject) { described_class.new(['/^(?!master).+/']) }
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
it 'ignores invalid regexp' do
expect(subject)
.not_to be_satisfied_by(pipeline)
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is satisfied by regexp' do
expect(subject)
.to be_satisfied_by(pipeline)
end
end
end
end
context 'malicious regexp' do
......
require 'fast_spec_helper'
require 'support/helpers/stub_feature_flags'
require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Policy do
......@@ -33,6 +34,44 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
context 'when config is an empty regexp' do
let(:config) { ['//'] }
describe '#valid?' do
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when using unsafe regexp' do
include StubFeatureFlags
let(:config) { ['/^(?!master).+/'] }
subject { described_class.new([regexp]) }
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
it 'is not valid' do
expect(entry).not_to be_valid
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when config is a special keyword' do
let(:config) { %w[tags triggers branches] }
......@@ -67,6 +106,34 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
context 'when using unsafe regexp' do
include StubFeatureFlags
let(:config) { { refs: ['/^(?!master).+/'] } }
subject { described_class.new([regexp]) }
context 'when allow_unsafe_ruby_regexp is disabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: false)
end
it 'is not valid' do
expect(entry).not_to be_valid
end
end
context 'when allow_unsafe_ruby_regexp is enabled' do
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
end
it 'is valid' do
expect(entry).to be_valid
end
end
end
context 'when specifying kubernetes policy' do
let(:config) { { kubernetes: 'active' } }
......
......@@ -9,55 +9,88 @@ describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
let(:current_user) { double(:current_user) }
let(:abilities) { [double(:first_ability), double(:last_ability)] }
let(:checker) do
service = described_class.new(double(resolve_proc: proc {}))
allow(service).to receive(:authorizations).and_return(abilities)
service.__send__(:build_checker, current_user)
end
context 'when authorizing against the object' do
let(:checker) do
service = described_class.new(double(resolve_proc: proc {}))
allow(service).to receive(:authorizations).and_return(abilities)
service.__send__(:build_checker, current_user, nil)
end
it 'returns a checker which checks for a single object' do
object = double(:object)
it 'returns a checker which checks for a single object' do
object = double(:object)
abilities.each do |ability|
spy_ability_check_for(ability, object, passed: true)
end
abilities.each do |ability|
spy_ability_check_for(ability, object, passed: true)
end
expect(checker.call(object)).to eq(object)
end
expect(checker.call(object)).to eq(object)
end
it 'returns a checker which checks for all objects' do
objects = [double(:first), double(:last)]
it 'returns a checker which checks for all objects' do
objects = [double(:first), double(:last)]
abilities.each do |ability|
objects.each do |object|
spy_ability_check_for(ability, object, passed: true)
abilities.each do |ability|
objects.each do |object|
spy_ability_check_for(ability, object, passed: true)
end
end
expect(checker.call(objects)).to eq(objects)
end
expect(checker.call(objects)).to eq(objects)
end
context 'when some objects would not pass the check' do
it 'returns nil when it is single object' do
disallowed = double(:object)
spy_ability_check_for(abilities.first, disallowed, passed: false)
context 'when some objects would not pass the check' do
it 'returns nil when it is single object' do
disallowed = double(:object)
expect(checker.call(disallowed)).to be_nil
end
it 'returns only objects which passed when there are more than one' do
allowed = double(:allowed)
disallowed = double(:disallowed)
spy_ability_check_for(abilities.first, disallowed, passed: false)
spy_ability_check_for(abilities.first, disallowed, passed: false)
expect(checker.call(disallowed)).to be_nil
abilities.each do |ability|
spy_ability_check_for(ability, allowed, passed: true)
end
expect(checker.call([disallowed, allowed])).to contain_exactly(allowed)
end
end
end
context 'when authorizing against another object' do
let(:authorizing_obj) { double(:object) }
it 'returns only objects which passed when there are more than one' do
allowed = double(:allowed)
disallowed = double(:disallowed)
let(:checker) do
service = described_class.new(double(resolve_proc: proc {}))
allow(service).to receive(:authorizations).and_return(abilities)
service.__send__(:build_checker, current_user, authorizing_obj)
end
it 'returns a checker which checks for a single object' do
object = double(:object)
abilities.each do |ability|
spy_ability_check_for(ability, authorizing_obj, passed: true)
end
expect(checker.call(object)).to eq(object)
end
spy_ability_check_for(abilities.first, disallowed, passed: false)
it 'returns a checker which checks for all objects' do
objects = [double(:first), double(:last)]
abilities.each do |ability|
spy_ability_check_for(ability, allowed, passed: true)
objects.each do |object|
spy_ability_check_for(ability, authorizing_obj, passed: true)
end
end
expect(checker.call([disallowed, allowed]))
.to contain_exactly(allowed)
expect(checker.call(objects)).to eq(objects)
end
end
end
......
......@@ -230,4 +230,32 @@ describe Gitlab::PrometheusClient do
let(:execute_query) { subject.query_range(prometheus_query) }
end
end
describe '.compute_step' do
using RSpec::Parameterized::TableSyntax
let(:now) { Time.now.utc }
subject { described_class.compute_step(start, stop) }
where(:time_interval_in_seconds, :step) do
0 | 60
10.hours | 60
10.hours + 1 | 61
# frontend options
30.minutes | 60
3.hours | 60
8.hours | 60
1.day | 144
3.days | 432
1.week | 1008
end
with_them do
let(:start) { now - time_interval_in_seconds }
let(:stop) { now }
it { is_expected.to eq(step) }
end
end
end
require 'fast_spec_helper'
require 'support/shared_examples/malicious_regexp_shared_examples'
require 'support/helpers/stub_feature_flags'
describe Gitlab::UntrustedRegexp::RubySyntax do
describe '.matches_syntax?' do
......@@ -33,6 +34,12 @@ describe Gitlab::UntrustedRegexp::RubySyntax do
end
end
context 'when regexp is empty' do
it 'fabricates regexp correctly' do
expect(described_class.fabricate('//')).not_to be_nil
end
end
context 'when regexp is a raw pattern' do
it 'returns error' do
expect(described_class.fabricate('some .* thing')).to be_nil
......@@ -41,24 +48,63 @@ describe Gitlab::UntrustedRegexp::RubySyntax do
end
describe '.fabricate!' do
context 'when regexp is using /regexp/ scheme with flags' do
it 'fabricates regexp with a single flag' do
regexp = described_class.fabricate!('/something/i')
context 'safe regexp is used' do
context 'when regexp is using /regexp/ scheme with flags' do
it 'fabricates regexp with a single flag' do
regexp = described_class.fabricate!('/something/i')
expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?i)something')
expect(regexp.scan('SOMETHING')).to be_one
end
expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?i)something')
expect(regexp.scan('SOMETHING')).to be_one
it 'fabricates regexp with multiple flags' do
regexp = described_class.fabricate!('/something/im')
expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?im)something')
end
it 'fabricates regexp without flags' do
regexp = described_class.fabricate!('/something/')
expect(regexp).to eq Gitlab::UntrustedRegexp.new('something')
end
end
end
it 'fabricates regexp with multiple flags' do
regexp = described_class.fabricate!('/something/im')
context 'when unsafe regexp is used' do
include StubFeatureFlags
expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?im)something')
before do
stub_feature_flags(allow_unsafe_ruby_regexp: true)
allow(Gitlab::UntrustedRegexp).to receive(:new).and_raise(RegexpError)
end
it 'fabricates regexp without flags' do
regexp = described_class.fabricate!('/something/')
context 'when no fallback is enabled' do
it 'raises an exception' do
expect { described_class.fabricate!('/something/') }
.to raise_error(RegexpError)
end
end
context 'when fallback is used' do
it 'fabricates regexp with a single flag' do
regexp = described_class.fabricate!('/something/i', fallback: true)
expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE)
end
it 'fabricates regexp with multiple flags' do
regexp = described_class.fabricate!('/something/im', fallback: true)
expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE | Regexp::MULTILINE)
end
it 'fabricates regexp without flags' do
regexp = described_class.fabricate!('/something/', fallback: true)
expect(regexp).to eq Gitlab::UntrustedRegexp.new('something')
expect(regexp).to eq Regexp.new('something')
end
end
end
......
......@@ -94,7 +94,7 @@ describe Gitlab::Workhorse do
end
end
describe '.terminal_websocket' do
describe '.channel_websocket' do
def terminal(ca_pem: nil)
out = {
subprotocols: ['foo'],
......@@ -108,25 +108,25 @@ describe Gitlab::Workhorse do
def workhorse(ca_pem: nil)
out = {
'Terminal' => {
'Channel' => {
'Subprotocols' => ['foo'],
'Url' => 'wss://example.com/terminal.ws',
'Header' => { 'Authorization' => ['Token x'] },
'MaxSessionTime' => 600
}
}
out['Terminal']['CAPem'] = ca_pem if ca_pem
out['Channel']['CAPem'] = ca_pem if ca_pem
out
end
context 'without ca_pem' do
subject { described_class.terminal_websocket(terminal) }
subject { described_class.channel_websocket(terminal) }
it { is_expected.to eq(workhorse) }
end
context 'with ca_pem' do
subject { described_class.terminal_websocket(terminal(ca_pem: "foo")) }
subject { described_class.channel_websocket(terminal(ca_pem: "foo")) }
it { is_expected.to eq(workhorse(ca_pem: "foo")) }
end
......
......@@ -13,25 +13,33 @@ describe Ci::BuildRunnerSession, model: true do
it { is_expected.to validate_presence_of(:url).with_message('must be a valid URL') }
describe '#terminal_specification' do
let(:terminal_specification) { subject.terminal_specification }
let(:specification) { subject.terminal_specification }
it 'returns terminal.gitlab.com protocol' do
expect(specification[:subprotocols]).to eq ['terminal.gitlab.com']
end
it 'returns a wss url' do
expect(specification[:url]).to start_with('wss://')
end
it 'returns empty hash if no url' do
subject.url = ''
expect(terminal_specification).to be_empty
expect(specification).to be_empty
end
context 'when url is present' do
it 'returns ca_pem nil if empty certificate' do
subject.certificate = ''
expect(terminal_specification[:ca_pem]).to be_nil
expect(specification[:ca_pem]).to be_nil
end
it 'adds Authorization header if authorization is present' do
subject.authorization = 'whatever'
expect(terminal_specification[:headers]).to include(Authorization: ['whatever'])
expect(specification[:headers]).to include(Authorization: ['whatever'])
end
end
end
......
......@@ -117,6 +117,16 @@ describe Ci::Build do
it 'returns the job' do
is_expected.to include(job)
end
context 'when ci_enable_legacy_artifacts feature flag is disabled' do
before do
stub_feature_flags(ci_enable_legacy_artifacts: false)
end
it 'does not return the job' do
is_expected.not_to include(job)
end
end
end
context 'when job has a job artifact archive' do
......@@ -471,6 +481,14 @@ describe Ci::Build do
let(:build) { create(:ci_build, :legacy_artifacts) }
it { is_expected.to be_truthy }
context 'when ci_enable_legacy_artifacts feature flag is disabled' do
before do
stub_feature_flags(ci_enable_legacy_artifacts: false)
end
it { is_expected.to be_falsy }
end
end
end
end
......
......@@ -16,6 +16,10 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
attr_reader :id
def self.primary_key
:id
end
def initialize(id, &blk)
@id = id
@calculator = blk
......@@ -106,6 +110,46 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
end
describe '.reactive_cache_worker_finder' do
context 'with default reactive_cache_worker_finder' do
let(:args) { %w(other args) }
before do
allow(instance.class).to receive(:find_by).with(id: instance.id)
.and_return(instance)
end
it 'calls the activerecord find_by method' do
result = instance.class.reactive_cache_worker_finder.call(instance.id, *args)
expect(result).to eq(instance)
expect(instance.class).to have_received(:find_by).with(id: instance.id)
end
end
context 'with custom reactive_cache_worker_finder' do
let(:args) { %w(arg1 arg2) }
let(:instance) { CustomFinderCacheTest.new(666, &calculation) }
class CustomFinderCacheTest < CacheTest
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
def self.from_cache(*args); end
end
before do
allow(instance.class).to receive(:from_cache).with(*args).and_return(instance)
end
it 'overrides the default reactive_cache_worker_finder' do
result = instance.class.reactive_cache_worker_finder.call(instance.id, *args)
expect(result).to eq(instance)
expect(instance.class).to have_received(:from_cache).with(*args)
end
end
end
describe '#clear_reactive_cache!' do
before do
stub_reactive_cache(instance, 4)
......
......@@ -20,12 +20,16 @@ describe WikiPage do
context 'when there are pages' do
before do
create_page('dir_1/dir_1_1/page_3', 'content')
create_page('page_1', 'content')
create_page('dir_1/page_2', 'content')
create_page('dir_2/page_5', 'content')
create_page('page_6', 'content')
create_page('dir_2/page_4', 'content')
create_page('page_1', 'content')
end
let(:page_1) { wiki.find_page('page_1') }
let(:page_6) { wiki.find_page('page_6') }
let(:dir_1) do
WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')])
end
......@@ -38,25 +42,38 @@ describe WikiPage do
WikiDirectory.new('dir_2', pages)
end
it 'returns an array with pages and directories' do
expected_grouped_entries = [page_1, dir_1, dir_1_1, dir_2]
context 'sort by title' do
let(:grouped_entries) { described_class.group_by_directory(wiki.pages) }
let(:expected_grouped_entries) { [dir_1_1, dir_1, dir_2, page_1, page_6] }
grouped_entries = described_class.group_by_directory(wiki.pages)
it 'returns an array with pages and directories' do
grouped_entries.each_with_index do |page_or_dir, i|
expected_page_or_dir = expected_grouped_entries[i]
expected_slugs = get_slugs(expected_page_or_dir)
slugs = get_slugs(page_or_dir)
grouped_entries.each_with_index do |page_or_dir, i|
expected_page_or_dir = expected_grouped_entries[i]
expected_slugs = get_slugs(expected_page_or_dir)
slugs = get_slugs(page_or_dir)
expect(slugs).to match_array(expected_slugs)
end
end
end
context 'sort by created_at' do
let(:grouped_entries) { described_class.group_by_directory(wiki.pages(sort: 'created_at')) }
let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, dir_2, page_6] }
expect(slugs).to match_array(expected_slugs)
it 'returns an array with pages and directories' do
grouped_entries.each_with_index do |page_or_dir, i|
expected_page_or_dir = expected_grouped_entries[i]
expected_slugs = get_slugs(expected_page_or_dir)
slugs = get_slugs(page_or_dir)
expect(slugs).to match_array(expected_slugs)
end
end
end
it 'returns an array sorted by alphabetical position' do
# Directories and pages within directories are sorted alphabetically.
# Pages at root come before everything.
expected_order = ['page_1', 'dir_1/page_2', 'dir_1/dir_1_1/page_3',
'dir_2/page_4', 'dir_2/page_5']
it 'returns an array with retained order with directories at the top' do
expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']
grouped_entries = described_class.group_by_directory(wiki.pages)
......
require 'spec_helper'
describe 'GitlabSchema configurations' do
include GraphqlHelpers
let(:project) { create(:project, :repository) }
let!(:query) { graphql_query_for('project', 'fullPath' => project.full_path) }
it 'shows an error if complexity it too high' do
allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
post_graphql(query, current_user: nil)
expect(graphql_errors.first['message']).to include('which exceeds max complexity of 1')
end
end
......@@ -93,6 +93,8 @@ module GraphqlHelpers
end
def all_graphql_fields_for(class_name, parent_types = Set.new)
allow_unlimited_graphql_complexity
type = GitlabSchema.types[class_name.to_s]
return "" unless type
......@@ -170,4 +172,10 @@ module GraphqlHelpers
field_type
end
# for most tests, we want to allow unlimited complexity
def allow_unlimited_graphql_complexity
allow_any_instance_of(GitlabSchema).to receive(:max_complexity).and_return nil
allow(GitlabSchema).to receive(:max_query_complexity).with(any_args).and_return nil
end
end
......@@ -25,12 +25,16 @@ module PrometheusHelpers
"https://prometheus.example.com/api/v1/query?#{query}"
end
def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now.to_f)
def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now, step: nil)
start = start.to_f
stop = stop.to_f
step ||= Gitlab::PrometheusClient.compute_step(start, stop)
query = {
query: prometheus_query,
start: start.to_f,
start: start,
end: stop,
step: 1.minute.to_i
step: step
}.to_query
"https://prometheus.example.com/api/v1/query_range?#{query}"
......
# frozen_string_literal: true
shared_examples 'confidential quick action' do
context 'when the current user can update issues' do
it 'does not create a note, and marks the issue as confidential' do
add_note('/confidential')
expect(page).not_to have_content '/confidential'
expect(page).to have_content 'Commands applied'
expect(page).to have_content 'made the issue confidential'
expect(issue.reload).to be_confidential
end
end
context 'when the current user cannot update the issue' do
let(:guest) { create(:user) }
before do
project.add_guest(guest)
gitlab_sign_out
sign_in(guest)
visit project_issue_path(project, issue)
end
it 'does not create a note, and does not mark the issue as confidential' do
add_note('/confidential')
expect(page).not_to have_content 'Commands applied'
expect(page).not_to have_content 'made the issue confidential'
expect(issue.reload).not_to be_confidential
end
end
end
......@@ -645,17 +645,17 @@
dependencies:
bootstrap "^4.1.3"
"@gitlab/eslint-config@^1.4.0":
version "1.4.0"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.4.0.tgz#2e59e55a7cd024e3a450d2a896060ec4d763a5dc"
integrity sha512-nkecTWRNS/KD9q5lHFSc3J6zO/g1/OV9DaKiay+0nLjnGO9jQVRArRIYpnzgbUz2p15jOMVToVafW0YbbHZkwg==
"@gitlab/eslint-config@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@gitlab/eslint-config/-/eslint-config-1.5.0.tgz#0c8c3ae74f276eb6671bd7c60f331bc0f2d2e5cf"
integrity sha512-KgJgoIZNpGauFpCV1iCptesYN7I8abtYRBLU9xcH0oocC/xp3JmbLfsZ+lEtrk8pl99Q2mKiAuaPpzxjXr6hBw==
dependencies:
babel-eslint "^10.0.1"
eslint-config-airbnb-base "^13.1.0"
eslint-config-prettier "^3.3.0"
eslint-plugin-filenames "^1.3.2"
eslint-plugin-import "^2.14.0"
eslint-plugin-promise "^4.0.1"
eslint-plugin-import "^2.16.0"
eslint-plugin-promise "^4.1.1"
eslint-plugin-vue "^5.0.0"
"@gitlab/svgs@^1.58.0":
......@@ -3665,7 +3665,7 @@ eslint-import-resolver-jest@^2.1.1:
micromatch "^3.1.6"
resolve "^1.5.0"
eslint-import-resolver-node@^0.3.1:
eslint-import-resolver-node@^0.3.1, eslint-import-resolver-node@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==
......@@ -3697,6 +3697,14 @@ eslint-module-utils@^2.2.0:
debug "^2.6.8"
pkg-dir "^1.0.0"
eslint-module-utils@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.3.0.tgz#546178dab5e046c8b562bbb50705e2456d7bda49"
integrity sha512-lmDJgeOOjk8hObTysjqH7wyMi+nsHwwvfBykwfhjR1LNdd7C2uFJBvx4OpWYpXOw4df1yE1cDEVd1yLHitk34w==
dependencies:
debug "^2.6.8"
pkg-dir "^2.0.0"
eslint-plugin-filenames@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-filenames/-/eslint-plugin-filenames-1.3.2.tgz#7094f00d7aefdd6999e3ac19f72cea058e590cf7"
......@@ -3730,6 +3738,22 @@ eslint-plugin-import@^2.14.0:
read-pkg-up "^2.0.0"
resolve "^1.6.0"
eslint-plugin-import@^2.16.0:
version "2.16.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.16.0.tgz#97ac3e75d0791c4fac0e15ef388510217be7f66f"
integrity sha512-z6oqWlf1x5GkHIFgrSvtmudnqM6Q60KM4KvpWi5ubonMjycLjndvd5+8VAZIsTlHC03djdgJuyKG6XO577px6A==
dependencies:
contains-path "^0.1.0"
debug "^2.6.9"
doctrine "1.5.0"
eslint-import-resolver-node "^0.3.2"
eslint-module-utils "^2.3.0"
has "^1.0.3"
lodash "^4.17.11"
minimatch "^3.0.4"
read-pkg-up "^2.0.0"
resolve "^1.9.0"
eslint-plugin-jasmine@^2.10.1:
version "2.10.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jasmine/-/eslint-plugin-jasmine-2.10.1.tgz#5733b709e751f4bc40e31e1c16989bd2cdfbec97"
......@@ -3740,10 +3764,10 @@ eslint-plugin-jest@^22.3.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2"
integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA==
eslint-plugin-promise@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.0.1.tgz#2d074b653f35a23d1ba89d8e976a985117d1c6a2"
integrity sha512-Si16O0+Hqz1gDHsys6RtFRrW7cCTB6P7p3OJmKp3Y3dxpQE2qwOA7d3xnV+0mBmrPoi0RBnxlCKvqu70te6wjg==
eslint-plugin-promise@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db"
integrity sha512-faAHw7uzlNPy7b45J1guyjazw28M+7gJokKUjC5JSFoYfUEyy6Gw/i7YQvmv2Yk00sUjWcmzXQLpU1Ki/C2IZQ==
eslint-plugin-vue@^5.0.0:
version "5.0.0"
......@@ -4909,6 +4933,13 @@ has@^1.0.1:
dependencies:
function-bind "^1.0.2"
has@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796"
integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==
dependencies:
function-bind "^1.1.1"
hash-base@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-2.0.2.tgz#66ea1d856db4e8a5470cadf6fce23ae5244ef2e1"
......@@ -8087,6 +8118,13 @@ pkg-dir@^1.0.0:
dependencies:
find-up "^1.0.0"
pkg-dir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b"
integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=
dependencies:
find-up "^2.1.0"
pkg-dir@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
......@@ -9094,7 +9132,7 @@ resolve@1.1.7, resolve@1.1.x:
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=
resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0:
resolve@1.x, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.9.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba"
integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment