Commit 17ab40ca authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 66d42037
...@@ -50,10 +50,6 @@ After your merge request has being approved according to our [approval guideline ...@@ -50,10 +50,6 @@ After your merge request has being approved according to our [approval guideline
| -------- | -------- | | -------- | -------- |
| Issue on [GitLab](https://gitlab.com/gitlab-org/gitlab/issues) | #TODO | | Issue on [GitLab](https://gitlab.com/gitlab-org/gitlab/issues) | #TODO |
| Security Release tracking issue | #TODO | | Security Release tracking issue | #TODO |
| `master` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
| `Backport X.Y` MR | !TODO |
### Details ### Details
......
...@@ -8,11 +8,11 @@ See [the general developer security release guidelines](https://gitlab.com/gitla ...@@ -8,11 +8,11 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
## Related issues ## Related issues
<!-- Mention the issue(s) this MR is related to --> <!-- Mention the GitLab Security issue this MR is related to -->
## Developer checklist ## Developer checklist
- [ ] Link this MR in the `links` section of the related issue on [GitLab Security]. - [ ] **Make sure this merge request mentions the [GitLab Security] issue it belongs to (i.e. `Related to <issue_id>`).**
- [ ] Merge request targets `master`, or `X-Y-stable` for backports. - [ ] Merge request targets `master`, or `X-Y-stable` for backports.
- [ ] Milestone is set for the version this merge request applies to. A closed milestone can be assigned via [quick actions]. - [ ] Milestone is set for the version this merge request applies to. A closed milestone can be assigned via [quick actions].
- [ ] Title of this merge request is the same as for all backports. - [ ] Title of this merge request is the same as for all backports.
......
...@@ -349,8 +349,8 @@ RSpec/HaveGitlabHttpStatus: ...@@ -349,8 +349,8 @@ RSpec/HaveGitlabHttpStatus:
- 'ee/spec/requests/{groups,projects,repositories}/**/*' - 'ee/spec/requests/{groups,projects,repositories}/**/*'
- 'spec/requests/api/*/**/*.rb' - 'spec/requests/api/*/**/*.rb'
- 'ee/spec/requests/api/*/**/*.rb' - 'ee/spec/requests/api/*/**/*.rb'
- 'spec/requests/api/[a-l]*.rb' - 'spec/requests/api/[a-o]*.rb'
- 'ee/spec/requests/api/[a-l]*.rb' - 'ee/spec/requests/api/[a-o]*.rb'
Style/MultilineWhenThen: Style/MultilineWhenThen:
Enabled: false Enabled: false
......
...@@ -44,6 +44,7 @@ class DueDateSelect { ...@@ -44,6 +44,7 @@ class DueDateSelect {
this.$selectbox.hide(); this.$selectbox.hide();
this.$value.css('display', ''); this.$value.css('display', '');
}, },
shouldPropagate: false,
}); });
} }
......
<script> <script>
import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '../../locale'; import { __ } from '../../locale';
import Icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
import getRefMixin from '../mixins/get_ref'; import getRefMixin from '../mixins/get_ref';
...@@ -102,12 +103,12 @@ export default { ...@@ -102,12 +103,12 @@ export default {
.filter(p => p !== '') .filter(p => p !== '')
.reduce( .reduce(
(acc, name, i) => { (acc, name, i) => {
const path = `${i > 0 ? acc[i].path : ''}/${name}`; const path = joinPaths(i > 0 ? acc[i].path : '', encodeURIComponent(name));
return acc.concat({ return acc.concat({
name, name,
path, path,
to: `/-/tree/${escape(this.ref)}${escape(path)}`, to: `/-/tree/${joinPaths(escape(this.ref), path)}`,
}); });
}, },
[ [
......
...@@ -91,7 +91,9 @@ export default { ...@@ -91,7 +91,9 @@ export default {
}, },
computed: { computed: {
routerLinkTo() { routerLinkTo() {
return this.isFolder ? { path: `/-/tree/${escape(this.ref)}/${escape(this.path)}` } : null; return this.isFolder
? { path: `/-/tree/${escape(this.ref)}/${encodeURIComponent(this.path)}` }
: null;
}, },
iconName() { iconName() {
return `fa-${getIconName(this.type, this.path)}`; return `fa-${getIconName(this.type, this.path)}`;
...@@ -141,6 +143,7 @@ export default { ...@@ -141,6 +143,7 @@ export default {
<i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i> <i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
<component <component
:is="linkComponent" :is="linkComponent"
ref="link"
:to="routerLinkTo" :to="routerLinkTo"
:href="url" :href="url"
class="str-truncated" class="str-truncated"
......
...@@ -27,7 +27,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) { ...@@ -27,7 +27,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
fetchpromise = axios fetchpromise = axios
.get( .get(
`${gon.relative_url_root}/${projectPath}/-/refs/${escape(ref)}/logs_tree/${escape( `${gon.relative_url_root}/${projectPath}/-/refs/${escape(ref)}/logs_tree/${encodeURIComponent(
path.replace(/^\//, ''), path.replace(/^\//, ''),
)}`, )}`,
{ {
......
...@@ -3,8 +3,8 @@ import { s__ } from '~/locale'; ...@@ -3,8 +3,8 @@ import { s__ } from '~/locale';
export const PAGINATION_UI_BUTTON_LIMIT = 4; export const PAGINATION_UI_BUTTON_LIMIT = 4;
export const UI_LIMIT = 6; export const UI_LIMIT = 6;
export const SPREAD = '...'; export const SPREAD = '...';
export const PREV = s__('Pagination|Prev'); export const PREV = s__('Pagination|Prev');
export const NEXT = s__('Pagination|Next'); export const NEXT = s__('Pagination|Next');
export const FIRST = s__('Pagination|« First'); export const FIRST = s__('Pagination|« First');
export const LAST = s__('Pagination|Last »'); export const LAST = s__('Pagination|Last »');
export const LABEL_FIRST_PAGE = s__('Pagination|Go to first page'); export const LABEL_FIRST_PAGE = s__('Pagination|Go to first page');
......
...@@ -22,6 +22,16 @@ module Projects ...@@ -22,6 +22,16 @@ module Projects
end end
end end
def update
result = ::Metrics::Dashboard::UpdateDashboardService.new(project, current_user, dashboard_params.merge(file_content_params)).execute
if result[:status] == :success
respond_update_success(result)
else
respond_error(result)
end
end
private private
def respond_success(result) def respond_success(result)
...@@ -43,6 +53,19 @@ module Projects ...@@ -43,6 +53,19 @@ module Projects
flash[:notice] = message.html_safe flash[:notice] = message.html_safe
end end
def respond_update_success(result)
set_web_ide_link_update_notice(result.dig(:dashboard, :path))
respond_to do |format|
format.json { render status: result.delete(:http_status), json: result }
end
end
def set_web_ide_link_update_notice(new_dashboard_path)
web_ide_link_start = "<a href=\"#{ide_edit_path(project, redirect_safe_branch_name, new_dashboard_path)}\">"
message = _("Your dashboard has been updated. You can %{web_ide_link_start}edit it here%{web_ide_link_end}.") % { web_ide_link_start: web_ide_link_start, web_ide_link_end: "</a>" }
flash[:notice] = message.html_safe
end
def validate_required_params! def validate_required_params!
params.require(%i(branch file_name dashboard commit_message)) params.require(%i(branch file_name dashboard commit_message))
end end
...@@ -54,6 +77,31 @@ module Projects ...@@ -54,6 +77,31 @@ module Projects
def dashboard_params def dashboard_params
params.permit(%i(branch file_name dashboard commit_message)).to_h params.permit(%i(branch file_name dashboard commit_message)).to_h
end end
def file_content_params
params.permit(
file_content: [
:dashboard,
panel_groups: [
:group,
:priority,
panels: [
:type,
:title,
:y_label,
:weight,
metrics: [
:id,
:unit,
:label,
:query,
:query_range
]
]
]
]
)
end
end end
end end
end end
...@@ -90,6 +90,12 @@ module Ci ...@@ -90,6 +90,12 @@ module Ci
end end
end end
def needs_attributes
strong_memoize(:needs_attributes) do
needs.map { |need| need.attributes.except('id', 'build_id') }
end
end
private private
def validate_scheduling_type? def validate_scheduling_type?
......
...@@ -52,6 +52,13 @@ module ReactiveCaching ...@@ -52,6 +52,13 @@ module ReactiveCaching
end end
end end
def with_reactive_cache_set(resource, opts, &blk)
data = with_reactive_cache(resource, opts, &blk)
save_keys_in_set(resource, opts) if data
data
end
# This method is used for debugging purposes and should not be used otherwise. # This method is used for debugging purposes and should not be used otherwise.
def without_reactive_cache(*args, &blk) def without_reactive_cache(*args, &blk)
return with_reactive_cache(*args, &blk) unless Rails.env.development? return with_reactive_cache(*args, &blk) unless Rails.env.development?
...@@ -65,6 +72,12 @@ module ReactiveCaching ...@@ -65,6 +72,12 @@ module ReactiveCaching
Rails.cache.delete(alive_reactive_cache_key(*args)) Rails.cache.delete(alive_reactive_cache_key(*args))
end end
def clear_reactive_cache_set!(*args)
cache_key = full_reactive_cache_key(args)
reactive_set_cache.clear_cache!(cache_key)
end
def exclusively_update_reactive_cache!(*args) def exclusively_update_reactive_cache!(*args)
locking_reactive_cache(*args) do locking_reactive_cache(*args) do
key = full_reactive_cache_key(*args) key = full_reactive_cache_key(*args)
...@@ -86,6 +99,16 @@ module ReactiveCaching ...@@ -86,6 +99,16 @@ module ReactiveCaching
private private
def save_keys_in_set(resource, opts)
cache_key = full_reactive_cache_key(resource)
reactive_set_cache.write(cache_key, "#{cache_key}:#{opts}")
end
def reactive_set_cache
Gitlab::ReactiveCacheSetCache.new(expires_in: reactive_cache_lifetime)
end
def refresh_reactive_cache!(*args) def refresh_reactive_cache!(*args)
clear_reactive_cache!(*args) clear_reactive_cache!(*args)
keep_alive_reactive_cache!(*args) keep_alive_reactive_cache!(*args)
......
...@@ -85,7 +85,7 @@ module ErrorTracking ...@@ -85,7 +85,7 @@ module ErrorTracking
end end
def list_sentry_issues(opts = {}) def list_sentry_issues(opts = {})
with_reactive_cache('list_issues', opts.stringify_keys) do |result| with_reactive_cache_set('list_issues', opts.stringify_keys) do |result|
result result
end end
end end
...@@ -130,6 +130,10 @@ module ErrorTracking ...@@ -130,6 +130,10 @@ module ErrorTracking
end end
end end
def expire_issues_cache
clear_reactive_cache_set!('list_issues')
end
# http://HOST/api/0/projects/ORG/PROJECT # http://HOST/api/0/projects/ORG/PROJECT
# -> # ->
# http://HOST/ORG/PROJECT # http://HOST/ORG/PROJECT
......
# frozen_string_literal: true # frozen_string_literal: true
module Ci module Ci
class CreateJobArtifactsService class CreateJobArtifactsService < ::BaseService
ArtifactsExistError = Class.new(StandardError) ArtifactsExistError = Class.new(StandardError)
OBJECT_STORAGE_ERRORS = [
Errno::EIO,
Google::Apis::ServerError,
Signet::RemoteServerError
].freeze
def execute(job, artifacts_file, params, metadata_file: nil) def execute(job, artifacts_file, params, metadata_file: nil)
expire_in = params['expire_in'] || expire_in = params['expire_in'] ||
...@@ -26,18 +31,20 @@ module Ci ...@@ -26,18 +31,20 @@ module Ci
expire_in: expire_in) expire_in: expire_in)
end end
job.update(artifacts_expire_in: expire_in) if job.update(artifacts_expire_in: expire_in)
rescue ActiveRecord::RecordNotUnique => error success
return true if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file) else
error(job.errors.messages, :bad_request)
end
Gitlab::ErrorTracking.track_exception(error, rescue ActiveRecord::RecordNotUnique => error
job_id: job.id, return success if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file)
project_id: job.project_id,
uploading_type: params['artifact_type']
)
job.errors.add(:base, 'another artifact of the same type already exists') track_exception(error, job, params)
false error('another artifact of the same type already exists', :bad_request)
rescue *OBJECT_STORAGE_ERRORS => error
track_exception(error, job, params)
error(error.message, :service_unavailable)
end end
private private
...@@ -48,5 +55,13 @@ module Ci ...@@ -48,5 +55,13 @@ module Ci
existing_artifact.file_sha256 == artifacts_file.sha256 existing_artifact.file_sha256 == artifacts_file.sha256
end end
def track_exception(error, job, params)
Gitlab::ErrorTracking.track_exception(error,
job_id: job.id,
project_id: job.project_id,
uploading_type: params['artifact_type']
)
end
end end
end end
...@@ -5,7 +5,8 @@ module Ci ...@@ -5,7 +5,8 @@ module Ci
CLONE_ACCESSORS = %i[pipeline project ref tag options name CLONE_ACCESSORS = %i[pipeline project ref tag options name
allow_failure stage stage_id stage_idx trigger_request allow_failure stage stage_id stage_idx trigger_request
yaml_variables when environment coverage_regex yaml_variables when environment coverage_regex
description tag_list protected needs resource_group scheduling_type].freeze description tag_list protected needs_attributes
resource_group scheduling_type].freeze
def execute(build) def execute(build)
reprocess!(build).tap do |new_build| reprocess!(build).tap do |new_build|
......
...@@ -11,6 +11,7 @@ module ErrorTracking ...@@ -11,6 +11,7 @@ module ErrorTracking
) )
compose_response(response) do compose_response(response) do
project_error_tracking_setting.expire_issues_cache
response[:closed_issue_iid] = update_related_issue&.iid response[:closed_issue_iid] = update_related_issue&.iid
end end
end end
......
# frozen_string_literal: true
# Updates the content of a specified dashboard in .yml file inside `.gitlab/dashboards`
module Metrics
module Dashboard
class UpdateDashboardService < ::BaseService
ALLOWED_FILE_TYPE = '.yml'
USER_DASHBOARDS_DIR = ::Metrics::Dashboard::ProjectDashboardService::DASHBOARD_ROOT
def execute
catch(:error) do
throw(:error, error(_(%q(You can't commit to this project)), :forbidden)) unless push_authorized?
result = ::Files::UpdateService.new(project, current_user, dashboard_attrs).execute
throw(:error, result.merge(http_status: :bad_request)) unless result[:status] == :success
success(result.merge(http_status: :created, dashboard: dashboard_details))
end
end
private
def dashboard_attrs
{
commit_message: params[:commit_message],
file_path: update_dashboard_path,
file_content: update_dashboard_content,
encoding: 'text',
branch_name: branch,
start_branch: repository.branch_exists?(branch) ? branch : project.default_branch
}
end
def dashboard_details
{
path: update_dashboard_path,
display_name: ::Metrics::Dashboard::ProjectDashboardService.name_for_path(update_dashboard_path),
default: false,
system_dashboard: false
}
end
def push_authorized?
Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch)
end
def branch
@branch ||= begin
throw(:error, error(_('There was an error updating the dashboard, branch name is invalid.'), :bad_request)) unless valid_branch_name?
throw(:error, error(_('There was an error updating the dashboard, branch named: %{branch} already exists.') % { branch: params[:branch] }, :bad_request)) unless new_or_default_branch?
params[:branch]
end
end
def new_or_default_branch?
!repository.branch_exists?(params[:branch]) || project.default_branch == params[:branch]
end
def valid_branch_name?
Gitlab::GitRefValidator.validate(params[:branch])
end
def update_dashboard_path
File.join(USER_DASHBOARDS_DIR, file_name)
end
def target_file_type_valid?
File.extname(params[:file_name]) == ALLOWED_FILE_TYPE
end
def file_name
@file_name ||= begin
throw(:error, error(_('The file name should have a .yml extension'), :bad_request)) unless target_file_type_valid?
File.basename(CGI.unescape(params[:file_name]))
end
end
def update_dashboard_content
::PerformanceMonitoring::PrometheusDashboard.from_json(params[:file_content]).to_yaml
end
def repository
@repository ||= project.repository
end
end
end
end
---
title: Added the multiSelect option to stop event propagation when clicking on the
dropdown
merge_request: 24611
author: Gwen_
type: fixed
---
title: Don't show issue as blocked on the issue board if blocking issue is closed
merge_request: 25817
author:
type: fixed
---
title: Keep needs association on the retried build
merge_request: 25888
author:
type: fixed
---
title: Update file content of an existing custom dashboard
merge_request: 25024
author:
type: added
---
title: Return 503 to the Runner when the object storage is unavailable
merge_request: 25822
author:
type: fixed
---
title: Fixed repository browsing for folders with non-ascii characters
merge_request: 25877
author:
type: fixed
---
title: Remove special chars from previous and next items in pagination
merge_request: 25891
author:
type: other
...@@ -252,7 +252,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -252,7 +252,11 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
namespace :performance_monitoring do namespace :performance_monitoring do
resources :dashboards, only: [:create] resources :dashboards, only: [:create] do
collection do
put '/:file_name', to: 'dashboards#update', constraints: { file_name: /.+\.yml/ }
end
end
end end
namespace :error_tracking do namespace :error_tracking do
......
...@@ -85,13 +85,27 @@ the following feature flags are enabled on your GitLab instance: ...@@ -85,13 +85,27 @@ the following feature flags are enabled on your GitLab instance:
- `:ci_use_merge_request_ref` - `:ci_use_merge_request_ref`
- `:merge_ref_auto_sync` - `:merge_ref_auto_sync`
To check these feature flag values, please ask administrator to execute the following commands: To check and set these feature flag values, please ask an administrator to:
```shell 1. Log into the Rails console of the GitLab instance:
> sudo gitlab-rails console # Login to Rails console of GitLab instance.
> Feature.enabled?(:ci_use_merge_request_ref) # Check if it's enabled or not. ```shell
> Feature.enable(:ci_use_merge_request_ref) # Enable the feature flag. sudo gitlab-rails console
``` ```
1. Check if the flags are enabled or not:
```ruby
Feature.enabled?(:ci_use_merge_request_ref)
Feature.enabled?(:merge_ref_auto_sync)
```
1. If needed, enable the feature flags:
```ruby
Feature.enable(:ci_use_merge_request_ref)
Feature.enable(:merge_ref_auto_sync)
```
### Intermittently pipelines fail by `fatal: reference is not a tree:` error ### Intermittently pipelines fail by `fatal: reference is not a tree:` error
......
...@@ -283,10 +283,12 @@ module API ...@@ -283,10 +283,12 @@ module API
bad_request!('Missing artifacts file!') unless artifacts bad_request!('Missing artifacts file!') unless artifacts
file_too_large! unless artifacts.size < max_artifacts_size(job) file_too_large! unless artifacts.size < max_artifacts_size(job)
if Ci::CreateJobArtifactsService.new.execute(job, artifacts, params, metadata_file: metadata) result = Ci::CreateJobArtifactsService.new(job.project).execute(job, artifacts, params, metadata_file: metadata)
if result[:status] == :success
status :created status :created
else else
render_validation_error!(job) render_api_error!(result[:message], result[:http_status])
end end
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
module Gitlab module Gitlab
module MarkdownCache module MarkdownCache
# Increment this number every time the renderer changes its output # Increment this number every time the renderer changes its output
CACHE_COMMONMARK_VERSION = 19 CACHE_COMMONMARK_VERSION = 20
CACHE_COMMONMARK_VERSION_START = 10 CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError) BaseError = Class.new(StandardError)
......
...@@ -10,9 +10,13 @@ module Gitlab ...@@ -10,9 +10,13 @@ module Gitlab
@expires_in = expires_in @expires_in = expires_in
end end
def cache_key(key)
"#{cache_type}:#{key}:set"
end
def clear_cache!(key) def clear_cache!(key)
with do |redis| with do |redis|
keys = read(key).map { |value| "#{cache_type}#{value}" } keys = read(key).map { |value| "#{cache_type}:#{value}" }
keys << cache_key(key) keys << cache_key(key)
redis.pipelined do redis.pipelined do
...@@ -24,7 +28,7 @@ module Gitlab ...@@ -24,7 +28,7 @@ module Gitlab
private private
def cache_type def cache_type
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:" Gitlab::Redis::Cache::CACHE_NAMESPACE
end end
end end
end end
...@@ -69,7 +69,7 @@ module Gitlab ...@@ -69,7 +69,7 @@ module Gitlab
end end
def entry_path(entry) def entry_path(entry)
File.join(*[path, entry[:file_name]].compact) File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT)
end end
def build_entry(entry) def build_entry(entry)
......
...@@ -13658,18 +13658,12 @@ msgstr "" ...@@ -13658,18 +13658,12 @@ msgstr ""
msgid "Pagination|Next" msgid "Pagination|Next"
msgstr "" msgstr ""
msgid "Pagination|Next ›"
msgstr ""
msgid "Pagination|Prev" msgid "Pagination|Prev"
msgstr "" msgstr ""
msgid "Pagination|« First" msgid "Pagination|« First"
msgstr "" msgstr ""
msgid "Pagination|‹ Prev"
msgstr ""
msgid "Parameter" msgid "Parameter"
msgstr "" msgstr ""
...@@ -18615,6 +18609,9 @@ msgstr "" ...@@ -18615,6 +18609,9 @@ msgstr ""
msgid "Subscription deletion failed." msgid "Subscription deletion failed."
msgstr "" msgstr ""
msgid "Subscription successfully applied to \"%{group_name}\""
msgstr ""
msgid "Subscription successfully created." msgid "Subscription successfully created."
msgstr "" msgstr ""
...@@ -19633,6 +19630,12 @@ msgstr "" ...@@ -19633,6 +19630,12 @@ msgstr ""
msgid "There was an error trying to validate your query" msgid "There was an error trying to validate your query"
msgstr "" msgstr ""
msgid "There was an error updating the dashboard, branch name is invalid."
msgstr ""
msgid "There was an error updating the dashboard, branch named: %{branch} already exists."
msgstr ""
msgid "There was an error when reseting email token." msgid "There was an error when reseting email token."
msgstr "" msgstr ""
...@@ -22692,6 +22695,9 @@ msgstr "" ...@@ -22692,6 +22695,9 @@ msgstr ""
msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}." msgid "Your dashboard has been copied. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
msgstr "" msgstr ""
msgid "Your dashboard has been updated. You can %{web_ide_link_start}edit it here%{web_ide_link_end}."
msgstr ""
msgid "Your deployment services will be broken, you will need to manually fix the services after renaming." msgid "Your deployment services will be broken, you will need to manually fix the services after renaming."
msgstr "" msgstr ""
......
...@@ -129,4 +129,130 @@ describe Projects::PerformanceMonitoring::DashboardsController do ...@@ -129,4 +129,130 @@ describe Projects::PerformanceMonitoring::DashboardsController do
end end
end end
end end
describe 'PUT #update' do
context 'authenticated user' do
before do
sign_in(user)
end
let(:file_content) do
{
"dashboard" => "Dashboard Title",
"panel_groups" => [{
"group" => "Group Title",
"panels" => [{
"type" => "area-chart",
"title" => "Chart Title",
"y_label" => "Y-Axis",
"metrics" => [{
"id" => "metric_of_ages",
"unit" => "count",
"label" => "Metric of Ages",
"query_range" => "http_requests_total"
}]
}]
}]
}
end
let(:params) do
{
namespace_id: namespace,
project_id: project,
dashboard: dashboard,
file_name: file_name,
file_content: file_content,
commit_message: commit_message,
branch: branch_name,
format: :json
}
end
context 'project with repository feature' do
context 'with rights to push to the repository' do
before do
project.add_maintainer(user)
end
context 'valid parameters' do
context 'request format json' do
let(:update_dashboard_service_params) { params.except(:namespace_id, :project_id, :format) }
let(:update_dashboard_service_results) do
{
status: :success,
http_status: :created,
dashboard: {
path: ".gitlab/dashboards/custom_dashboard.yml",
display_name: "custom_dashboard.yml",
default: false,
system_dashboard: false
}
}
end
let(:update_dashboard_service) { instance_double(::Metrics::Dashboard::UpdateDashboardService, execute: update_dashboard_service_results) }
it 'returns path to new file' do
allow(controller).to receive(:repository).and_return(repository)
allow(repository).to receive(:find_branch).and_return(branch)
allow(::Metrics::Dashboard::UpdateDashboardService).to receive(:new).with(project, user, update_dashboard_service_params).and_return(update_dashboard_service)
put :update, params: params
expect(response).to have_gitlab_http_status :created
expect(response).to set_flash[:notice].to eq("Your dashboard has been updated. You can <a href=\"/-/ide/project/#{namespace.path}/#{project.name}/edit/#{branch_name}/-/.gitlab/dashboards/#{file_name}\">edit it here</a>.")
expect(json_response).to eq('status' => 'success', 'dashboard' => { 'default' => false, 'display_name' => "custom_dashboard.yml", 'path' => ".gitlab/dashboards/#{file_name}", 'system_dashboard' => false })
end
context 'UpdateDashboardService failure' do
it 'returns json with failure message' do
allow(::Metrics::Dashboard::UpdateDashboardService).to receive(:new).and_return(double(execute: { status: :error, message: 'something went wrong', http_status: :bad_request }))
put :update, params: params
expect(response).to have_gitlab_http_status :bad_request
expect(json_response).to eq('error' => 'something went wrong')
end
end
end
end
context 'missing branch' do
let(:branch_name) { nil }
it 'raises responds with :bad_request status code and error message' do
put :update, params: params
expect(response).to have_gitlab_http_status :bad_request
expect(json_response).to eq('error' => "Request parameter branch is missing.")
end
end
end
context 'without rights to push to repository' do
before do
project.add_guest(user)
end
it 'responds with :forbidden status code' do
put :update, params: params
expect(response).to have_gitlab_http_status :forbidden
end
end
end
context 'project without repository feature' do
let!(:project) { create(:project, name: 'dashboard-project', namespace: namespace) }
it 'responds with :not_found status code' do
put :update, params: params
expect(response).to have_gitlab_http_status :not_found
end
end
end
end
end end
...@@ -24,7 +24,7 @@ describe 'Thread Comments Commit', :js do ...@@ -24,7 +24,7 @@ describe 'Thread Comments Commit', :js do
expect(page).to have_css('.js-note-emoji') expect(page).to have_css('.js-note-emoji')
end end
it 'adds award to the correct note' do it 'adds award to the correct note', quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/207973' do
find("#note_#{commit_discussion_note2.id} .js-note-emoji").click find("#note_#{commit_discussion_note2.id} .js-note-emoji").click
first('.emoji-menu .js-emoji-btn').click first('.emoji-menu .js-emoji-btn').click
......
...@@ -37,6 +37,16 @@ describe 'Projects tree', :js do ...@@ -37,6 +37,16 @@ describe 'Projects tree', :js do
expect(page).not_to have_selector('.flash-alert') expect(page).not_to have_selector('.flash-alert')
end end
it 'renders tree table with non-ASCII filenames without errors' do
visit project_tree_path(project, File.join(test_sha, 'encoding'))
wait_for_requests
expect(page).to have_selector('.tree-item')
expect(page).to have_content('Files, encoding and much more')
expect(page).to have_content('テスト.txt')
expect(page).not_to have_selector('.flash-alert')
end
context 'gravatar disabled' do context 'gravatar disabled' do
let(:gravatar_enabled) { false } let(:gravatar_enabled) { false }
......
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import GkeZoneDropdown from '~/create_cluster/gke_cluster/components/gke_zone_dropdown.vue'; import GkeZoneDropdown from '~/create_cluster/gke_cluster/components/gke_zone_dropdown.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import { createStore } from '~/create_cluster/gke_cluster/store'; import { createStore } from '~/create_cluster/gke_cluster/store';
import { import {
SET_PROJECT, SET_PROJECT,
...@@ -9,7 +10,7 @@ import { ...@@ -9,7 +10,7 @@ import {
} from '~/create_cluster/gke_cluster/store/mutation_types'; } from '~/create_cluster/gke_cluster/store/mutation_types';
import { selectedZoneMock, selectedProjectMock, gapiZonesResponseMock } from '../mock_data'; import { selectedZoneMock, selectedProjectMock, gapiZonesResponseMock } from '../mock_data';
const componentConfig = { const propsData = {
fieldId: 'cluster_provider_gcp_attributes_gcp_zone', fieldId: 'cluster_provider_gcp_attributes_gcp_zone',
fieldName: 'cluster[provider_gcp_attributes][gcp_zone]', fieldName: 'cluster[provider_gcp_attributes][gcp_zone]',
}; };
...@@ -20,75 +21,81 @@ const LABELS = { ...@@ -20,75 +21,81 @@ const LABELS = {
DEFAULT: 'Select zone', DEFAULT: 'Select zone',
}; };
const createComponent = (store, props = componentConfig) => {
const Component = Vue.extend(GkeZoneDropdown);
return mountComponentWithStore(Component, {
el: null,
props,
store,
});
};
describe('GkeZoneDropdown', () => { describe('GkeZoneDropdown', () => {
let vm;
let store; let store;
let wrapper;
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
vm = createComponent(store); wrapper = shallowMount(GkeZoneDropdown, { propsData, store });
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
}); });
describe('toggleText', () => { describe('toggleText', () => {
let dropdownButton;
beforeEach(() => {
dropdownButton = wrapper.find(DropdownButton);
});
it('returns disabled state toggle text', () => { it('returns disabled state toggle text', () => {
expect(vm.toggleText).toBe(LABELS.DISABLED); expect(dropdownButton.props('toggleText')).toBe(LABELS.DISABLED);
}); });
it('returns loading toggle text', () => { describe('isLoading', () => {
vm.isLoading = true; beforeEach(() => {
wrapper.setData({ isLoading: true });
return wrapper.vm.$nextTick();
});
expect(vm.toggleText).toBe(LABELS.LOADING); it('returns loading toggle text', () => {
expect(dropdownButton.props('toggleText')).toBe(LABELS.LOADING);
});
}); });
it('returns default toggle text', () => { describe('project is set', () => {
expect(vm.toggleText).toBe(LABELS.DISABLED); beforeEach(() => {
wrapper.vm.$store.commit(SET_PROJECT, selectedProjectMock);
wrapper.vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
return wrapper.vm.$nextTick();
});
vm.$store.commit(SET_PROJECT, selectedProjectMock); it('returns default toggle text', () => {
vm.$store.commit(SET_PROJECT_BILLING_STATUS, true); expect(dropdownButton.props('toggleText')).toBe(LABELS.DEFAULT);
});
expect(vm.toggleText).toBe(LABELS.DEFAULT);
}); });
it('returns project name if project selected', () => { describe('project is selected', () => {
vm.setItem(selectedZoneMock); beforeEach(() => {
wrapper.vm.setItem(selectedZoneMock);
return wrapper.vm.$nextTick();
});
expect(vm.toggleText).toBe(selectedZoneMock); it('returns project name if project selected', () => {
expect(dropdownButton.props('toggleText')).toBe(selectedZoneMock);
});
}); });
}); });
describe('selectItem', () => { describe('selectItem', () => {
it('reflects new value when dropdown item is clicked', done => { beforeEach(() => {
expect(vm.$el.querySelector('input').value).toBe(''); wrapper.vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items);
vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items); return wrapper.vm.$nextTick();
});
return vm
.$nextTick() it('reflects new value when dropdown item is clicked', () => {
.then(() => { const dropdown = wrapper.find(DropdownHiddenInput);
vm.$el.querySelector('.dropdown-content button').click();
expect(dropdown.attributes('value')).toBe('');
return vm
.$nextTick() wrapper.find('.dropdown-content button').trigger('click');
.then(() => {
expect(vm.$el.querySelector('input').value).toBe(selectedZoneMock); return wrapper.vm.$nextTick().then(() => {
done(); expect(dropdown.attributes('value')).toBe(selectedZoneMock);
}) });
.catch(done.fail);
})
.catch(done.fail);
}); });
}); });
}); });
...@@ -41,7 +41,7 @@ describe('Repository breadcrumbs component', () => { ...@@ -41,7 +41,7 @@ describe('Repository breadcrumbs component', () => {
.findAll(RouterLinkStub) .findAll(RouterLinkStub)
.at(3) .at(3)
.props('to'), .props('to'),
).toEqual('/-/tree//app/assets/javascripts%23'); ).toEqual('/-/tree/app/assets/javascripts%23');
}); });
it('renders last link as active', () => { it('renders last link as active', () => {
......
...@@ -109,6 +109,26 @@ describe('Repository table row component', () => { ...@@ -109,6 +109,26 @@ describe('Repository table row component', () => {
}); });
}); });
it.each`
path
${'test#'}
${'Änderungen'}
`('renders link for $path', ({ path }) => {
factory({
id: '1',
sha: '123',
path,
type: 'tree',
currentPath: '/',
});
return vm.vm.$nextTick().then(() => {
expect(vm.find({ ref: 'link' }).props('to')).toEqual({
path: `/-/tree/master/${encodeURIComponent(path)}`,
});
});
});
it('pushes new route for directory with hash', () => { it('pushes new route for directory with hash', () => {
factory({ factory({
id: '1', id: '1',
......
rules:
# https://gitlab.com/gitlab-org/gitlab/issues/33025
promise/no-nesting: off
export const emptyProjectMock = {
projectId: '',
name: '',
};
export const selectedProjectMock = {
projectId: 'gcp-project-123',
name: 'gcp-project',
};
export const selectedZoneMock = 'us-central1-a';
export const selectedMachineTypeMock = 'n1-standard-2';
export const gapiProjectsResponseMock = {
projects: [
{
projectNumber: '1234',
projectId: 'gcp-project-123',
lifecycleState: 'ACTIVE',
name: 'gcp-project',
createTime: '2017-12-16T01:48:29.129Z',
parent: {
type: 'organization',
id: '12345',
},
},
],
};
export const gapiZonesResponseMock = {
kind: 'compute#zoneList',
id: 'projects/gitlab-internal-153318/zones',
items: [
{
kind: 'compute#zone',
id: '2000',
creationTimestamp: '1969-12-31T16:00:00.000-08:00',
name: 'us-central1-a',
description: 'us-central1-a',
status: 'UP',
region:
'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/regions/us-central1',
selfLink:
'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a',
availableCpuPlatforms: ['Intel Skylake', 'Intel Broadwell', 'Intel Sandy Bridge'],
},
],
selfLink: 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones',
};
export const gapiMachineTypesResponseMock = {
kind: 'compute#machineTypeList',
id: 'projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
items: [
{
kind: 'compute#machineType',
id: '3002',
creationTimestamp: '1969-12-31T16:00:00.000-08:00',
name: 'n1-standard-2',
description: '2 vCPUs, 7.5 GB RAM',
guestCpus: 2,
memoryMb: 7680,
imageSpaceGb: 10,
maximumPersistentDisks: 64,
maximumPersistentDisksSizeGb: '65536',
zone: 'us-central1-a',
selfLink:
'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes/n1-standard-2',
isSharedCpu: false,
},
],
selfLink:
'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
};
...@@ -41,7 +41,9 @@ describe('diffs/components/commit_item', () => { ...@@ -41,7 +41,9 @@ describe('diffs/components/commit_item', () => {
expect(titleElement).toHaveText(commit.title_html); expect(titleElement).toHaveText(commit.title_html);
}); });
it('renders commit description', () => { // https://gitlab.com/gitlab-org/gitlab/issues/197139
// eslint-disable-next-line jasmine/no-disabled-tests
xit('renders commit description', () => {
const descElement = getDescElement(vm); const descElement = getDescElement(vm);
const descExpandElement = getDescExpandElement(vm); const descExpandElement = getDescExpandElement(vm);
......
...@@ -14,6 +14,8 @@ export const issuable1 = { ...@@ -14,6 +14,8 @@ export const issuable1 = {
path: '/foo/bar/issues/123', path: '/foo/bar/issues/123',
state: 'opened', state: 'opened',
linkType: 'relates_to', linkType: 'relates_to',
dueDate: '2010-11-22',
weight: 5,
}; };
export const issuable2 = { export const issuable2 = {
......
...@@ -212,7 +212,7 @@ describe Gitlab::Profiler do ...@@ -212,7 +212,7 @@ describe Gitlab::Profiler do
stub_const('STDOUT', stdout) stub_const('STDOUT', stdout)
end end
it 'prints a profile result sorted by total time' do it 'prints a profile result sorted by total time', quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/206907' do
described_class.print_by_total_time(result) described_class.print_by_total_time(result)
total_times = total_times =
......
...@@ -12,7 +12,7 @@ describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do ...@@ -12,7 +12,7 @@ describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do
subject { cache.cache_key(cache_prefix) } subject { cache.cache_key(cache_prefix) }
it 'includes the suffix' do it 'includes the suffix' do
expect(subject).to eq "#{cache_prefix}:set" expect(subject).to eq "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:#{cache_prefix}:set"
end end
end end
......
...@@ -129,6 +129,17 @@ describe Gitlab::TreeSummary do ...@@ -129,6 +129,17 @@ describe Gitlab::TreeSummary do
expect(commits).to satisfy_one { |c| c.id == whitespace_commit_sha } expect(commits).to satisfy_one { |c| c.id == whitespace_commit_sha }
end end
end end
context 'in a subdirectory with non-ASCII filenames' do
let(:path) { 'encoding' }
it 'returns commits for entries in the subdirectory' do
entry = entries.find { |x| x[:file_name] == 'テスト.txt' }
expect(entry).to be_a(Hash)
expect(entry).to include(:commit)
end
end
end end
describe '#more?' do describe '#more?' do
......
...@@ -145,4 +145,28 @@ describe Ci::Processable do ...@@ -145,4 +145,28 @@ describe Ci::Processable do
expect(another_build.reload.scheduling_type).to be_nil expect(another_build.reload.scheduling_type).to be_nil
end end
end end
describe '#needs_attributes' do
let(:build) { create(:ci_build, :created, project: project, pipeline: pipeline) }
context 'with needs' do
before do
create(:ci_build_need, build: build, name: 'test1')
create(:ci_build_need, build: build, name: 'test2')
end
it 'returns all needs attributes' do
expect(build.needs_attributes).to contain_exactly(
{ 'artifacts' => true, 'name' => 'test1' },
{ 'artifacts' => true, 'name' => 'test2' }
)
end
end
context 'without needs' do
it 'returns all needs attributes' do
expect(build.needs_attributes).to be_empty
end
end
end
end end
...@@ -112,6 +112,43 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do ...@@ -112,6 +112,43 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end end
end end
describe '#with_reactive_cache_set', :use_clean_rails_redis_caching do
subject(:go!) do
instance.with_reactive_cache_set('resource', {}) do |data|
data
end
end
it 'calls with_reactive_cache' do
expect(instance)
.to receive(:with_reactive_cache)
go!
end
context 'data returned' do
let(:resource) { 'resource' }
let(:set_key) { "#{cache_key}:#{resource}" }
let(:set_cache) { Gitlab::ReactiveCacheSetCache.new }
before do
stub_reactive_cache(instance, true, resource, {})
end
it 'saves keys in set' do
expect(set_cache.read(set_key)).to be_empty
go!
expect(set_cache.read(set_key)).not_to be_empty
end
it 'returns the data' do
expect(go!).to eq(true)
end
end
end
describe '.reactive_cache_worker_finder' do describe '.reactive_cache_worker_finder' do
context 'with default reactive_cache_worker_finder' do context 'with default reactive_cache_worker_finder' do
let(:args) { %w(other args) } let(:args) { %w(other args) }
......
...@@ -8,7 +8,7 @@ describe ErrorTracking::ProjectErrorTrackingSetting do ...@@ -8,7 +8,7 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
subject { create(:project_error_tracking_setting, project: project) } subject(:setting) { create(:project_error_tracking_setting, project: project) }
describe 'Associations' do describe 'Associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
...@@ -453,4 +453,23 @@ describe ErrorTracking::ProjectErrorTrackingSetting do ...@@ -453,4 +453,23 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
end end
end end
end end
describe '#expire_issues_cache', :use_clean_rails_redis_caching do
let(:issues) { [:some, :issues] }
let(:opt) { 'list_issues' }
let(:params) { { issue_status: 'unresolved', limit: 20, sort: 'last_seen' } }
before do
start_reactive_cache_lifetime(subject, opt, params.stringify_keys)
stub_reactive_cache(subject, issues, opt, params.stringify_keys)
end
it 'clears the cache' do
expect(subject.list_sentry_issues(params)).to eq(issues)
subject.expire_issues_cache
expect(subject.list_sentry_issues(params)).to eq(nil)
end
end
end end
...@@ -16,7 +16,7 @@ describe API::Markdown do ...@@ -16,7 +16,7 @@ describe API::Markdown do
shared_examples "rendered markdown text without GFM" do shared_examples "rendered markdown text without GFM" do
it "renders markdown text" do it "renders markdown text" do
expect(response).to have_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(response.headers["Content-Type"]).to eq("application/json") expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash) expect(json_response).to be_a(Hash)
expect(json_response["html"]).to eq("<p>#{text}</p>") expect(json_response["html"]).to eq("<p>#{text}</p>")
...@@ -25,7 +25,7 @@ describe API::Markdown do ...@@ -25,7 +25,7 @@ describe API::Markdown do
shared_examples "404 Project Not Found" do shared_examples "404 Project Not Found" do
it "responses with 404 Not Found" do it "responses with 404 Not Found" do
expect(response).to have_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
expect(response.headers["Content-Type"]).to eq("application/json") expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash) expect(json_response).to be_a(Hash)
expect(json_response["message"]).to eq("404 Project Not Found") expect(json_response["message"]).to eq("404 Project Not Found")
...@@ -37,7 +37,7 @@ describe API::Markdown do ...@@ -37,7 +37,7 @@ describe API::Markdown do
let(:params) { {} } let(:params) { {} }
it "responses with 400 Bad Request" do it "responses with 400 Bad Request" do
expect(response).to have_http_status(400) expect(response).to have_gitlab_http_status(:bad_request)
expect(response.headers["Content-Type"]).to eq("application/json") expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash) expect(json_response).to be_a(Hash)
expect(json_response["error"]).to eq("text is missing") expect(json_response["error"]).to eq("text is missing")
...@@ -83,7 +83,7 @@ describe API::Markdown do ...@@ -83,7 +83,7 @@ describe API::Markdown do
let(:params) { { text: text, gfm: true } } let(:params) { { text: text, gfm: true } }
it "renders markdown text" do it "renders markdown text" do
expect(response).to have_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(response.headers["Content-Type"]).to eq("application/json") expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash) expect(json_response).to be_a(Hash)
expect(json_response["html"]).to include("Hello world!") expect(json_response["html"]).to include("Hello world!")
...@@ -100,7 +100,7 @@ describe API::Markdown do ...@@ -100,7 +100,7 @@ describe API::Markdown do
let(:user) { project.owner } let(:user) { project.owner }
it "renders markdown text" do it "renders markdown text" do
expect(response).to have_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(response.headers["Content-Type"]).to eq("application/json") expect(response.headers["Content-Type"]).to eq("application/json")
expect(json_response).to be_a(Hash) expect(json_response).to be_a(Hash)
expect(json_response["html"]).to include("Hello world!") expect(json_response["html"]).to include("Hello world!")
...@@ -120,7 +120,7 @@ describe API::Markdown do ...@@ -120,7 +120,7 @@ describe API::Markdown do
shared_examples 'user without proper access' do shared_examples 'user without proper access' do
it 'does not render the title or link' do it 'does not render the title or link' do
expect(response).to have_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response["html"]).not_to include('Confidential title') expect(json_response["html"]).not_to include('Confidential title')
expect(json_response["html"]).not_to include('<a href=') expect(json_response["html"]).not_to include('<a href=')
expect(json_response["html"]).to include('Hello world!') expect(json_response["html"]).to include('Hello world!')
...@@ -146,7 +146,7 @@ describe API::Markdown do ...@@ -146,7 +146,7 @@ describe API::Markdown do
let(:user) { confidential_issue.author } let(:user) { confidential_issue.author }
it 'renders the title or link' do it 'renders the title or link' do
expect(response).to have_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response["html"]).to include('Confidential title') expect(json_response["html"]).to include('Confidential title')
expect(json_response["html"]).to include('Hello world!') expect(json_response["html"]).to include('Hello world!')
.and include('data-name="tada"') .and include('data-name="tada"')
......
This diff is collapsed.
...@@ -28,12 +28,12 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do ...@@ -28,12 +28,12 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
it 'returns a 404 when merge_request id is used instead of the iid' do it 'returns a 404 when merge_request id is used instead of the iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'returns a 404 when merge_request_iid not found' do it 'returns a 404 when merge_request_iid not found' do
get api("/projects/#{project.id}/merge_requests/0/versions", user) get api("/projects/#{project.id}/merge_requests/0/versions", user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
...@@ -51,17 +51,17 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do ...@@ -51,17 +51,17 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
it 'returns a 404 when merge_request id is used instead of the iid' do it 'returns a 404 when merge_request id is used instead of the iid' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'returns a 404 when merge_request version_id is not found' do it 'returns a 404 when merge_request version_id is not found' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/0", user) get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/0", user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'returns a 404 when merge_request_iid is not found' do it 'returns a 404 when merge_request_iid is not found' do
get api("/projects/#{project.id}/merge_requests/12345/versions/#{merge_request_diff.id}", user) get api("/projects/#{project.id}/merge_requests/12345/versions/#{merge_request_diff.id}", user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end end
This diff is collapsed.
...@@ -12,7 +12,7 @@ describe API::Namespaces do ...@@ -12,7 +12,7 @@ describe API::Namespaces do
context "when unauthenticated" do context "when unauthenticated" do
it "returns authentication error" do it "returns authentication error" do
get api("/namespaces") get api("/namespaces")
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(:unauthorized)
end end
end end
...@@ -23,7 +23,7 @@ describe API::Namespaces do ...@@ -23,7 +23,7 @@ describe API::Namespaces do
group_kind_json_response = json_response.find { |resource| resource['kind'] == 'group' } group_kind_json_response = json_response.find { |resource| resource['kind'] == 'group' }
user_kind_json_response = json_response.find { |resource| resource['kind'] == 'user' } user_kind_json_response = json_response.find { |resource| resource['kind'] == 'user' }
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(group_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path', expect(group_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path',
'parent_id', 'members_count_with_descendants') 'parent_id', 'members_count_with_descendants')
...@@ -34,7 +34,7 @@ describe API::Namespaces do ...@@ -34,7 +34,7 @@ describe API::Namespaces do
it "admin: returns an array of all namespaces" do it "admin: returns an array of all namespaces" do
get api("/namespaces", admin) get api("/namespaces", admin)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.length).to eq(Namespace.count) expect(json_response.length).to eq(Namespace.count)
...@@ -43,7 +43,7 @@ describe API::Namespaces do ...@@ -43,7 +43,7 @@ describe API::Namespaces do
it "admin: returns an array of matched namespaces" do it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{group2.name}", admin) get api("/namespaces?search=#{group2.name}", admin)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.length).to eq(1) expect(json_response.length).to eq(1)
...@@ -77,7 +77,7 @@ describe API::Namespaces do ...@@ -77,7 +77,7 @@ describe API::Namespaces do
it "user: returns an array of namespaces" do it "user: returns an array of namespaces" do
get api("/namespaces", user) get api("/namespaces", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.length).to eq(1) expect(json_response.length).to eq(1)
...@@ -86,7 +86,7 @@ describe API::Namespaces do ...@@ -86,7 +86,7 @@ describe API::Namespaces do
it "admin: returns an array of matched namespaces" do it "admin: returns an array of matched namespaces" do
get api("/namespaces?search=#{user.username}", user) get api("/namespaces?search=#{user.username}", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.length).to eq(1) expect(json_response.length).to eq(1)
...@@ -102,7 +102,7 @@ describe API::Namespaces do ...@@ -102,7 +102,7 @@ describe API::Namespaces do
it 'returns namespace details' do it 'returns namespace details' do
get api("/namespaces/#{namespace_id}", request_actor) get api("/namespaces/#{namespace_id}", request_actor)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(requested_namespace.id) expect(json_response['id']).to eq(requested_namespace.id)
expect(json_response['path']).to eq(requested_namespace.path) expect(json_response['path']).to eq(requested_namespace.path)
...@@ -153,7 +153,7 @@ describe API::Namespaces do ...@@ -153,7 +153,7 @@ describe API::Namespaces do
it 'returns not-found' do it 'returns not-found' do
get api('/namespaces/0', request_actor) get api('/namespaces/0', request_actor)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end end
...@@ -162,7 +162,7 @@ describe API::Namespaces do ...@@ -162,7 +162,7 @@ describe API::Namespaces do
it 'returns authentication error' do it 'returns authentication error' do
get api("/namespaces/#{group1.id}") get api("/namespaces/#{group1.id}")
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(:unauthorized)
end end
end end
...@@ -174,7 +174,7 @@ describe API::Namespaces do ...@@ -174,7 +174,7 @@ describe API::Namespaces do
it 'returns not-found' do it 'returns not-found' do
get api("/namespaces/#{group2.id}", request_actor) get api("/namespaces/#{group2.id}", request_actor)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
...@@ -182,7 +182,7 @@ describe API::Namespaces do ...@@ -182,7 +182,7 @@ describe API::Namespaces do
it 'returns not-found' do it 'returns not-found' do
get api("/namespaces/#{user2.namespace.id}", request_actor) get api("/namespaces/#{user2.namespace.id}", request_actor)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end end
......
...@@ -72,7 +72,7 @@ describe API::Notes do ...@@ -72,7 +72,7 @@ describe API::Notes do
it "returns an empty array" do it "returns an empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response).to be_empty expect(json_response).to be_empty
...@@ -86,7 +86,7 @@ describe API::Notes do ...@@ -86,7 +86,7 @@ describe API::Notes do
it "returns 404" do it "returns 404" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user) get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end end
...@@ -95,7 +95,7 @@ describe API::Notes do ...@@ -95,7 +95,7 @@ describe API::Notes do
it "returns a non-empty array" do it "returns a non-empty array" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", private_user) get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes", private_user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
expect(json_response.first['body']).to eq(cross_reference_note.note) expect(json_response.first['body']).to eq(cross_reference_note.note)
...@@ -114,7 +114,7 @@ describe API::Notes do ...@@ -114,7 +114,7 @@ describe API::Notes do
shared_examples 'a notes request' do shared_examples 'a notes request' do
it 'is a note array response' do it 'is a note array response' do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers expect(response).to include_pagination_headers
expect(json_response).to be_an Array expect(json_response).to be_an Array
end end
...@@ -177,7 +177,7 @@ describe API::Notes do ...@@ -177,7 +177,7 @@ describe API::Notes do
it "returns a 404 error" do it "returns a 404 error" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", user) get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
context "when issue is confidential" do context "when issue is confidential" do
...@@ -188,7 +188,7 @@ describe API::Notes do ...@@ -188,7 +188,7 @@ describe API::Notes do
it "returns 404" do it "returns 404" do
get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", private_user) get api("/projects/#{project.id}/issues/#{issue.iid}/notes/#{issue_note.id}", private_user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end end
...@@ -197,7 +197,7 @@ describe API::Notes do ...@@ -197,7 +197,7 @@ describe API::Notes do
it "returns an issue note by id" do it "returns an issue note by id" do
get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", private_user) get api("/projects/#{ext_proj.id}/issues/#{ext_issue.iid}/notes/#{cross_reference_note.id}", private_user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['body']).to eq(cross_reference_note.note) expect(json_response['body']).to eq(cross_reference_note.note)
end end
end end
...@@ -237,7 +237,7 @@ describe API::Notes do ...@@ -237,7 +237,7 @@ describe API::Notes do
it 'returns 200 status' do it 'returns 200 status' do
subject subject
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
end end
it 'creates a new note' do it 'creates a new note' do
...@@ -251,7 +251,7 @@ describe API::Notes do ...@@ -251,7 +251,7 @@ describe API::Notes do
it 'returns 403 status' do it 'returns 403 status' do
subject subject
expect(response).to have_gitlab_http_status(403) expect(response).to have_gitlab_http_status(:forbidden)
end end
it 'does not create a new note' do it 'does not create a new note' do
......
...@@ -11,7 +11,7 @@ describe API::NotificationSettings do ...@@ -11,7 +11,7 @@ describe API::NotificationSettings do
it "returns global notification settings for the current user" do it "returns global notification settings for the current user" do
get api("/notification_settings", user) get api("/notification_settings", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a Hash expect(json_response).to be_a Hash
expect(json_response['notification_email']).to eq(user.notification_email) expect(json_response['notification_email']).to eq(user.notification_email)
expect(json_response['level']).to eq(user.global_notification_setting.level) expect(json_response['level']).to eq(user.global_notification_setting.level)
...@@ -24,7 +24,7 @@ describe API::NotificationSettings do ...@@ -24,7 +24,7 @@ describe API::NotificationSettings do
it "updates global notification settings for the current user" do it "updates global notification settings for the current user" do
put api("/notification_settings", user), params: { level: 'watch', notification_email: email.email } put api("/notification_settings", user), params: { level: 'watch', notification_email: email.email }
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['notification_email']).to eq(email.email) expect(json_response['notification_email']).to eq(email.email)
expect(user.reload.notification_email).to eq(email.email) expect(user.reload.notification_email).to eq(email.email)
expect(json_response['level']).to eq(user.reload.global_notification_setting.level) expect(json_response['level']).to eq(user.reload.global_notification_setting.level)
...@@ -35,7 +35,7 @@ describe API::NotificationSettings do ...@@ -35,7 +35,7 @@ describe API::NotificationSettings do
it "fails on non-user email address" do it "fails on non-user email address" do
put api("/notification_settings", user), params: { notification_email: 'invalid@example.com' } put api("/notification_settings", user), params: { notification_email: 'invalid@example.com' }
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(:bad_request)
end end
end end
...@@ -43,7 +43,7 @@ describe API::NotificationSettings do ...@@ -43,7 +43,7 @@ describe API::NotificationSettings do
it "returns group level notification settings for the current user" do it "returns group level notification settings for the current user" do
get api("/groups/#{group.id}/notification_settings", user) get api("/groups/#{group.id}/notification_settings", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a Hash expect(json_response).to be_a Hash
expect(json_response['level']).to eq(user.notification_settings_for(group).level) expect(json_response['level']).to eq(user.notification_settings_for(group).level)
end end
...@@ -53,7 +53,7 @@ describe API::NotificationSettings do ...@@ -53,7 +53,7 @@ describe API::NotificationSettings do
it "updates group level notification settings for the current user" do it "updates group level notification settings for the current user" do
put api("/groups/#{group.id}/notification_settings", user), params: { level: 'watch' } put api("/groups/#{group.id}/notification_settings", user), params: { level: 'watch' }
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['level']).to eq(user.reload.notification_settings_for(group).level) expect(json_response['level']).to eq(user.reload.notification_settings_for(group).level)
end end
end end
...@@ -62,7 +62,7 @@ describe API::NotificationSettings do ...@@ -62,7 +62,7 @@ describe API::NotificationSettings do
it "returns project level notification settings for the current user" do it "returns project level notification settings for the current user" do
get api("/projects/#{project.id}/notification_settings", user) get api("/projects/#{project.id}/notification_settings", user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_a Hash expect(json_response).to be_a Hash
expect(json_response['level']).to eq(user.notification_settings_for(project).level) expect(json_response['level']).to eq(user.notification_settings_for(project).level)
end end
...@@ -72,7 +72,7 @@ describe API::NotificationSettings do ...@@ -72,7 +72,7 @@ describe API::NotificationSettings do
it "updates project level notification settings for the current user" do it "updates project level notification settings for the current user" do
put api("/projects/#{project.id}/notification_settings", user), params: { level: 'custom', new_note: true } put api("/projects/#{project.id}/notification_settings", user), params: { level: 'custom', new_note: true }
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level) expect(json_response['level']).to eq(user.reload.notification_settings_for(project).level)
expect(json_response['events']['new_note']).to be_truthy expect(json_response['events']['new_note']).to be_truthy
expect(json_response['events']['new_issue']).to be_falsey expect(json_response['events']['new_issue']).to be_falsey
...@@ -83,7 +83,7 @@ describe API::NotificationSettings do ...@@ -83,7 +83,7 @@ describe API::NotificationSettings do
it "fails on invalid level" do it "fails on invalid level" do
put api("/projects/#{project.id}/notification_settings", user), params: { level: 'invalid' } put api("/projects/#{project.id}/notification_settings", user), params: { level: 'invalid' }
expect(response).to have_gitlab_http_status(400) expect(response).to have_gitlab_http_status(:bad_request)
end end
end end
end end
...@@ -14,7 +14,7 @@ describe 'OAuth tokens' do ...@@ -14,7 +14,7 @@ describe 'OAuth tokens' do
request_oauth_token(user) request_oauth_token(user)
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['error']).to eq('invalid_grant') expect(json_response['error']).to eq('invalid_grant')
end end
end end
...@@ -25,7 +25,7 @@ describe 'OAuth tokens' do ...@@ -25,7 +25,7 @@ describe 'OAuth tokens' do
request_oauth_token(user) request_oauth_token(user)
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(:ok)
expect(json_response['access_token']).not_to be_nil expect(json_response['access_token']).not_to be_nil
end end
end end
...@@ -33,7 +33,7 @@ describe 'OAuth tokens' do ...@@ -33,7 +33,7 @@ describe 'OAuth tokens' do
shared_examples 'does not create an access token' do shared_examples 'does not create an access token' do
let(:user) { create(:user) } let(:user) { create(:user) }
it { expect(response).to have_gitlab_http_status(401) } it { expect(response).to have_gitlab_http_status(:unauthorized) }
end end
context 'when user is blocked' do context 'when user is blocked' do
......
...@@ -1979,6 +1979,21 @@ describe API::Runner, :clean_gitlab_redis_shared_state do ...@@ -1979,6 +1979,21 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end end
end end
context 'when object storage throws errors' do
let(:params) { { artifact_type: :archive, artifact_format: :zip } }
it 'does not store artifacts' do
allow_next_instance_of(JobArtifactUploader) do |uploader|
allow(uploader).to receive(:store!).and_raise(Errno::EIO)
end
upload_artifacts(file_upload, headers_with_token, params)
expect(response).to have_gitlab_http_status(:service_unavailable)
expect(job.reload.job_artifacts_archive).to be_nil
end
end
context 'when artifacts are being stored outside of tmp path' do context 'when artifacts are being stored outside of tmp path' do
let(:new_tmpdir) { Dir.mktmpdir } let(:new_tmpdir) { Dir.mktmpdir }
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
require 'spec_helper' require 'spec_helper'
describe Ci::CreateJobArtifactsService do describe Ci::CreateJobArtifactsService do
let(:service) { described_class.new } let_it_be(:project) { create(:project) }
let(:job) { create(:ci_build) } let(:service) { described_class.new(project) }
let(:job) { create(:ci_build, project: project) }
let(:artifacts_sha256) { '0' * 64 } let(:artifacts_sha256) { '0' * 64 }
let(:metadata_file) { nil } let(:metadata_file) { nil }
...@@ -64,7 +65,7 @@ describe Ci::CreateJobArtifactsService do ...@@ -64,7 +65,7 @@ describe Ci::CreateJobArtifactsService do
it 'sets expiration date according to application settings' do it 'sets expiration date according to application settings' do
expected_expire_at = 1.day.from_now expected_expire_at = 1.day.from_now
expect(subject).to be_truthy expect(subject).to match(a_hash_including(status: :success))
archive_artifact, metadata_artifact = job.job_artifacts.last(2) archive_artifact, metadata_artifact = job.job_artifacts.last(2)
expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at) expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
...@@ -80,7 +81,7 @@ describe Ci::CreateJobArtifactsService do ...@@ -80,7 +81,7 @@ describe Ci::CreateJobArtifactsService do
it 'sets expiration date according to the parameter' do it 'sets expiration date according to the parameter' do
expected_expire_at = 2.hours.from_now expected_expire_at = 2.hours.from_now
expect(subject).to be_truthy expect(subject).to match(a_hash_including(status: :success))
archive_artifact, metadata_artifact = job.job_artifacts.last(2) archive_artifact, metadata_artifact = job.job_artifacts.last(2)
expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at) expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
...@@ -101,21 +102,50 @@ describe Ci::CreateJobArtifactsService do ...@@ -101,21 +102,50 @@ describe Ci::CreateJobArtifactsService do
it 'ignores the changes' do it 'ignores the changes' do
expect { subject }.not_to change { Ci::JobArtifact.count } expect { subject }.not_to change { Ci::JobArtifact.count }
expect(subject).to be_truthy expect(subject).to match(a_hash_including(status: :success))
end end
end end
context 'when sha256 of uploading artifact is different than the existing one' do context 'when sha256 of uploading artifact is different than the existing one' do
let(:existing_sha256) { '1' * 64 } let(:existing_sha256) { '1' * 64 }
it 'returns false and logs the error' do it 'returns error status' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
expect { subject }.not_to change { Ci::JobArtifact.count } expect { subject }.not_to change { Ci::JobArtifact.count }
expect(subject).to be_falsey expect(subject).to match(
expect(job.errors[:base]).to contain_exactly('another artifact of the same type already exists') a_hash_including(http_status: :bad_request,
message: 'another artifact of the same type already exists',
status: :error))
end end
end end
end end
shared_examples 'rescues object storage error' do |klass, message, expected_message|
it "handles #{klass}" do
allow_next_instance_of(JobArtifactUploader) do |uploader|
allow(uploader).to receive(:store!).and_raise(klass, message)
end
expect(Gitlab::ErrorTracking)
.to receive(:track_exception)
.and_call_original
expect(subject).to match(
a_hash_including(
http_status: :service_unavailable,
message: expected_message || message,
status: :error))
end
end
it_behaves_like 'rescues object storage error',
Errno::EIO, 'some/path', 'Input/output error - some/path'
it_behaves_like 'rescues object storage error',
Google::Apis::ServerError, 'Server error'
it_behaves_like 'rescues object storage error',
Signet::RemoteServerError, 'The service is currently unavailable'
end end
end end
...@@ -36,7 +36,7 @@ describe Ci::RetryBuildService do ...@@ -36,7 +36,7 @@ describe Ci::RetryBuildService do
job_artifacts_performance job_artifacts_lsif job_artifacts_performance job_artifacts_lsif
job_artifacts_codequality job_artifacts_metrics scheduled_at job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee].freeze job_artifacts_network_referee needs].freeze
IGNORE_ACCESSORS = IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections %i[type lock_version target_url base_tags trace_sections
...@@ -46,7 +46,8 @@ describe Ci::RetryBuildService do ...@@ -46,7 +46,8 @@ describe Ci::RetryBuildService do
sourced_pipelines artifacts_file_store artifacts_metadata_store sourced_pipelines artifacts_file_store artifacts_metadata_store
metadata runner_session trace_chunks upstream_pipeline_id metadata runner_session trace_chunks upstream_pipeline_id
artifacts_file artifacts_metadata artifacts_size commands artifacts_file artifacts_metadata artifacts_size commands
resource resource_group_id processed security_scans].freeze resource resource_group_id processed security_scans author
pipeline_id].freeze
shared_examples 'build duplication' do shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) } let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
...@@ -79,8 +80,15 @@ describe Ci::RetryBuildService do ...@@ -79,8 +80,15 @@ describe Ci::RetryBuildService do
end end
describe 'clone accessors' do describe 'clone accessors' do
let(:forbidden_associations) do
Ci::Build.reflect_on_all_associations.each_with_object(Set.new) do |assoc, memo|
memo << assoc.name unless assoc.macro == :belongs_to
end
end
CLONE_ACCESSORS.each do |attribute| CLONE_ACCESSORS.each do |attribute|
it "clones #{attribute} build attribute" do it "clones #{attribute} build attribute" do
expect(attribute).not_to be_in(forbidden_associations), "association #{attribute} must be `belongs_to`"
expect(build.send(attribute)).not_to be_nil expect(build.send(attribute)).not_to be_nil
expect(new_build.send(attribute)).not_to be_nil expect(new_build.send(attribute)).not_to be_nil
expect(new_build.send(attribute)).to eq build.send(attribute) expect(new_build.send(attribute)).to eq build.send(attribute)
...@@ -97,9 +105,17 @@ describe Ci::RetryBuildService do ...@@ -97,9 +105,17 @@ describe Ci::RetryBuildService do
expect(new_build.protected).to eq build.protected expect(new_build.protected).to eq build.protected
end end
end end
it 'clones only the needs attributes' do
expect(new_build.needs.exists?).to be_truthy
expect(build.needs.exists?).to be_truthy
expect(new_build.needs_attributes).to match(build.needs_attributes)
expect(new_build.needs).not_to match(build.needs)
end
end end
describe 'reject acessors' do describe 'reject accessors' do
REJECT_ACCESSORS.each do |attribute| REJECT_ACCESSORS.each do |attribute|
it "does not clone #{attribute} build attribute" do it "does not clone #{attribute} build attribute" do
expect(new_build.send(attribute)).not_to eq build.send(attribute) expect(new_build.send(attribute)).not_to eq build.send(attribute)
...@@ -117,8 +133,9 @@ describe Ci::RetryBuildService do ...@@ -117,8 +133,9 @@ describe Ci::RetryBuildService do
# #
current_accessors = current_accessors =
Ci::Build.attribute_names.map(&:to_sym) + Ci::Build.attribute_names.map(&:to_sym) +
Ci::Build.attribute_aliases.keys.map(&:to_sym) +
Ci::Build.reflect_on_all_associations.map(&:name) + Ci::Build.reflect_on_all_associations.map(&:name) +
[:tag_list] [:tag_list, :needs_attributes]
current_accessors.uniq! current_accessors.uniq!
......
...@@ -43,6 +43,12 @@ describe ErrorTracking::IssueUpdateService do ...@@ -43,6 +43,12 @@ describe ErrorTracking::IssueUpdateService do
update_service.execute update_service.execute
end end
it 'clears the reactive cache' do
expect(error_tracking_setting).to receive(:expire_issues_cache)
result
end
context 'related issue and resolving' do context 'related issue and resolving' do
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:sentry_issue) { create(:sentry_issue, issue: issue) } let(:sentry_issue) { create(:sentry_issue, issue: issue) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_store_caching do
include MetricsDashboardHelpers
set(:user) { create(:user) }
set(:project) { create(:project, :repository) }
set(:environment) { create(:environment, project: project) }
describe '#execute' do
subject(:service_call) { described_class.new(project, user, params).execute }
let(:commit_message) { 'test' }
let(:branch) { 'dashboard_new_branch' }
let(:dashboard) { 'config/prometheus/common_metrics.yml' }
let(:file_name) { 'custom_dashboard.yml' }
let(:file_content_hash) { YAML.safe_load(File.read(dashboard)) }
let(:params) do
{
file_name: file_name,
file_content: file_content_hash,
commit_message: commit_message,
branch: branch
}
end
context 'user does not have push right to repository' do
it_behaves_like 'misconfigured dashboard service response', :forbidden, "You can't commit to this project"
end
context 'with rights to push to the repository' do
before do
project.add_maintainer(user)
end
context 'path traversal attack attempt' do
context 'with a yml extension' do
let(:file_name) { 'config/prometheus/../database.yml' }
it_behaves_like 'misconfigured dashboard service response', :bad_request, "A file with this name doesn't exist"
end
context 'without a yml extension' do
let(:file_name) { '../../..../etc/passwd' }
it_behaves_like 'misconfigured dashboard service response', :bad_request, "The file name should have a .yml extension"
end
end
context 'valid parameters' do
it_behaves_like 'valid dashboard update process'
end
context 'selected branch already exists' do
let(:branch) { 'existing_branch' }
before do
project.repository.add_branch(user, branch, 'master')
end
it_behaves_like 'misconfigured dashboard service response', :bad_request, "There was an error updating the dashboard, branch named: existing_branch already exists."
end
context 'Files::UpdateService success' do
before do
allow(::Files::UpdateService).to receive(:new).and_return(double(execute: { status: :success }))
end
it 'returns success', :aggregate_failures do
dashboard_details = {
path: '.gitlab/dashboards/custom_dashboard.yml',
display_name: 'custom_dashboard.yml',
default: false,
system_dashboard: false
}
expect(service_call[:status]).to be :success
expect(service_call[:http_status]).to be :created
expect(service_call[:dashboard]).to match dashboard_details
end
context 'with escaped characters in file name' do
let(:file_name) { "custom_dashboard%26copy.yml" }
it 'escapes the special characters', :aggregate_failures do
dashboard_details = {
path: '.gitlab/dashboards/custom_dashboard&copy.yml',
display_name: 'custom_dashboard&copy.yml',
default: false,
system_dashboard: false
}
expect(service_call[:status]).to be :success
expect(service_call[:http_status]).to be :created
expect(service_call[:dashboard]).to match dashboard_details
end
end
end
context 'Files::UpdateService fails' do
before do
allow(::Files::UpdateService).to receive(:new).and_return(double(execute: { status: :error }))
end
it 'returns error' do
expect(service_call[:status]).to be :error
end
end
end
end
end
...@@ -85,3 +85,24 @@ RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template, ...@@ -85,3 +85,24 @@ RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template,
end end
end end
end end
RSpec.shared_examples 'valid dashboard update process' do
let(:dashboard_attrs) do
{
commit_message: commit_message,
branch_name: branch,
start_branch: project.default_branch,
encoding: 'text',
file_path: ".gitlab/dashboards/#{file_name}",
file_content: ::PerformanceMonitoring::PrometheusDashboard.from_json(file_content_hash).to_yaml
}
end
it 'delegates commit creation to Files::UpdateService', :aggregate_failures do
service_instance = instance_double(::Files::UpdateService)
expect(::Files::UpdateService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance)
expect(service_instance).to receive(:execute).and_return(status: :success)
service_call
end
end
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