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
| -------- | -------- |
| Issue on [GitLab](https://gitlab.com/gitlab-org/gitlab/issues) | #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
......
......@@ -8,11 +8,11 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
## Related issues
<!-- Mention the issue(s) this MR is related to -->
<!-- Mention the GitLab Security issue this MR is related to -->
## 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.
- [ ] 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.
......
......@@ -349,8 +349,8 @@ RSpec/HaveGitlabHttpStatus:
- 'ee/spec/requests/{groups,projects,repositories}/**/*'
- 'spec/requests/api/*/**/*.rb'
- 'ee/spec/requests/api/*/**/*.rb'
- 'spec/requests/api/[a-l]*.rb'
- 'ee/spec/requests/api/[a-l]*.rb'
- 'spec/requests/api/[a-o]*.rb'
- 'ee/spec/requests/api/[a-o]*.rb'
Style/MultilineWhenThen:
Enabled: false
......
......@@ -44,6 +44,7 @@ class DueDateSelect {
this.$selectbox.hide();
this.$value.css('display', '');
},
shouldPropagate: false,
});
}
......
<script>
import { GlDropdown, GlDropdownDivider, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '../../locale';
import Icon from '../../vue_shared/components/icon.vue';
import getRefMixin from '../mixins/get_ref';
......@@ -102,12 +103,12 @@ export default {
.filter(p => p !== '')
.reduce(
(acc, name, i) => {
const path = `${i > 0 ? acc[i].path : ''}/${name}`;
const path = joinPaths(i > 0 ? acc[i].path : '', encodeURIComponent(name));
return acc.concat({
name,
path,
to: `/-/tree/${escape(this.ref)}${escape(path)}`,
to: `/-/tree/${joinPaths(escape(this.ref), path)}`,
});
},
[
......
......@@ -91,7 +91,9 @@ export default {
},
computed: {
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() {
return `fa-${getIconName(this.type, this.path)}`;
......@@ -141,6 +143,7 @@ export default {
<i v-else :aria-label="type" role="img" :class="iconName" class="fa fa-fw"></i>
<component
:is="linkComponent"
ref="link"
:to="routerLinkTo"
:href="url"
class="str-truncated"
......
......@@ -27,7 +27,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
fetchpromise = axios
.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(/^\//, ''),
)}`,
{
......
......@@ -3,8 +3,8 @@ import { s__ } from '~/locale';
export const PAGINATION_UI_BUTTON_LIMIT = 4;
export const UI_LIMIT = 6;
export const SPREAD = '...';
export const PREV = s__('Pagination|Prev');
export const NEXT = s__('Pagination|Next');
export const PREV = s__('Pagination|Prev');
export const NEXT = s__('Pagination|Next');
export const FIRST = s__('Pagination|« First');
export const LAST = s__('Pagination|Last »');
export const LABEL_FIRST_PAGE = s__('Pagination|Go to first page');
......
......@@ -22,6 +22,16 @@ module Projects
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
def respond_success(result)
......@@ -43,6 +53,19 @@ module Projects
flash[:notice] = message.html_safe
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!
params.require(%i(branch file_name dashboard commit_message))
end
......@@ -54,6 +77,31 @@ module Projects
def dashboard_params
params.permit(%i(branch file_name dashboard commit_message)).to_h
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
......@@ -90,6 +90,12 @@ module Ci
end
end
def needs_attributes
strong_memoize(:needs_attributes) do
needs.map { |need| need.attributes.except('id', 'build_id') }
end
end
private
def validate_scheduling_type?
......
......@@ -52,6 +52,13 @@ module ReactiveCaching
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.
def without_reactive_cache(*args, &blk)
return with_reactive_cache(*args, &blk) unless Rails.env.development?
......@@ -65,6 +72,12 @@ module ReactiveCaching
Rails.cache.delete(alive_reactive_cache_key(*args))
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)
locking_reactive_cache(*args) do
key = full_reactive_cache_key(*args)
......@@ -86,6 +99,16 @@ module ReactiveCaching
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)
clear_reactive_cache!(*args)
keep_alive_reactive_cache!(*args)
......
......@@ -85,7 +85,7 @@ module ErrorTracking
end
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
end
end
......@@ -130,6 +130,10 @@ module ErrorTracking
end
end
def expire_issues_cache
clear_reactive_cache_set!('list_issues')
end
# http://HOST/api/0/projects/ORG/PROJECT
# ->
# http://HOST/ORG/PROJECT
......
# frozen_string_literal: true
module Ci
class CreateJobArtifactsService
class CreateJobArtifactsService < ::BaseService
ArtifactsExistError = Class.new(StandardError)
OBJECT_STORAGE_ERRORS = [
Errno::EIO,
Google::Apis::ServerError,
Signet::RemoteServerError
].freeze
def execute(job, artifacts_file, params, metadata_file: nil)
expire_in = params['expire_in'] ||
......@@ -26,18 +31,20 @@ module Ci
expire_in: expire_in)
end
job.update(artifacts_expire_in: expire_in)
rescue ActiveRecord::RecordNotUnique => error
return true if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file)
if job.update(artifacts_expire_in: expire_in)
success
else
error(job.errors.messages, :bad_request)
end
Gitlab::ErrorTracking.track_exception(error,
job_id: job.id,
project_id: job.project_id,
uploading_type: params['artifact_type']
)
rescue ActiveRecord::RecordNotUnique => error
return success if sha256_matches_existing_artifact?(job, params['artifact_type'], artifacts_file)
job.errors.add(:base, 'another artifact of the same type already exists')
false
track_exception(error, job, params)
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
private
......@@ -48,5 +55,13 @@ module Ci
existing_artifact.file_sha256 == artifacts_file.sha256
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
......@@ -5,7 +5,8 @@ module Ci
CLONE_ACCESSORS = %i[pipeline project ref tag options name
allow_failure stage stage_id stage_idx trigger_request
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)
reprocess!(build).tap do |new_build|
......
......@@ -11,6 +11,7 @@ module ErrorTracking
)
compose_response(response) do
project_error_tracking_setting.expire_issues_cache
response[:closed_issue_iid] = update_related_issue&.iid
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
end
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
namespace :error_tracking do
......
......@@ -85,13 +85,27 @@ the following feature flags are enabled on your GitLab instance:
- `:ci_use_merge_request_ref`
- `: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
> 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.
> Feature.enable(:ci_use_merge_request_ref) # Enable the feature flag.
```
1. Log into the Rails console of the GitLab instance:
```shell
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
......
......@@ -283,10 +283,12 @@ module API
bad_request!('Missing artifacts file!') unless artifacts
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
else
render_validation_error!(job)
render_api_error!(result[:message], result[:http_status])
end
end
......
......@@ -3,7 +3,7 @@
module Gitlab
module MarkdownCache
# Increment this number every time the renderer changes its output
CACHE_COMMONMARK_VERSION = 19
CACHE_COMMONMARK_VERSION = 20
CACHE_COMMONMARK_VERSION_START = 10
BaseError = Class.new(StandardError)
......
......@@ -10,9 +10,13 @@ module Gitlab
@expires_in = expires_in
end
def cache_key(key)
"#{cache_type}:#{key}:set"
end
def clear_cache!(key)
with do |redis|
keys = read(key).map { |value| "#{cache_type}#{value}" }
keys = read(key).map { |value| "#{cache_type}:#{value}" }
keys << cache_key(key)
redis.pipelined do
......@@ -24,7 +28,7 @@ module Gitlab
private
def cache_type
"#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:"
Gitlab::Redis::Cache::CACHE_NAMESPACE
end
end
end
......@@ -69,7 +69,7 @@ module Gitlab
end
def entry_path(entry)
File.join(*[path, entry[:file_name]].compact)
File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT)
end
def build_entry(entry)
......
......@@ -13658,18 +13658,12 @@ msgstr ""
msgid "Pagination|Next"
msgstr ""
msgid "Pagination|Next ›"
msgstr ""
msgid "Pagination|Prev"
msgstr ""
msgid "Pagination|« First"
msgstr ""
msgid "Pagination|‹ Prev"
msgstr ""
msgid "Parameter"
msgstr ""
......@@ -18615,6 +18609,9 @@ msgstr ""
msgid "Subscription deletion failed."
msgstr ""
msgid "Subscription successfully applied to \"%{group_name}\""
msgstr ""
msgid "Subscription successfully created."
msgstr ""
......@@ -19633,6 +19630,12 @@ msgstr ""
msgid "There was an error trying to validate your query"
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."
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}."
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."
msgstr ""
......
......@@ -129,4 +129,130 @@ describe Projects::PerformanceMonitoring::DashboardsController do
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
......@@ -24,7 +24,7 @@ describe 'Thread Comments Commit', :js do
expect(page).to have_css('.js-note-emoji')
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
first('.emoji-menu .js-emoji-btn').click
......
......@@ -37,6 +37,16 @@ describe 'Projects tree', :js do
expect(page).not_to have_selector('.flash-alert')
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
let(:gravatar_enabled) { false }
......
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { shallowMount } from '@vue/test-utils';
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 {
SET_PROJECT,
......@@ -9,7 +10,7 @@ import {
} from '~/create_cluster/gke_cluster/store/mutation_types';
import { selectedZoneMock, selectedProjectMock, gapiZonesResponseMock } from '../mock_data';
const componentConfig = {
const propsData = {
fieldId: 'cluster_provider_gcp_attributes_gcp_zone',
fieldName: 'cluster[provider_gcp_attributes][gcp_zone]',
};
......@@ -20,75 +21,81 @@ const LABELS = {
DEFAULT: 'Select zone',
};
const createComponent = (store, props = componentConfig) => {
const Component = Vue.extend(GkeZoneDropdown);
return mountComponentWithStore(Component, {
el: null,
props,
store,
});
};
describe('GkeZoneDropdown', () => {
let vm;
let store;
let wrapper;
beforeEach(() => {
store = createStore();
vm = createComponent(store);
wrapper = shallowMount(GkeZoneDropdown, { propsData, store });
});
afterEach(() => {
vm.$destroy();
wrapper.destroy();
});
describe('toggleText', () => {
let dropdownButton;
beforeEach(() => {
dropdownButton = wrapper.find(DropdownButton);
});
it('returns disabled state toggle text', () => {
expect(vm.toggleText).toBe(LABELS.DISABLED);
expect(dropdownButton.props('toggleText')).toBe(LABELS.DISABLED);
});
it('returns loading toggle text', () => {
vm.isLoading = true;
describe('isLoading', () => {
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', () => {
expect(vm.toggleText).toBe(LABELS.DISABLED);
describe('project is set', () => {
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);
vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
expect(vm.toggleText).toBe(LABELS.DEFAULT);
it('returns default toggle text', () => {
expect(dropdownButton.props('toggleText')).toBe(LABELS.DEFAULT);
});
});
it('returns project name if project selected', () => {
vm.setItem(selectedZoneMock);
describe('project is selected', () => {
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', () => {
it('reflects new value when dropdown item is clicked', done => {
expect(vm.$el.querySelector('input').value).toBe('');
vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items);
return vm
.$nextTick()
.then(() => {
vm.$el.querySelector('.dropdown-content button').click();
return vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('input').value).toBe(selectedZoneMock);
done();
})
.catch(done.fail);
})
.catch(done.fail);
beforeEach(() => {
wrapper.vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items);
return wrapper.vm.$nextTick();
});
it('reflects new value when dropdown item is clicked', () => {
const dropdown = wrapper.find(DropdownHiddenInput);
expect(dropdown.attributes('value')).toBe('');
wrapper.find('.dropdown-content button').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(dropdown.attributes('value')).toBe(selectedZoneMock);
});
});
});
});
......@@ -41,7 +41,7 @@ describe('Repository breadcrumbs component', () => {
.findAll(RouterLinkStub)
.at(3)
.props('to'),
).toEqual('/-/tree//app/assets/javascripts%23');
).toEqual('/-/tree/app/assets/javascripts%23');
});
it('renders last link as active', () => {
......
......@@ -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', () => {
factory({
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', () => {
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 descExpandElement = getDescExpandElement(vm);
......
......@@ -14,6 +14,8 @@ export const issuable1 = {
path: '/foo/bar/issues/123',
state: 'opened',
linkType: 'relates_to',
dueDate: '2010-11-22',
weight: 5,
};
export const issuable2 = {
......
......@@ -212,7 +212,7 @@ describe Gitlab::Profiler do
stub_const('STDOUT', stdout)
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)
total_times =
......
......@@ -12,7 +12,7 @@ describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do
subject { cache.cache_key(cache_prefix) }
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
......
......@@ -129,6 +129,17 @@ describe Gitlab::TreeSummary do
expect(commits).to satisfy_one { |c| c.id == whitespace_commit_sha }
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
describe '#more?' do
......
......@@ -145,4 +145,28 @@ describe Ci::Processable do
expect(another_build.reload.scheduling_type).to be_nil
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
......@@ -112,6 +112,43 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
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
context 'with default reactive_cache_worker_finder' do
let(:args) { %w(other args) }
......
......@@ -8,7 +8,7 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
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
it { is_expected.to belong_to(:project) }
......@@ -453,4 +453,23 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
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
......@@ -16,7 +16,7 @@ describe API::Markdown do
shared_examples "rendered markdown text without GFM" 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(json_response).to be_a(Hash)
expect(json_response["html"]).to eq("<p>#{text}</p>")
......@@ -25,7 +25,7 @@ describe API::Markdown do
shared_examples "404 Project 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(json_response).to be_a(Hash)
expect(json_response["message"]).to eq("404 Project Not Found")
......@@ -37,7 +37,7 @@ describe API::Markdown do
let(:params) { {} }
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(json_response).to be_a(Hash)
expect(json_response["error"]).to eq("text is missing")
......@@ -83,7 +83,7 @@ describe API::Markdown do
let(:params) { { text: text, gfm: true } }
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(json_response).to be_a(Hash)
expect(json_response["html"]).to include("Hello world!")
......@@ -100,7 +100,7 @@ describe API::Markdown do
let(:user) { project.owner }
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(json_response).to be_a(Hash)
expect(json_response["html"]).to include("Hello world!")
......@@ -120,7 +120,7 @@ describe API::Markdown do
shared_examples 'user without proper access' 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('<a href=')
expect(json_response["html"]).to include('Hello world!')
......@@ -146,7 +146,7 @@ describe API::Markdown do
let(:user) { confidential_issue.author }
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('Hello world!')
.and include('data-name="tada"')
......
This diff is collapsed.
......@@ -28,12 +28,12 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' 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)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns a 404 when merge_request_iid not found' do
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
......@@ -51,17 +51,17 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' 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)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
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)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
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)
expect(response).to have_gitlab_http_status(404)
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
This diff is collapsed.
......@@ -12,7 +12,7 @@ describe API::Namespaces do
context "when unauthenticated" do
it "returns authentication error" do
get api("/namespaces")
expect(response).to have_gitlab_http_status(401)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
......@@ -23,7 +23,7 @@ describe API::Namespaces do
group_kind_json_response = json_response.find { |resource| resource['kind'] == 'group' }
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(group_kind_json_response.keys).to include('id', 'kind', 'name', 'path', 'full_path',
'parent_id', 'members_count_with_descendants')
......@@ -34,7 +34,7 @@ describe API::Namespaces do
it "admin: returns an array of all namespaces" do
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(json_response).to be_an Array
expect(json_response.length).to eq(Namespace.count)
......@@ -43,7 +43,7 @@ describe API::Namespaces do
it "admin: returns an array of matched namespaces" do
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(json_response).to be_an Array
expect(json_response.length).to eq(1)
......@@ -77,7 +77,7 @@ describe API::Namespaces do
it "user: returns an array of namespaces" do
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(json_response).to be_an Array
expect(json_response.length).to eq(1)
......@@ -86,7 +86,7 @@ describe API::Namespaces do
it "admin: returns an array of matched namespaces" do
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(json_response).to be_an Array
expect(json_response.length).to eq(1)
......@@ -102,7 +102,7 @@ describe API::Namespaces do
it 'returns namespace details' do
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['path']).to eq(requested_namespace.path)
......@@ -153,7 +153,7 @@ describe API::Namespaces do
it 'returns not-found' do
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
......@@ -162,7 +162,7 @@ describe API::Namespaces do
it 'returns authentication error' do
get api("/namespaces/#{group1.id}")
expect(response).to have_gitlab_http_status(401)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
......@@ -174,7 +174,7 @@ describe API::Namespaces do
it 'returns not-found' do
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
......@@ -182,7 +182,7 @@ describe API::Namespaces do
it 'returns not-found' do
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
......
......@@ -72,7 +72,7 @@ describe API::Notes do
it "returns an empty array" do
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(json_response).to be_an Array
expect(json_response).to be_empty
......@@ -86,7 +86,7 @@ describe API::Notes do
it "returns 404" do
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
......@@ -95,7 +95,7 @@ describe API::Notes do
it "returns a non-empty array" do
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(json_response).to be_an Array
expect(json_response.first['body']).to eq(cross_reference_note.note)
......@@ -114,7 +114,7 @@ describe API::Notes do
shared_examples 'a notes request' 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(json_response).to be_an Array
end
......@@ -177,7 +177,7 @@ describe API::Notes do
it "returns a 404 error" do
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
context "when issue is confidential" do
......@@ -188,7 +188,7 @@ describe API::Notes do
it "returns 404" do
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
......@@ -197,7 +197,7 @@ describe API::Notes 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)
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)
end
end
......@@ -237,7 +237,7 @@ describe API::Notes do
it 'returns 200 status' do
subject
expect(response).to have_gitlab_http_status(201)
expect(response).to have_gitlab_http_status(:created)
end
it 'creates a new note' do
......@@ -251,7 +251,7 @@ describe API::Notes do
it 'returns 403 status' do
subject
expect(response).to have_gitlab_http_status(403)
expect(response).to have_gitlab_http_status(:forbidden)
end
it 'does not create a new note' do
......
......@@ -11,7 +11,7 @@ describe API::NotificationSettings do
it "returns global notification settings for the current user" do
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['notification_email']).to eq(user.notification_email)
expect(json_response['level']).to eq(user.global_notification_setting.level)
......@@ -24,7 +24,7 @@ describe API::NotificationSettings do
it "updates global notification settings for the current user" do
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(user.reload.notification_email).to eq(email.email)
expect(json_response['level']).to eq(user.reload.global_notification_setting.level)
......@@ -35,7 +35,7 @@ describe API::NotificationSettings do
it "fails on non-user email address" do
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
......@@ -43,7 +43,7 @@ describe API::NotificationSettings do
it "returns group level notification settings for the current user" do
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['level']).to eq(user.notification_settings_for(group).level)
end
......@@ -53,7 +53,7 @@ describe API::NotificationSettings do
it "updates group level notification settings for the current user" do
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)
end
end
......@@ -62,7 +62,7 @@ describe API::NotificationSettings do
it "returns project level notification settings for the current user" do
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['level']).to eq(user.notification_settings_for(project).level)
end
......@@ -72,7 +72,7 @@ describe API::NotificationSettings 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 }
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['events']['new_note']).to be_truthy
expect(json_response['events']['new_issue']).to be_falsey
......@@ -83,7 +83,7 @@ describe API::NotificationSettings do
it "fails on invalid level" do
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
......@@ -14,7 +14,7 @@ describe 'OAuth tokens' do
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')
end
end
......@@ -25,7 +25,7 @@ describe 'OAuth tokens' do
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
end
end
......@@ -33,7 +33,7 @@ describe 'OAuth tokens' do
shared_examples 'does not create an access token' do
let(:user) { create(:user) }
it { expect(response).to have_gitlab_http_status(401) }
it { expect(response).to have_gitlab_http_status(:unauthorized) }
end
context 'when user is blocked' do
......
......@@ -1979,6 +1979,21 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
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
let(:new_tmpdir) { Dir.mktmpdir }
......
......@@ -3,8 +3,9 @@
require 'spec_helper'
describe Ci::CreateJobArtifactsService do
let(:service) { described_class.new }
let(:job) { create(:ci_build) }
let_it_be(:project) { create(:project) }
let(:service) { described_class.new(project) }
let(:job) { create(:ci_build, project: project) }
let(:artifacts_sha256) { '0' * 64 }
let(:metadata_file) { nil }
......@@ -64,7 +65,7 @@ describe Ci::CreateJobArtifactsService do
it 'sets expiration date according to application settings' do
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)
expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
......@@ -80,7 +81,7 @@ describe Ci::CreateJobArtifactsService do
it 'sets expiration date according to the parameter' do
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)
expect(job.artifacts_expire_at).to be_within(1.minute).of(expected_expire_at)
......@@ -101,21 +102,50 @@ describe Ci::CreateJobArtifactsService do
it 'ignores the changes' do
expect { subject }.not_to change { Ci::JobArtifact.count }
expect(subject).to be_truthy
expect(subject).to match(a_hash_including(status: :success))
end
end
context 'when sha256 of uploading artifact is different than the existing one' do
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 { subject }.not_to change { Ci::JobArtifact.count }
expect(subject).to be_falsey
expect(job.errors[:base]).to contain_exactly('another artifact of the same type already exists')
expect(subject).to match(
a_hash_including(http_status: :bad_request,
message: 'another artifact of the same type already exists',
status: :error))
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
......@@ -36,7 +36,7 @@ describe Ci::RetryBuildService do
job_artifacts_performance job_artifacts_lsif
job_artifacts_codequality job_artifacts_metrics scheduled_at
job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee].freeze
job_artifacts_network_referee needs].freeze
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections
......@@ -46,7 +46,8 @@ describe Ci::RetryBuildService do
sourced_pipelines artifacts_file_store artifacts_metadata_store
metadata runner_session trace_chunks upstream_pipeline_id
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
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
......@@ -79,8 +80,15 @@ describe Ci::RetryBuildService do
end
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|
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(new_build.send(attribute)).not_to be_nil
expect(new_build.send(attribute)).to eq build.send(attribute)
......@@ -97,9 +105,17 @@ describe Ci::RetryBuildService do
expect(new_build.protected).to eq build.protected
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
describe 'reject acessors' do
describe 'reject accessors' do
REJECT_ACCESSORS.each do |attribute|
it "does not clone #{attribute} build attribute" do
expect(new_build.send(attribute)).not_to eq build.send(attribute)
......@@ -117,8 +133,9 @@ describe Ci::RetryBuildService do
#
current_accessors =
Ci::Build.attribute_names.map(&:to_sym) +
Ci::Build.attribute_aliases.keys.map(&:to_sym) +
Ci::Build.reflect_on_all_associations.map(&:name) +
[:tag_list]
[:tag_list, :needs_attributes]
current_accessors.uniq!
......
......@@ -43,6 +43,12 @@ describe ErrorTracking::IssueUpdateService do
update_service.execute
end
it 'clears the reactive cache' do
expect(error_tracking_setting).to receive(:expire_issues_cache)
result
end
context 'related issue and resolving' do
let(:issue) { create(:issue, project: project) }
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,
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