Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
03c3f9f501
commit
b5249f2d99
12
Dangerfile
12
Dangerfile
|
@ -7,8 +7,12 @@ danger.import_plugin('danger/plugins/helper.rb')
|
|||
danger.import_plugin('danger/plugins/roulette.rb')
|
||||
danger.import_plugin('danger/plugins/changelog.rb')
|
||||
|
||||
unless helper.release_automation?
|
||||
GitlabDanger.new(helper.gitlab_helper).rule_names.each do |file|
|
||||
danger.import_dangerfile(path: File.join('danger', file))
|
||||
end
|
||||
return if helper.release_automation?
|
||||
|
||||
gitlab_danger = GitlabDanger.new(helper.gitlab_helper)
|
||||
|
||||
gitlab_danger.rule_names.each do |file|
|
||||
danger.import_dangerfile(path: File.join('danger', file))
|
||||
end
|
||||
|
||||
markdown("**If needed, you can retry the [`danger-review` job](#{ENV['CI_JOB_URL']}) that generated this comment.**") if gitlab_danger.ci?
|
||||
|
|
|
@ -14,7 +14,6 @@ import Editor from '../lib/editor';
|
|||
import FileTemplatesBar from './file_templates/bar.vue';
|
||||
import { __ } from '~/locale';
|
||||
import { extractMarkdownImagesFromEntries } from '../stores/utils';
|
||||
import { addFinalNewline } from '../utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -32,7 +31,6 @@ export default {
|
|||
return {
|
||||
content: '',
|
||||
images: {},
|
||||
addFinalNewline: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -253,10 +251,8 @@ export default {
|
|||
|
||||
const monacoModel = model.getModel();
|
||||
const content = monacoModel.getValue();
|
||||
this.changeFileContent({
|
||||
path: file.path,
|
||||
content: this.addFinalNewline ? addFinalNewline(content, monacoModel.getEOL()) : content,
|
||||
});
|
||||
this.changeFileContent({ path: file.path, content });
|
||||
this.setFileEOL({ eol: this.model.eol });
|
||||
});
|
||||
|
||||
// Handle Cursor Position
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { editor as monacoEditor, Uri } from 'monaco-editor';
|
||||
import Disposable from './disposable';
|
||||
import eventHub from '../../eventhub';
|
||||
import { trimTrailingWhitespace, insertFinalNewline } from '../../utils';
|
||||
import { defaultModelOptions } from '../editor_options';
|
||||
|
||||
export default class Model {
|
||||
constructor(file, head = null) {
|
||||
|
@ -8,6 +10,7 @@ export default class Model {
|
|||
this.file = file;
|
||||
this.head = head;
|
||||
this.content = file.content !== '' || file.deleted ? file.content : file.raw;
|
||||
this.options = { ...defaultModelOptions };
|
||||
|
||||
this.disposable.add(
|
||||
(this.originalModel = monacoEditor.createModel(
|
||||
|
@ -94,8 +97,32 @@ export default class Model {
|
|||
this.getModel().setValue(content);
|
||||
}
|
||||
|
||||
updateOptions(obj = {}) {
|
||||
Object.assign(this.options, obj);
|
||||
this.model.updateOptions(obj);
|
||||
this.applyCustomOptions();
|
||||
}
|
||||
|
||||
applyCustomOptions() {
|
||||
this.updateNewContent(
|
||||
Object.entries(this.options).reduce((content, [key, value]) => {
|
||||
switch (key) {
|
||||
case 'endOfLine':
|
||||
this.model.pushEOL(value);
|
||||
return this.model.getValue();
|
||||
case 'insertFinalNewline':
|
||||
return value ? insertFinalNewline(content) : content;
|
||||
case 'trimTrailingWhitespace':
|
||||
return value ? trimTrailingWhitespace(content) : content;
|
||||
default:
|
||||
return content;
|
||||
}
|
||||
}, this.model.getValue()),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
if (!this.model.isDisposed()) this.applyCustomOptions();
|
||||
|
||||
this.events.forEach(cb => {
|
||||
if (typeof cb === 'function') cb();
|
||||
|
@ -106,5 +133,7 @@ export default class Model {
|
|||
eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
|
||||
eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent);
|
||||
eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent);
|
||||
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,10 +50,15 @@ export default class DirtyDiffController {
|
|||
}
|
||||
|
||||
computeDiff(model) {
|
||||
const originalModel = model.getOriginalModel();
|
||||
const newModel = model.getModel();
|
||||
|
||||
if (originalModel.isDisposed() || newModel.isDisposed()) return;
|
||||
|
||||
this.dirtyDiffWorker.postMessage({
|
||||
path: model.path,
|
||||
originalContent: model.getOriginalModel().getValue(),
|
||||
newContent: model.getModel().getValue(),
|
||||
originalContent: originalModel.getValue(),
|
||||
newContent: newModel.getValue(),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,15 @@
|
|||
import { diffLines } from 'diff';
|
||||
import { defaultDiffOptions } from '../editor_options';
|
||||
|
||||
// See: https://gitlab.com/gitlab-org/frontend/rfcs/-/issues/20
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const computeDiff = (originalContent, newContent) => {
|
||||
const changes = diffLines(originalContent, newContent);
|
||||
// prevent EOL changes from highlighting the entire file
|
||||
const changes = diffLines(
|
||||
originalContent.replace(/\r\n/g, '\n'),
|
||||
newContent.replace(/\r\n/g, '\n'),
|
||||
defaultDiffOptions,
|
||||
);
|
||||
|
||||
let lineNumber = 1;
|
||||
return changes.reduce((acc, change) => {
|
||||
|
|
|
@ -5,7 +5,7 @@ import DecorationsController from './decorations/controller';
|
|||
import DirtyDiffController from './diff/controller';
|
||||
import Disposable from './common/disposable';
|
||||
import ModelManager from './common/model_manager';
|
||||
import editorOptions, { defaultEditorOptions } from './editor_options';
|
||||
import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
|
||||
import { themes } from './themes';
|
||||
import languages from './languages';
|
||||
import keymap from './keymap.json';
|
||||
|
@ -73,8 +73,7 @@ export default class Editor {
|
|||
this.disposable.add(
|
||||
(this.instance = monacoEditor.createDiffEditor(domElement, {
|
||||
...this.options,
|
||||
quickSuggestions: false,
|
||||
occurrencesHighlight: false,
|
||||
...defaultDiffEditorOptions,
|
||||
renderSideBySide: Editor.renderSideBySide(domElement),
|
||||
readOnly,
|
||||
renderLineHighlight: readOnly ? 'all' : 'none',
|
||||
|
|
|
@ -9,7 +9,23 @@ export const defaultEditorOptions = {
|
|||
wordWrap: 'on',
|
||||
};
|
||||
|
||||
export default [
|
||||
export const defaultDiffOptions = {
|
||||
ignoreWhitespace: false,
|
||||
};
|
||||
|
||||
export const defaultDiffEditorOptions = {
|
||||
quickSuggestions: false,
|
||||
occurrencesHighlight: false,
|
||||
ignoreTrimWhitespace: false,
|
||||
};
|
||||
|
||||
export const defaultModelOptions = {
|
||||
endOfLine: 0,
|
||||
insertFinalNewline: true,
|
||||
trimTrailingWhitespace: false,
|
||||
};
|
||||
|
||||
export const editorOptions = [
|
||||
{
|
||||
readOnly: model => Boolean(model.file.file_lock),
|
||||
quickSuggestions: model => !(model.language === 'markdown'),
|
||||
|
|
|
@ -77,7 +77,11 @@ export function registerLanguages(def, ...defs) {
|
|||
|
||||
export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
|
||||
|
||||
export function addFinalNewline(content, eol = '\n') {
|
||||
export function trimTrailingWhitespace(content) {
|
||||
return content.replace(/[^\S\r\n]+$/gm, '');
|
||||
}
|
||||
|
||||
export function insertFinalNewline(content, eol = '\n') {
|
||||
return content.slice(-eol.length) !== eol ? `${content}${eol}` : content;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
before_action :runner, except: [:index, :tag_list]
|
||||
|
||||
def index
|
||||
finder = Admin::RunnersFinder.new(params: params)
|
||||
finder = Ci::RunnersFinder.new(current_user: current_user, params: params)
|
||||
@runners = finder.execute
|
||||
@active_runners_count = Ci::Runner.online.count
|
||||
@sort = finder.sort_key
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::RunnersFinder < UnionFinder
|
||||
NUMBER_OF_RUNNERS_PER_PAGE = 30
|
||||
|
||||
def initialize(params:)
|
||||
@params = params
|
||||
end
|
||||
|
||||
def execute
|
||||
search!
|
||||
filter_by_status!
|
||||
filter_by_runner_type!
|
||||
filter_by_tag_list!
|
||||
sort!
|
||||
paginate!
|
||||
|
||||
@runners.with_tags
|
||||
end
|
||||
|
||||
def sort_key
|
||||
if @params[:sort] == 'contacted_asc'
|
||||
'contacted_asc'
|
||||
else
|
||||
'created_date'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search!
|
||||
@runners =
|
||||
if @params[:search].present?
|
||||
Ci::Runner.search(@params[:search])
|
||||
else
|
||||
Ci::Runner.all
|
||||
end
|
||||
end
|
||||
|
||||
def filter_by_status!
|
||||
filter_by!(:status_status, Ci::Runner::AVAILABLE_STATUSES)
|
||||
end
|
||||
|
||||
def filter_by_runner_type!
|
||||
filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES)
|
||||
end
|
||||
|
||||
def filter_by_tag_list!
|
||||
tag_list = @params[:tag_name].presence
|
||||
|
||||
if tag_list
|
||||
@runners = @runners.tagged_with(tag_list)
|
||||
end
|
||||
end
|
||||
|
||||
def sort!
|
||||
@runners = @runners.order_by(sort_key)
|
||||
end
|
||||
|
||||
def paginate!
|
||||
@runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
|
||||
end
|
||||
|
||||
def filter_by!(scope_name, available_scopes)
|
||||
scope = @params[scope_name]
|
||||
|
||||
if scope.present? && available_scopes.include?(scope)
|
||||
@runners = @runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,92 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Ci
|
||||
class RunnersFinder < UnionFinder
|
||||
include Gitlab::Allowable
|
||||
|
||||
NUMBER_OF_RUNNERS_PER_PAGE = 30
|
||||
|
||||
def initialize(current_user:, group: nil, params:)
|
||||
@params = params
|
||||
@group = group
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def execute
|
||||
search!
|
||||
filter_by_status!
|
||||
filter_by_runner_type!
|
||||
filter_by_tag_list!
|
||||
sort!
|
||||
paginate!
|
||||
|
||||
@runners.with_tags
|
||||
|
||||
rescue Gitlab::Access::AccessDeniedError
|
||||
Ci::Runner.none
|
||||
end
|
||||
|
||||
def sort_key
|
||||
if @params[:sort] == 'contacted_asc'
|
||||
'contacted_asc'
|
||||
else
|
||||
'created_date'
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def search!
|
||||
@group ? group_runners : all_runners
|
||||
|
||||
@runners = @runners.search(@params[:search]) if @params[:search].present?
|
||||
end
|
||||
|
||||
def all_runners
|
||||
raise Gitlab::Access::AccessDeniedError unless @current_user&.admin?
|
||||
|
||||
@runners = Ci::Runner.all
|
||||
end
|
||||
|
||||
def group_runners
|
||||
raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_group, @group)
|
||||
|
||||
# Getting all runners from the group itself and all its descendants
|
||||
descentant_projects = Project.for_group_and_its_subgroups(@group)
|
||||
|
||||
@runners = Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descentant_projects)
|
||||
end
|
||||
|
||||
def filter_by_status!
|
||||
filter_by!(:status_status, Ci::Runner::AVAILABLE_STATUSES)
|
||||
end
|
||||
|
||||
def filter_by_runner_type!
|
||||
filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES)
|
||||
end
|
||||
|
||||
def filter_by_tag_list!
|
||||
tag_list = @params[:tag_name].presence
|
||||
|
||||
if tag_list
|
||||
@runners = @runners.tagged_with(tag_list)
|
||||
end
|
||||
end
|
||||
|
||||
def sort!
|
||||
@runners = @runners.order_by(sort_key)
|
||||
end
|
||||
|
||||
def paginate!
|
||||
@runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
|
||||
end
|
||||
|
||||
def filter_by!(scope_name, available_scopes)
|
||||
scope = @params[scope_name]
|
||||
|
||||
if scope.present? && available_scopes.include?(scope)
|
||||
@runners = @runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class UploaderFinder
|
||||
# Instantiates a a new FileUploader
|
||||
# FileUploader can be opened via .open agnostic of storage type
|
||||
# Arguments correspond to Upload.secret, Upload.model_type and Upload.file_path
|
||||
# Returns a FileUploader with uploaded file retrieved into the object state
|
||||
def initialize(project, secret, file_path)
|
||||
@project = project
|
||||
@secret = secret
|
||||
@file_path = file_path
|
||||
end
|
||||
|
||||
def execute
|
||||
prevent_path_traversal_attack!
|
||||
retrieve_file_state!
|
||||
|
||||
uploader
|
||||
rescue ::Gitlab::Utils::PathTraversalAttackError
|
||||
nil # no-op if for incorrect files
|
||||
end
|
||||
|
||||
def prevent_path_traversal_attack!
|
||||
Gitlab::Utils.check_path_traversal!(@file_path)
|
||||
end
|
||||
|
||||
def retrieve_file_state!
|
||||
uploader.retrieve_from_store!(@file_path)
|
||||
end
|
||||
|
||||
def uploader
|
||||
@uploader ||= FileUploader.new(@project, secret: @secret)
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@
|
|||
module Ci
|
||||
class InstanceVariable < ApplicationRecord
|
||||
extend Gitlab::Ci::Model
|
||||
extend Gitlab::ProcessMemoryCache::Helper
|
||||
include Ci::NewHasVariable
|
||||
include Ci::Maskable
|
||||
|
||||
|
@ -13,7 +14,8 @@ module Ci
|
|||
}
|
||||
|
||||
scope :unprotected, -> { where(protected: false) }
|
||||
after_commit { self.class.touch_redis_cache_timestamp }
|
||||
|
||||
after_commit { self.class.invalidate_memory_cache(:ci_instance_variable_data) }
|
||||
|
||||
class << self
|
||||
def all_cached
|
||||
|
@ -24,10 +26,6 @@ module Ci
|
|||
cached_data[:unprotected]
|
||||
end
|
||||
|
||||
def touch_redis_cache_timestamp(time = Time.current.to_f)
|
||||
shared_backend.write(:ci_instance_variable_changed_at, time)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cached_data
|
||||
|
@ -37,40 +35,6 @@ module Ci
|
|||
{ all: all_records, unprotected: all_records.reject(&:protected?) }
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_memory_cache(key, &payload)
|
||||
cache = process_backend.read(key)
|
||||
|
||||
if cache && !stale_cache?(cache)
|
||||
cache[:data]
|
||||
else
|
||||
store_cache(key, &payload)
|
||||
end
|
||||
end
|
||||
|
||||
def stale_cache?(cache_info)
|
||||
shared_timestamp = shared_backend.read(:ci_instance_variable_changed_at)
|
||||
return true unless shared_timestamp
|
||||
|
||||
shared_timestamp.to_f > cache_info[:cached_at].to_f
|
||||
end
|
||||
|
||||
def store_cache(key)
|
||||
data = yield
|
||||
time = Time.current.to_f
|
||||
|
||||
process_backend.write(key, data: data, cached_at: time)
|
||||
touch_redis_cache_timestamp(time)
|
||||
data
|
||||
end
|
||||
|
||||
def shared_backend
|
||||
Rails.cache
|
||||
end
|
||||
|
||||
def process_backend
|
||||
Gitlab::ProcessMemoryCache.cache_backend
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -81,6 +81,17 @@ module Ci
|
|||
joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups })
|
||||
}
|
||||
|
||||
scope :belonging_to_group_or_project, -> (group_id, project_id) {
|
||||
groups = ::Group.where(id: group_id)
|
||||
|
||||
group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups })
|
||||
project_runners = joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
|
||||
|
||||
union_sql = ::Gitlab::SQL::Union.new([group_runners, project_runners]).to_sql
|
||||
|
||||
from("(#{union_sql}) #{table_name}")
|
||||
}
|
||||
|
||||
scope :belonging_to_parent_group_of_project, -> (project_id) {
|
||||
project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
|
||||
hierarchy_groups = Gitlab::ObjectHierarchy.new(project_groups).base_and_ancestors
|
||||
|
|
|
@ -319,7 +319,7 @@
|
|||
|
||||
- if project_nav_tab? :snippets
|
||||
= nav_link(controller: :snippets) do
|
||||
= link_to project_snippets_path(@project), class: 'shortcuts-snippets' do
|
||||
= link_to project_snippets_path(@project), class: 'shortcuts-snippets', data: { qa_selector: 'snippets_link' } do
|
||||
.nav-icon-container
|
||||
= sprite_icon('snippet')
|
||||
%span.nav-item-name
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.row.empty-state.mt-0
|
||||
.col-12
|
||||
.svg-content
|
||||
= image_tag 'illustrations/snippets_empty.svg'
|
||||
= image_tag 'illustrations/snippets_empty.svg', data: { qa_selector: 'svg_content' }
|
||||
.text-content.text-center.pt-0
|
||||
- if current_user
|
||||
%h4
|
||||
|
@ -12,7 +12,7 @@
|
|||
= s_('SnippetsEmptyState|Store, share, and embed small pieces of code and text.')
|
||||
.mt-2<
|
||||
- if button_path
|
||||
= link_to s_('SnippetsEmptyState|New snippet'), button_path, class: 'btn btn-success', title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link'
|
||||
= link_to s_('SnippetsEmptyState|New snippet'), button_path, class: 'btn btn-success', title: s_('SnippetsEmptyState|New snippet'), id: 'new_snippet_link', data: { qa_selector: 'create_first_snippet_link' }
|
||||
= link_to s_('SnippetsEmptyState|Documentation'), help_page_path('user/snippets.md'), class: 'btn btn-default', title: s_('SnippetsEmptyState|Documentation')
|
||||
- else
|
||||
%h4.text-center= s_('SnippetsEmptyState|There are no snippets to show.')
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add finder for group-level runners
|
||||
merge_request: 29283
|
||||
author: Arthur de Lapertosa Lisboa
|
||||
type: added
|
|
@ -71,7 +71,8 @@ The incident detail page shows detailed information about a particular incident.
|
|||
|
||||
- Status on the incident, including when the incident was last updated.
|
||||
- The incident title, including any emojis.
|
||||
- The description of the incident, including emojis and static images.
|
||||
- The description of the incident, including emojis.
|
||||
- Any file attachments provided in the incident description or comments with a valid image extension. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205166) in GitLab 13.1.
|
||||
- A chronological ordered list of updates to the incident.
|
||||
|
||||
![Status Page detail](../img/status_page_detail_v12_10.png)
|
||||
|
@ -108,3 +109,15 @@ Anyone with access to view the Issue can add an Emoji Award to a comment, so you
|
|||
### Changing the Incident status
|
||||
|
||||
To change the incident status from `open` to `closed`, close the incident issue within GitLab. This will then be updated shortly on the Status Page website.
|
||||
|
||||
## Attachment storage
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205166) in GitLab 13.1.
|
||||
|
||||
Beginning with GitLab 13.1, files attached to incident issue descriptions or
|
||||
comments are published and unpublished to the status page storage as part of
|
||||
the [publication flow](#how-it-works).
|
||||
|
||||
### Limit
|
||||
|
||||
Only 5000 attachments per issue will be transferred to the status page.
|
||||
|
|
|
@ -8,7 +8,7 @@ module API
|
|||
path = params[attr_name]
|
||||
|
||||
Gitlab::Utils.check_path_traversal!(path)
|
||||
rescue StandardError
|
||||
rescue ::Gitlab::Utils::PathTraversalAttackError
|
||||
raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)],
|
||||
message: "should be a valid file path"
|
||||
end
|
||||
|
|
|
@ -22,9 +22,10 @@ module Gitlab
|
|||
return @text unless needs_rewrite?
|
||||
|
||||
@text.gsub(@pattern) do |markdown|
|
||||
Gitlab::Utils.check_path_traversal!($~[:file])
|
||||
file = find_file($~[:secret], $~[:file])
|
||||
# No file will be returned for a path traversal
|
||||
next if file.nil?
|
||||
|
||||
file = find_file(@source_project, $~[:secret], $~[:file])
|
||||
break markdown unless file.try(:exists?)
|
||||
|
||||
klass = target_parent.is_a?(Namespace) ? NamespaceFileUploader : FileUploader
|
||||
|
@ -47,7 +48,7 @@ module Gitlab
|
|||
|
||||
def files
|
||||
referenced_files = @text.scan(@pattern).map do
|
||||
find_file(@source_project, $~[:secret], $~[:file])
|
||||
find_file($~[:secret], $~[:file])
|
||||
end
|
||||
|
||||
referenced_files.compact.select(&:exists?)
|
||||
|
@ -57,12 +58,8 @@ module Gitlab
|
|||
markdown.starts_with?("!")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_file(project, secret, file)
|
||||
uploader = FileUploader.new(project, secret: secret)
|
||||
uploader.retrieve_from_store!(file)
|
||||
uploader
|
||||
def find_file(secret, file_name)
|
||||
UploaderFinder.new(@source_project, secret, file_name).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
class ProcessMemoryCache
|
||||
module Helper
|
||||
def fetch_memory_cache(key, &payload)
|
||||
cache = cache_backend.read(key)
|
||||
|
||||
if cache && !stale_cache?(key, cache)
|
||||
cache[:data]
|
||||
else
|
||||
store_cache(key, &payload)
|
||||
end
|
||||
end
|
||||
|
||||
def invalidate_memory_cache(key)
|
||||
touch_cache_timestamp(key)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def touch_cache_timestamp(key, time = Time.current.to_f)
|
||||
shared_backend.write(key, time)
|
||||
end
|
||||
|
||||
def stale_cache?(key, cache_info)
|
||||
shared_timestamp = shared_backend.read(key)
|
||||
return true unless shared_timestamp
|
||||
|
||||
shared_timestamp.to_f > cache_info[:cached_at].to_f
|
||||
end
|
||||
|
||||
def store_cache(key)
|
||||
data = yield
|
||||
time = Time.current.to_f
|
||||
|
||||
cache_backend.write(key, data: data, cached_at: time)
|
||||
touch_cache_timestamp(key, time)
|
||||
data
|
||||
end
|
||||
|
||||
def shared_backend
|
||||
Rails.cache
|
||||
end
|
||||
|
||||
def cache_backend
|
||||
::Gitlab::ProcessMemoryCache.cache_backend
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,6 +3,7 @@
|
|||
module Gitlab
|
||||
module Utils
|
||||
extend self
|
||||
PathTraversalAttackError ||= Class.new(StandardError)
|
||||
|
||||
# Ensure that the relative path will not traverse outside the base directory
|
||||
# We url decode the path to avoid passing invalid paths forward in url encoded format.
|
||||
|
@ -17,7 +18,7 @@ module Gitlab
|
|||
path.end_with?("#{File::SEPARATOR}..") ||
|
||||
(!allowed_absolute && Pathname.new(path).absolute?)
|
||||
|
||||
raise StandardError.new("Invalid path")
|
||||
raise PathTraversalAttackError.new('Invalid path')
|
||||
end
|
||||
|
||||
path
|
||||
|
|
5
qa/qa.rb
5
qa/qa.rb
|
@ -86,6 +86,7 @@ module QA
|
|||
autoload :Snippet, 'qa/resource/snippet'
|
||||
autoload :Tag, 'qa/resource/tag'
|
||||
autoload :ProjectMember, 'qa/resource/project_member'
|
||||
autoload :ProjectSnippet, 'qa/resource/project_snippet'
|
||||
autoload :UserGPG, 'qa/resource/user_gpg'
|
||||
autoload :Visibility, 'qa/resource/visibility'
|
||||
|
||||
|
@ -332,6 +333,10 @@ module QA
|
|||
module WebIDE
|
||||
autoload :Edit, 'qa/page/project/web_ide/edit'
|
||||
end
|
||||
|
||||
module Snippet
|
||||
autoload :New, 'qa/page/project/snippet/new'
|
||||
end
|
||||
end
|
||||
|
||||
module Profile
|
||||
|
|
|
@ -16,6 +16,7 @@ module QA
|
|||
element :activity_link
|
||||
element :merge_requests_link
|
||||
element :wiki_link
|
||||
element :snippets_link
|
||||
end
|
||||
|
||||
def click_merge_requests
|
||||
|
@ -35,6 +36,12 @@ module QA
|
|||
click_element(:activity_link)
|
||||
end
|
||||
end
|
||||
|
||||
def click_snippets
|
||||
within_sidebar do
|
||||
click_element(:snippets_link)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Page
|
||||
module Project
|
||||
module Snippet
|
||||
class New < Page::Dashboard::Snippet::New
|
||||
include Component::LazyLoader
|
||||
view 'app/views/shared/empty_states/_snippets.html.haml' do
|
||||
element :create_first_snippet_link
|
||||
element :svg_content
|
||||
end
|
||||
|
||||
def click_create_first_snippet
|
||||
finished_loading?
|
||||
# The svg takes a fraction of a second to load after which the
|
||||
# "New snippet" button shifts up a bit. This can cause
|
||||
# webdriver to miss the hit so we wait for the svg to load before
|
||||
# clicking the button.
|
||||
within_element(:svg_content) do
|
||||
has_element?(:js_lazy_loaded)
|
||||
end
|
||||
click_element(:create_first_snippet_link)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
module Resource
|
||||
class ProjectSnippet < Snippet
|
||||
attribute :project do
|
||||
Project.fabricate_via_api! do |resource|
|
||||
resource.name = 'project-with-snippets'
|
||||
end
|
||||
end
|
||||
|
||||
def fabricate!
|
||||
project.visit!
|
||||
|
||||
Page::Project::Menu.perform { |sidebar| sidebar.click_snippets }
|
||||
|
||||
Page::Project::Snippet::New.perform do |new_snippet|
|
||||
new_snippet.click_create_first_snippet
|
||||
new_snippet.fill_title(@title)
|
||||
new_snippet.fill_description(@description)
|
||||
new_snippet.set_visibility(@visibility)
|
||||
new_snippet.fill_file_name(@file_name)
|
||||
new_snippet.fill_file_content(@file_content)
|
||||
new_snippet.click_create_snippet_button
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module QA
|
||||
context 'Create' do
|
||||
describe 'Version control for project snippets' do
|
||||
let(:new_file) { 'new_snippet_file' }
|
||||
let(:changed_content) { 'changes' }
|
||||
let(:commit_message) { 'Changes to snippets' }
|
||||
let(:added_content) { 'updated ' }
|
||||
let(:branch_name) { 'master' }
|
||||
|
||||
let(:snippet) do
|
||||
Resource::ProjectSnippet.fabricate! do |snippet|
|
||||
snippet.file_name = new_file
|
||||
end
|
||||
end
|
||||
|
||||
let(:ssh_key) do
|
||||
Resource::SSHKey.fabricate_via_api! do |resource|
|
||||
resource.title = "my key title #{Time.now.to_f}"
|
||||
end
|
||||
end
|
||||
|
||||
let(:repository_uri_http) do
|
||||
snippet
|
||||
Page::Dashboard::Snippet::Show.perform(&:get_repository_uri_http)
|
||||
end
|
||||
|
||||
let(:repository_uri_ssh) do
|
||||
ssh_key
|
||||
snippet
|
||||
Page::Dashboard::Snippet::Show.perform(&:get_repository_uri_ssh)
|
||||
end
|
||||
|
||||
before do
|
||||
Flow::Login.sign_in
|
||||
end
|
||||
|
||||
it 'clones, pushes, and pulls a project snippet over HTTP, edits via UI' do
|
||||
Resource::Repository::Push.fabricate! do |push|
|
||||
push.repository_http_uri = repository_uri_http
|
||||
push.file_name = new_file
|
||||
push.file_content = changed_content
|
||||
push.commit_message = commit_message
|
||||
push.new_branch = false
|
||||
end
|
||||
|
||||
page.refresh
|
||||
verify_changes_in_ui
|
||||
|
||||
Page::Dashboard::Snippet::Show.perform(&:click_edit_button)
|
||||
|
||||
Page::Dashboard::Snippet::Edit.perform do |snippet|
|
||||
snippet.add_to_file_content(added_content)
|
||||
snippet.save_changes
|
||||
end
|
||||
|
||||
Git::Repository.perform do |repository|
|
||||
repository.init_repository
|
||||
repository.pull(repository_uri_http, branch_name)
|
||||
|
||||
expect(repository.commits.size).to eq 3
|
||||
expect(repository.commits.first).to include 'Update snippet'
|
||||
expect(repository.file_content(new_file)).to include "#{added_content}#{changed_content}"
|
||||
end
|
||||
end
|
||||
|
||||
it 'clones, pushes, and pulls a project snippet over SSH, deletes via UI' do
|
||||
Resource::Repository::Push.fabricate! do |push|
|
||||
push.repository_ssh_uri = repository_uri_ssh
|
||||
push.ssh_key = ssh_key
|
||||
push.file_name = new_file
|
||||
push.file_content = changed_content
|
||||
push.commit_message = commit_message
|
||||
push.new_branch = false
|
||||
end
|
||||
|
||||
page.refresh
|
||||
verify_changes_in_ui
|
||||
Page::Dashboard::Snippet::Show.perform(&:click_delete_button)
|
||||
|
||||
# attempt to pull a deleted snippet, get a missing repository error
|
||||
Git::Repository.perform do |repository|
|
||||
repository.uri = repository_uri_ssh
|
||||
repository.use_ssh_key(ssh_key)
|
||||
repository.init_repository
|
||||
|
||||
expect { repository.pull(repository_uri_ssh, branch_name) }
|
||||
.to raise_error(QA::Git::Repository::RepositoryCommandError, /[fatal: Could not read from remote repository.]+/)
|
||||
end
|
||||
end
|
||||
|
||||
def verify_changes_in_ui
|
||||
Page::Dashboard::Snippet::Show.perform do |snippet|
|
||||
expect(snippet).to have_file_name(new_file)
|
||||
expect(snippet).to have_file_content(changed_content)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,81 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Admin::RunnersFinder do
|
||||
describe '#execute' do
|
||||
context 'with empty params' do
|
||||
it 'returns all runners' do
|
||||
runner1 = create :ci_runner, active: true
|
||||
runner2 = create :ci_runner, active: false
|
||||
|
||||
expect(described_class.new(params: {}).execute).to match_array [runner1, runner2]
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by search term' do
|
||||
it 'calls Ci::Runner.search' do
|
||||
expect(Ci::Runner).to receive(:search).with('term').and_call_original
|
||||
|
||||
described_class.new(params: { search: 'term' }).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by status' do
|
||||
it 'calls the corresponding scope on Ci::Runner' do
|
||||
expect(Ci::Runner).to receive(:paused).and_call_original
|
||||
|
||||
described_class.new(params: { status_status: 'paused' }).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by runner type' do
|
||||
it 'calls the corresponding scope on Ci::Runner' do
|
||||
expect(Ci::Runner).to receive(:project_type).and_call_original
|
||||
|
||||
described_class.new(params: { type_type: 'project_type' }).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by tag_name' do
|
||||
it 'calls the corresponding scope on Ci::Runner' do
|
||||
expect(Ci::Runner).to receive(:tagged_with).with(%w[tag1 tag2]).and_call_original
|
||||
|
||||
described_class.new(params: { tag_name: %w[tag1 tag2] }).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'sort' do
|
||||
context 'without sort param' do
|
||||
it 'sorts by created_at' do
|
||||
runner1 = create :ci_runner, created_at: '2018-07-12 07:00'
|
||||
runner2 = create :ci_runner, created_at: '2018-07-12 08:00'
|
||||
runner3 = create :ci_runner, created_at: '2018-07-12 09:00'
|
||||
|
||||
expect(described_class.new(params: {}).execute).to eq [runner3, runner2, runner1]
|
||||
end
|
||||
end
|
||||
|
||||
context 'with sort param' do
|
||||
it 'sorts by specified attribute' do
|
||||
runner1 = create :ci_runner, contacted_at: 1.minute.ago
|
||||
runner2 = create :ci_runner, contacted_at: 3.minutes.ago
|
||||
runner3 = create :ci_runner, contacted_at: 2.minutes.ago
|
||||
|
||||
expect(described_class.new(params: { sort: 'contacted_asc' }).execute).to eq [runner2, runner3, runner1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'paginate' do
|
||||
it 'returns the runners for the specified page' do
|
||||
stub_const('Admin::RunnersFinder::NUMBER_OF_RUNNERS_PER_PAGE', 1)
|
||||
runner1 = create :ci_runner, created_at: '2018-07-12 07:00'
|
||||
runner2 = create :ci_runner, created_at: '2018-07-12 08:00'
|
||||
|
||||
expect(described_class.new(params: { page: 1 }).execute).to eq [runner2]
|
||||
expect(described_class.new(params: { page: 2 }).execute).to eq [runner1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,304 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Ci::RunnersFinder do
|
||||
context 'admin' do
|
||||
let_it_be(:admin) { create(:user, :admin) }
|
||||
|
||||
describe '#execute' do
|
||||
context 'with empty params' do
|
||||
it 'returns all runners' do
|
||||
runner1 = create :ci_runner, active: true
|
||||
runner2 = create :ci_runner, active: false
|
||||
|
||||
expect(described_class.new(current_user: admin, params: {}).execute).to match_array [runner1, runner2]
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by search term' do
|
||||
it 'calls Ci::Runner.search' do
|
||||
expect(Ci::Runner).to receive(:search).with('term').and_call_original
|
||||
|
||||
described_class.new(current_user: admin, params: { search: 'term' }).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by status' do
|
||||
it 'calls the corresponding scope on Ci::Runner' do
|
||||
expect(Ci::Runner).to receive(:paused).and_call_original
|
||||
|
||||
described_class.new(current_user: admin, params: { status_status: 'paused' }).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by runner type' do
|
||||
it 'calls the corresponding scope on Ci::Runner' do
|
||||
expect(Ci::Runner).to receive(:project_type).and_call_original
|
||||
|
||||
described_class.new(current_user: admin, params: { type_type: 'project_type' }).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by tag_name' do
|
||||
it 'calls the corresponding scope on Ci::Runner' do
|
||||
expect(Ci::Runner).to receive(:tagged_with).with(%w[tag1 tag2]).and_call_original
|
||||
|
||||
described_class.new(current_user: admin, params: { tag_name: %w[tag1 tag2] }).execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'sort' do
|
||||
context 'without sort param' do
|
||||
it 'sorts by created_at' do
|
||||
runner1 = create :ci_runner, created_at: '2018-07-12 07:00'
|
||||
runner2 = create :ci_runner, created_at: '2018-07-12 08:00'
|
||||
runner3 = create :ci_runner, created_at: '2018-07-12 09:00'
|
||||
|
||||
expect(described_class.new(current_user: admin, params: {}).execute).to eq [runner3, runner2, runner1]
|
||||
end
|
||||
end
|
||||
|
||||
context 'with sort param' do
|
||||
it 'sorts by specified attribute' do
|
||||
runner1 = create :ci_runner, contacted_at: 1.minute.ago
|
||||
runner2 = create :ci_runner, contacted_at: 3.minutes.ago
|
||||
runner3 = create :ci_runner, contacted_at: 2.minutes.ago
|
||||
|
||||
expect(described_class.new(current_user: admin, params: { sort: 'contacted_asc' }).execute).to eq [runner2, runner3, runner1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'paginate' do
|
||||
it 'returns the runners for the specified page' do
|
||||
stub_const('Ci::RunnersFinder::NUMBER_OF_RUNNERS_PER_PAGE', 1)
|
||||
runner1 = create :ci_runner, created_at: '2018-07-12 07:00'
|
||||
runner2 = create :ci_runner, created_at: '2018-07-12 08:00'
|
||||
|
||||
expect(described_class.new(current_user: admin, params: { page: 1 }).execute).to eq [runner2]
|
||||
expect(described_class.new(current_user: admin, params: { page: 2 }).execute).to eq [runner1]
|
||||
end
|
||||
end
|
||||
|
||||
context 'non admin user' do
|
||||
it 'returns no runners' do
|
||||
user = create :user
|
||||
create :ci_runner, active: true
|
||||
create :ci_runner, active: false
|
||||
|
||||
expect(described_class.new(current_user: user, params: {}).execute).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'user is nil' do
|
||||
it 'returns no runners' do
|
||||
user = nil
|
||||
create :ci_runner, active: true
|
||||
create :ci_runner, active: false
|
||||
|
||||
expect(described_class.new(current_user: user, params: {}).execute).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'group' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:sub_group_1) { create(:group, parent: group) }
|
||||
let_it_be(:sub_group_2) { create(:group, parent: group) }
|
||||
let_it_be(:sub_group_3) { create(:group, parent: sub_group_1) }
|
||||
let_it_be(:sub_group_4) { create(:group, parent: sub_group_3) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
let_it_be(:project_2) { create(:project, group: group) }
|
||||
let_it_be(:project_3) { create(:project, group: sub_group_1) }
|
||||
let_it_be(:project_4) { create(:project, group: sub_group_2) }
|
||||
let_it_be(:project_5) { create(:project, group: sub_group_3) }
|
||||
let_it_be(:project_6) { create(:project, group: sub_group_4) }
|
||||
let_it_be(:runner_group) { create(:ci_runner, :group, contacted_at: 12.minutes.ago) }
|
||||
let_it_be(:runner_sub_group_1) { create(:ci_runner, :group, active: false, contacted_at: 11.minutes.ago) }
|
||||
let_it_be(:runner_sub_group_2) { create(:ci_runner, :group, contacted_at: 10.minutes.ago) }
|
||||
let_it_be(:runner_sub_group_3) { create(:ci_runner, :group, contacted_at: 9.minutes.ago) }
|
||||
let_it_be(:runner_sub_group_4) { create(:ci_runner, :group, contacted_at: 8.minutes.ago) }
|
||||
let_it_be(:runner_project_1) { create(:ci_runner, :project, contacted_at: 7.minutes.ago, projects: [project])}
|
||||
let_it_be(:runner_project_2) { create(:ci_runner, :project, contacted_at: 6.minutes.ago, projects: [project_2])}
|
||||
let_it_be(:runner_project_3) { create(:ci_runner, :project, contacted_at: 5.minutes.ago, description: 'runner_project_search', projects: [project, project_2])}
|
||||
let_it_be(:runner_project_4) { create(:ci_runner, :project, contacted_at: 4.minutes.ago, projects: [project_3])}
|
||||
let_it_be(:runner_project_5) { create(:ci_runner, :project, contacted_at: 3.minutes.ago, tag_list: %w[runner_tag], projects: [project_4])}
|
||||
let_it_be(:runner_project_6) { create(:ci_runner, :project, contacted_at: 2.minutes.ago, projects: [project_5])}
|
||||
let_it_be(:runner_project_7) { create(:ci_runner, :project, contacted_at: 1.minute.ago, projects: [project_6])}
|
||||
|
||||
let(:params) { {} }
|
||||
|
||||
before do
|
||||
group.runners << runner_group
|
||||
sub_group_1.runners << runner_sub_group_1
|
||||
sub_group_2.runners << runner_sub_group_2
|
||||
sub_group_3.runners << runner_sub_group_3
|
||||
sub_group_4.runners << runner_sub_group_4
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject { described_class.new(current_user: user, group: group, params: params).execute }
|
||||
|
||||
context 'no params' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'returns all runners' do
|
||||
expect(subject).to eq([runner_project_7, runner_project_6, runner_project_5,
|
||||
runner_project_4, runner_project_3, runner_project_2,
|
||||
runner_project_1, runner_sub_group_4, runner_sub_group_3,
|
||||
runner_sub_group_2, runner_sub_group_1, runner_group])
|
||||
end
|
||||
end
|
||||
|
||||
context 'with sort param' do
|
||||
let(:params) { { sort: 'contacted_asc' } }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'sorts by specified attribute' do
|
||||
expect(subject).to eq([runner_group, runner_sub_group_1, runner_sub_group_2,
|
||||
runner_sub_group_3, runner_sub_group_4, runner_project_1,
|
||||
runner_project_2, runner_project_3, runner_project_4,
|
||||
runner_project_5, runner_project_6, runner_project_7])
|
||||
end
|
||||
end
|
||||
|
||||
context 'paginate' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:runners) do
|
||||
[[runner_project_7, runner_project_6, runner_project_5],
|
||||
[runner_project_4, runner_project_3, runner_project_2],
|
||||
[runner_project_1, runner_sub_group_4, runner_sub_group_3],
|
||||
[runner_sub_group_2, runner_sub_group_1, runner_group]]
|
||||
end
|
||||
|
||||
where(:page, :index) do
|
||||
1 | 0
|
||||
2 | 1
|
||||
3 | 2
|
||||
4 | 3
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const('Ci::RunnersFinder::NUMBER_OF_RUNNERS_PER_PAGE', 3)
|
||||
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
with_them do
|
||||
let(:params) { { page: page } }
|
||||
|
||||
it 'returns the runners for the specified page' do
|
||||
expect(subject).to eq(runners[index])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by search term' do
|
||||
let(:params) { { search: 'runner_project_search' } }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'returns correct runner' do
|
||||
expect(subject).to eq([runner_project_3])
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by status' do
|
||||
let(:params) { { status_status: 'paused' } }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'returns correct runner' do
|
||||
expect(subject).to eq([runner_sub_group_1])
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by tag_name' do
|
||||
let(:params) { { tag_name: %w[runner_tag] } }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'returns correct runner' do
|
||||
expect(subject).to eq([runner_project_5])
|
||||
end
|
||||
end
|
||||
|
||||
context 'filter by runner type' do
|
||||
let(:params) { { type_type: 'project_type' } }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'returns correct runners' do
|
||||
expect(subject).to eq([runner_project_7, runner_project_6,
|
||||
runner_project_5, runner_project_4,
|
||||
runner_project_3, runner_project_2, runner_project_1])
|
||||
end
|
||||
end
|
||||
|
||||
context 'user has no access to runners' do
|
||||
where(:user_permission) do
|
||||
[:maintainer, :developer, :reporter, :guest]
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
create(:group_member, user_permission, group: group, user: user)
|
||||
end
|
||||
|
||||
it 'returns no runners' do
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'user with no access' do
|
||||
it 'returns no runners' do
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'user is nil' do
|
||||
let_it_be(:user) { nil }
|
||||
|
||||
it 'returns no runners' do
|
||||
expect(subject).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sort_key' do
|
||||
subject { described_class.new(current_user: user, group: group, params: params).sort_key }
|
||||
|
||||
context 'no params' do
|
||||
it 'returns created_date' do
|
||||
expect(subject).to eq('created_date')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with params' do
|
||||
let(:params) { { sort: 'contacted_asc' } }
|
||||
|
||||
it 'returns contacted_asc' do
|
||||
expect(subject).to eq('contacted_asc')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,55 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe UploaderFinder do
|
||||
describe '#execute' do
|
||||
let(:project) { build(:project) }
|
||||
let(:upload) { create(:upload, :issuable_upload, :with_file) }
|
||||
let(:secret) { upload.secret }
|
||||
let(:file_name) { upload.path }
|
||||
|
||||
subject { described_class.new(project, secret, file_name).execute }
|
||||
|
||||
before do
|
||||
upload.save
|
||||
end
|
||||
|
||||
context 'when successful' do
|
||||
before do
|
||||
allow_next_instance_of(FileUploader) do |uploader|
|
||||
allow(uploader).to receive(:retrieve_from_store!).with(upload.path).and_return(uploader)
|
||||
end
|
||||
end
|
||||
|
||||
it 'gets the file-like uploader' do
|
||||
expect(subject).to be_an_instance_of(FileUploader)
|
||||
expect(subject.model).to eq(project)
|
||||
expect(subject.secret).to eq(secret)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when path traversal in file name' do
|
||||
before do
|
||||
upload.path = '/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../../etc/passwd)'
|
||||
upload.save
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unexpected failure' do
|
||||
before do
|
||||
allow_next_instance_of(FileUploader) do |uploader|
|
||||
allow(uploader).to receive(:retrieve_from_store!).and_raise(StandardError)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns nil when unexpected error is raised' do
|
||||
expect { subject }.to raise_error(StandardError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,259 @@
|
|||
import InputSetter from '~/droplab/plugins/input_setter';
|
||||
|
||||
describe('InputSetter', () => {
|
||||
let testContext;
|
||||
|
||||
beforeEach(() => {
|
||||
testContext = {};
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
beforeEach(() => {
|
||||
testContext.config = { InputSetter: {} };
|
||||
testContext.hook = { config: testContext.config };
|
||||
testContext.inputSetter = {
|
||||
addEvents: jest.fn(),
|
||||
};
|
||||
|
||||
InputSetter.init.call(testContext.inputSetter, testContext.hook);
|
||||
});
|
||||
|
||||
it('should set .hook', () => {
|
||||
expect(testContext.inputSetter.hook).toBe(testContext.hook);
|
||||
});
|
||||
|
||||
it('should set .config', () => {
|
||||
expect(testContext.inputSetter.config).toBe(testContext.config.InputSetter);
|
||||
});
|
||||
|
||||
it('should set .eventWrapper', () => {
|
||||
expect(testContext.inputSetter.eventWrapper).toEqual({});
|
||||
});
|
||||
|
||||
it('should call .addEvents', () => {
|
||||
expect(testContext.inputSetter.addEvents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('if config.InputSetter is not set', () => {
|
||||
beforeEach(() => {
|
||||
testContext.config = { InputSetter: undefined };
|
||||
testContext.hook = { config: testContext.config };
|
||||
|
||||
InputSetter.init.call(testContext.inputSetter, testContext.hook);
|
||||
});
|
||||
|
||||
it('should set .config to an empty object', () => {
|
||||
expect(testContext.inputSetter.config).toEqual({});
|
||||
});
|
||||
|
||||
it('should set hook.config to an empty object', () => {
|
||||
expect(testContext.hook.config.InputSetter).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEvents', () => {
|
||||
beforeEach(() => {
|
||||
testContext.hook = {
|
||||
list: {
|
||||
list: {
|
||||
addEventListener: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
testContext.inputSetter = { eventWrapper: {}, hook: testContext.hook, setInputs: () => {} };
|
||||
|
||||
InputSetter.addEvents.call(testContext.inputSetter);
|
||||
});
|
||||
|
||||
it('should set .eventWrapper.setInputs', () => {
|
||||
expect(testContext.inputSetter.eventWrapper.setInputs).toEqual(expect.any(Function));
|
||||
});
|
||||
|
||||
it('should call .addEventListener', () => {
|
||||
expect(testContext.hook.list.list.addEventListener).toHaveBeenCalledWith(
|
||||
'click.dl',
|
||||
testContext.inputSetter.eventWrapper.setInputs,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeEvents', () => {
|
||||
beforeEach(() => {
|
||||
testContext.hook = {
|
||||
list: {
|
||||
list: {
|
||||
removeEventListener: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
testContext.eventWrapper = {
|
||||
setInputs: jest.fn(),
|
||||
};
|
||||
testContext.inputSetter = { eventWrapper: testContext.eventWrapper, hook: testContext.hook };
|
||||
|
||||
InputSetter.removeEvents.call(testContext.inputSetter);
|
||||
});
|
||||
|
||||
it('should call .removeEventListener', () => {
|
||||
expect(testContext.hook.list.list.removeEventListener).toHaveBeenCalledWith(
|
||||
'click.dl',
|
||||
testContext.eventWrapper.setInputs,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInputs', () => {
|
||||
beforeEach(() => {
|
||||
testContext.event = { detail: { selected: {} } };
|
||||
testContext.config = [0, 1];
|
||||
testContext.inputSetter = { config: testContext.config, setInput: () => {} };
|
||||
|
||||
jest.spyOn(testContext.inputSetter, 'setInput').mockImplementation(() => {});
|
||||
|
||||
InputSetter.setInputs.call(testContext.inputSetter, testContext.event);
|
||||
});
|
||||
|
||||
it('should call .setInput for each config element', () => {
|
||||
const allArgs = testContext.inputSetter.setInput.mock.calls;
|
||||
|
||||
expect(allArgs.length).toEqual(2);
|
||||
|
||||
allArgs.forEach((args, i) => {
|
||||
expect(args[0]).toBe(testContext.config[i]);
|
||||
expect(args[1]).toBe(testContext.event.detail.selected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if config isnt an array', () => {
|
||||
beforeEach(() => {
|
||||
testContext.inputSetter = { config: {}, setInput: () => {} };
|
||||
|
||||
InputSetter.setInputs.call(testContext.inputSetter, testContext.event);
|
||||
});
|
||||
|
||||
it('should set .config to an array with .config as the first element', () => {
|
||||
expect(testContext.inputSetter.config).toEqual([{}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInput', () => {
|
||||
beforeEach(() => {
|
||||
testContext.selectedItem = { getAttribute: () => {} };
|
||||
testContext.input = { value: 'oldValue', tagName: 'INPUT', hasAttribute: () => {} };
|
||||
testContext.config = { valueAttribute: {}, input: testContext.input };
|
||||
testContext.inputSetter = { hook: { trigger: {} } };
|
||||
testContext.newValue = 'newValue';
|
||||
|
||||
jest.spyOn(testContext.selectedItem, 'getAttribute').mockReturnValue(testContext.newValue);
|
||||
jest.spyOn(testContext.input, 'hasAttribute').mockReturnValue(false);
|
||||
|
||||
InputSetter.setInput.call(
|
||||
testContext.inputSetter,
|
||||
testContext.config,
|
||||
testContext.selectedItem,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call .getAttribute', () => {
|
||||
expect(testContext.selectedItem.getAttribute).toHaveBeenCalledWith(
|
||||
testContext.config.valueAttribute,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call .hasAttribute', () => {
|
||||
expect(testContext.input.hasAttribute).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should set the value of the input', () => {
|
||||
expect(testContext.input.value).toBe(testContext.newValue);
|
||||
});
|
||||
|
||||
describe('if no config.input is provided', () => {
|
||||
beforeEach(() => {
|
||||
testContext.config = { valueAttribute: {} };
|
||||
testContext.trigger = { value: 'oldValue', tagName: 'INPUT', hasAttribute: () => {} };
|
||||
testContext.inputSetter = { hook: { trigger: testContext.trigger } };
|
||||
|
||||
InputSetter.setInput.call(
|
||||
testContext.inputSetter,
|
||||
testContext.config,
|
||||
testContext.selectedItem,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set the value of the hook.trigger', () => {
|
||||
expect(testContext.trigger.value).toBe(testContext.newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the input tag is not INPUT', () => {
|
||||
beforeEach(() => {
|
||||
testContext.input = { textContent: 'oldValue', tagName: 'SPAN', hasAttribute: () => {} };
|
||||
testContext.config = { valueAttribute: {}, input: testContext.input };
|
||||
|
||||
InputSetter.setInput.call(
|
||||
testContext.inputSetter,
|
||||
testContext.config,
|
||||
testContext.selectedItem,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set the textContent of the input', () => {
|
||||
expect(testContext.input.textContent).toBe(testContext.newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if there is an inputAttribute', () => {
|
||||
beforeEach(() => {
|
||||
testContext.selectedItem = { getAttribute: () => {} };
|
||||
testContext.input = { id: 'oldValue', hasAttribute: () => {}, setAttribute: () => {} };
|
||||
testContext.inputSetter = { hook: { trigger: {} } };
|
||||
testContext.newValue = 'newValue';
|
||||
testContext.inputAttribute = 'id';
|
||||
testContext.config = {
|
||||
valueAttribute: {},
|
||||
input: testContext.input,
|
||||
inputAttribute: testContext.inputAttribute,
|
||||
};
|
||||
|
||||
jest.spyOn(testContext.selectedItem, 'getAttribute').mockReturnValue(testContext.newValue);
|
||||
jest.spyOn(testContext.input, 'hasAttribute').mockReturnValue(true);
|
||||
jest.spyOn(testContext.input, 'setAttribute').mockImplementation(() => {});
|
||||
|
||||
InputSetter.setInput.call(
|
||||
testContext.inputSetter,
|
||||
testContext.config,
|
||||
testContext.selectedItem,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call setAttribute', () => {
|
||||
expect(testContext.input.setAttribute).toHaveBeenCalledWith(
|
||||
testContext.inputAttribute,
|
||||
testContext.newValue,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not set the value or textContent of the input', () => {
|
||||
expect(testContext.input.value).not.toBe('newValue');
|
||||
expect(testContext.input.textContent).not.toBe('newValue');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
beforeEach(() => {
|
||||
testContext.inputSetter = {
|
||||
removeEvents: jest.fn(),
|
||||
};
|
||||
|
||||
InputSetter.destroy.call(testContext.inputSetter);
|
||||
});
|
||||
|
||||
it('should call .removeEvents', () => {
|
||||
expect(testContext.inputSetter.removeEvents).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -283,25 +283,13 @@ describe('RepoEditor', () => {
|
|||
expect(vm.model.events.size).toBe(2);
|
||||
});
|
||||
|
||||
it.each`
|
||||
insertFinalNewline | input | eol | output
|
||||
${true} | ${'testing 123\n'} | ${'\n'} | ${'testing 123\n'}
|
||||
${true} | ${'testing 123'} | ${'\n'} | ${'testing 123\n'}
|
||||
${false} | ${'testing 123'} | ${'\n'} | ${'testing 123'}
|
||||
${true} | ${'testing 123'} | ${'\r\n'} | ${'testing 123\r\n'}
|
||||
${false} | ${'testing 123'} | ${'\r\n'} | ${'testing 123'}
|
||||
`(
|
||||
'updates state with "$output" if `this.insertFinalNewline` is $insertFinalNewline',
|
||||
({ insertFinalNewline, input, eol, output }) => {
|
||||
jest.spyOn(vm.model.getModel(), 'getEOL').mockReturnValue(eol);
|
||||
it('updates state with the value of the model', () => {
|
||||
vm.model.setValue('testing 1234');
|
||||
|
||||
vm.addFinalNewline = insertFinalNewline;
|
||||
vm.setupEditor();
|
||||
|
||||
vm.model.setValue(input);
|
||||
|
||||
expect(vm.file.content).toBe(output);
|
||||
},
|
||||
);
|
||||
expect(vm.file.content).toBe('testing 1234');
|
||||
});
|
||||
|
||||
it('sets head model as staged file', () => {
|
||||
jest.spyOn(vm.editor, 'createModel');
|
||||
|
|
|
@ -133,5 +133,77 @@ describe('Multi-file editor library model', () => {
|
|||
|
||||
expect(disposeSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies custom options and triggers onChange callback', () => {
|
||||
const changeSpy = jest.fn();
|
||||
jest.spyOn(model, 'applyCustomOptions');
|
||||
|
||||
model.onChange(changeSpy);
|
||||
|
||||
model.dispose();
|
||||
|
||||
expect(model.applyCustomOptions).toHaveBeenCalled();
|
||||
expect(changeSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateOptions', () => {
|
||||
it('sets the options on the options object', () => {
|
||||
model.updateOptions({ insertSpaces: true, someOption: 'some value' });
|
||||
|
||||
expect(model.options).toEqual({
|
||||
endOfLine: 0,
|
||||
insertFinalNewline: true,
|
||||
insertSpaces: true,
|
||||
someOption: 'some value',
|
||||
trimTrailingWhitespace: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
option | value
|
||||
${'insertSpaces'} | ${true}
|
||||
${'insertSpaces'} | ${false}
|
||||
${'indentSize'} | ${4}
|
||||
${'tabSize'} | ${3}
|
||||
`("correctly sets option: $option=$value to Monaco's TextModel", ({ option, value }) => {
|
||||
model.updateOptions({ [option]: value });
|
||||
|
||||
expect(model.getModel().getOptions()).toMatchObject({ [option]: value });
|
||||
});
|
||||
|
||||
it('applies custom options immediately', () => {
|
||||
jest.spyOn(model, 'applyCustomOptions');
|
||||
|
||||
model.updateOptions({ trimTrailingWhitespace: true, someOption: 'some value' });
|
||||
|
||||
expect(model.applyCustomOptions).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyCustomOptions', () => {
|
||||
it.each`
|
||||
option | value | contentBefore | contentAfter
|
||||
${'endOfLine'} | ${0} | ${'hello\nworld\n'} | ${'hello\nworld\n'}
|
||||
${'endOfLine'} | ${0} | ${'hello\r\nworld\r\n'} | ${'hello\nworld\n'}
|
||||
${'endOfLine'} | ${1} | ${'hello\nworld\n'} | ${'hello\r\nworld\r\n'}
|
||||
${'endOfLine'} | ${1} | ${'hello\r\nworld\r\n'} | ${'hello\r\nworld\r\n'}
|
||||
${'insertFinalNewline'} | ${true} | ${'hello\nworld'} | ${'hello\nworld\n'}
|
||||
${'insertFinalNewline'} | ${true} | ${'hello\nworld\n'} | ${'hello\nworld\n'}
|
||||
${'insertFinalNewline'} | ${false} | ${'hello\nworld'} | ${'hello\nworld'}
|
||||
${'trimTrailingWhitespace'} | ${true} | ${'hello \t\nworld \t\n'} | ${'hello\nworld\n'}
|
||||
${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\nworld\n'}
|
||||
${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\nworld \t\n'}
|
||||
`(
|
||||
'correctly applies custom option $option=$value to content',
|
||||
({ option, value, contentBefore, contentAfter }) => {
|
||||
model.options[option] = value;
|
||||
|
||||
model.updateNewContent(contentBefore);
|
||||
model.applyCustomOptions();
|
||||
|
||||
expect(model.getModel().getValue()).toEqual(contentAfter);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -73,5 +73,13 @@ describe('Multi-file editor library diff calculator', () => {
|
|||
|
||||
expect(diff.endLineNumber).toBe(1);
|
||||
});
|
||||
|
||||
it('disregards changes for EOL type changes', () => {
|
||||
const text1 = 'line1\nline2\nline3\n';
|
||||
const text2 = 'line1\r\nline2\r\nline3\r\n';
|
||||
|
||||
expect(computeDiff(text1, text2)).toEqual([]);
|
||||
expect(computeDiff(text2, text1)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import editorOptions from '~/ide/lib/editor_options';
|
||||
|
||||
describe('Multi-file editor library editor options', () => {
|
||||
it('returns an array', () => {
|
||||
expect(editorOptions).toEqual(expect.any(Array));
|
||||
});
|
||||
|
||||
it('contains readOnly option', () => {
|
||||
expect(editorOptions[0].readOnly).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -72,6 +72,7 @@ describe('Multi-file editor library', () => {
|
|||
|
||||
expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, {
|
||||
...defaultEditorOptions,
|
||||
ignoreTrimWhitespace: false,
|
||||
quickSuggestions: false,
|
||||
occurrencesHighlight: false,
|
||||
renderSideBySide: false,
|
||||
|
|
|
@ -2,7 +2,8 @@ import {
|
|||
isTextFile,
|
||||
registerLanguages,
|
||||
trimPathComponents,
|
||||
addFinalNewline,
|
||||
insertFinalNewline,
|
||||
trimTrailingWhitespace,
|
||||
getPathParents,
|
||||
} from '~/ide/utils';
|
||||
import { languages } from 'monaco-editor';
|
||||
|
@ -155,6 +156,20 @@ describe('WebIDE utils', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('trimTrailingWhitespace', () => {
|
||||
it.each`
|
||||
input | output
|
||||
${'text \n more text \n'} | ${'text\n more text\n'}
|
||||
${'text \n more text \n\n \n'} | ${'text\n more text\n\n\n'}
|
||||
${'text \t\t \n more text \n\t\ttext\n \n\t\t'} | ${'text\n more text\n\t\ttext\n\n'}
|
||||
${'text \r\n more text \r\n'} | ${'text\r\n more text\r\n'}
|
||||
${'text \r\n more text \r\n\r\n \r\n'} | ${'text\r\n more text\r\n\r\n\r\n'}
|
||||
${'text \t\t \r\n more text \r\n\t\ttext\r\n \r\n\t\t'} | ${'text\r\n more text\r\n\t\ttext\r\n\r\n'}
|
||||
`("trims trailing whitespace in each line of file's contents: $input", ({ input, output }) => {
|
||||
expect(trimTrailingWhitespace(input)).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addFinalNewline', () => {
|
||||
it.each`
|
||||
input | output
|
||||
|
@ -163,7 +178,7 @@ describe('WebIDE utils', () => {
|
|||
${'some text\n\n'} | ${'some text\n\n'}
|
||||
${'some\n text'} | ${'some\n text\n'}
|
||||
`('adds a newline if it doesnt already exist for input: $input', ({ input, output }) => {
|
||||
expect(addFinalNewline(input)).toEqual(output);
|
||||
expect(insertFinalNewline(input)).toBe(output);
|
||||
});
|
||||
|
||||
it.each`
|
||||
|
@ -174,7 +189,7 @@ describe('WebIDE utils', () => {
|
|||
${'some text\r\n\r\n'} | ${'some text\r\n\r\n'}
|
||||
${'some\r\n text'} | ${'some\r\n text\r\n'}
|
||||
`('works with CRLF newline style; input: $input', ({ input, output }) => {
|
||||
expect(addFinalNewline(input, '\r\n')).toEqual(output);
|
||||
expect(insertFinalNewline(input, '\r\n')).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,214 +0,0 @@
|
|||
import InputSetter from '~/droplab/plugins/input_setter';
|
||||
|
||||
describe('InputSetter', function() {
|
||||
describe('init', function() {
|
||||
beforeEach(function() {
|
||||
this.config = { InputSetter: {} };
|
||||
this.hook = { config: this.config };
|
||||
this.inputSetter = jasmine.createSpyObj('inputSetter', ['addEvents']);
|
||||
|
||||
InputSetter.init.call(this.inputSetter, this.hook);
|
||||
});
|
||||
|
||||
it('should set .hook', function() {
|
||||
expect(this.inputSetter.hook).toBe(this.hook);
|
||||
});
|
||||
|
||||
it('should set .config', function() {
|
||||
expect(this.inputSetter.config).toBe(this.config.InputSetter);
|
||||
});
|
||||
|
||||
it('should set .eventWrapper', function() {
|
||||
expect(this.inputSetter.eventWrapper).toEqual({});
|
||||
});
|
||||
|
||||
it('should call .addEvents', function() {
|
||||
expect(this.inputSetter.addEvents).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('if config.InputSetter is not set', function() {
|
||||
beforeEach(function() {
|
||||
this.config = { InputSetter: undefined };
|
||||
this.hook = { config: this.config };
|
||||
|
||||
InputSetter.init.call(this.inputSetter, this.hook);
|
||||
});
|
||||
|
||||
it('should set .config to an empty object', function() {
|
||||
expect(this.inputSetter.config).toEqual({});
|
||||
});
|
||||
|
||||
it('should set hook.config to an empty object', function() {
|
||||
expect(this.hook.config.InputSetter).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('addEvents', function() {
|
||||
beforeEach(function() {
|
||||
this.hook = { list: { list: jasmine.createSpyObj('list', ['addEventListener']) } };
|
||||
this.inputSetter = { eventWrapper: {}, hook: this.hook, setInputs: () => {} };
|
||||
|
||||
InputSetter.addEvents.call(this.inputSetter);
|
||||
});
|
||||
|
||||
it('should set .eventWrapper.setInputs', function() {
|
||||
expect(this.inputSetter.eventWrapper.setInputs).toEqual(jasmine.any(Function));
|
||||
});
|
||||
|
||||
it('should call .addEventListener', function() {
|
||||
expect(this.hook.list.list.addEventListener).toHaveBeenCalledWith(
|
||||
'click.dl',
|
||||
this.inputSetter.eventWrapper.setInputs,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeEvents', function() {
|
||||
beforeEach(function() {
|
||||
this.hook = { list: { list: jasmine.createSpyObj('list', ['removeEventListener']) } };
|
||||
this.eventWrapper = jasmine.createSpyObj('eventWrapper', ['setInputs']);
|
||||
this.inputSetter = { eventWrapper: this.eventWrapper, hook: this.hook };
|
||||
|
||||
InputSetter.removeEvents.call(this.inputSetter);
|
||||
});
|
||||
|
||||
it('should call .removeEventListener', function() {
|
||||
expect(this.hook.list.list.removeEventListener).toHaveBeenCalledWith(
|
||||
'click.dl',
|
||||
this.eventWrapper.setInputs,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInputs', function() {
|
||||
beforeEach(function() {
|
||||
this.event = { detail: { selected: {} } };
|
||||
this.config = [0, 1];
|
||||
this.inputSetter = { config: this.config, setInput: () => {} };
|
||||
|
||||
spyOn(this.inputSetter, 'setInput');
|
||||
|
||||
InputSetter.setInputs.call(this.inputSetter, this.event);
|
||||
});
|
||||
|
||||
it('should call .setInput for each config element', function() {
|
||||
const allArgs = this.inputSetter.setInput.calls.allArgs();
|
||||
|
||||
expect(allArgs.length).toEqual(2);
|
||||
|
||||
allArgs.forEach((args, i) => {
|
||||
expect(args[0]).toBe(this.config[i]);
|
||||
expect(args[1]).toBe(this.event.detail.selected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if config isnt an array', function() {
|
||||
beforeEach(function() {
|
||||
this.inputSetter = { config: {}, setInput: () => {} };
|
||||
|
||||
InputSetter.setInputs.call(this.inputSetter, this.event);
|
||||
});
|
||||
|
||||
it('should set .config to an array with .config as the first element', function() {
|
||||
expect(this.inputSetter.config).toEqual([{}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInput', function() {
|
||||
beforeEach(function() {
|
||||
this.selectedItem = { getAttribute: () => {} };
|
||||
this.input = { value: 'oldValue', tagName: 'INPUT', hasAttribute: () => {} };
|
||||
this.config = { valueAttribute: {}, input: this.input };
|
||||
this.inputSetter = { hook: { trigger: {} } };
|
||||
this.newValue = 'newValue';
|
||||
|
||||
spyOn(this.selectedItem, 'getAttribute').and.returnValue(this.newValue);
|
||||
spyOn(this.input, 'hasAttribute').and.returnValue(false);
|
||||
|
||||
InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem);
|
||||
});
|
||||
|
||||
it('should call .getAttribute', function() {
|
||||
expect(this.selectedItem.getAttribute).toHaveBeenCalledWith(this.config.valueAttribute);
|
||||
});
|
||||
|
||||
it('should call .hasAttribute', function() {
|
||||
expect(this.input.hasAttribute).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('should set the value of the input', function() {
|
||||
expect(this.input.value).toBe(this.newValue);
|
||||
});
|
||||
|
||||
describe('if no config.input is provided', function() {
|
||||
beforeEach(function() {
|
||||
this.config = { valueAttribute: {} };
|
||||
this.trigger = { value: 'oldValue', tagName: 'INPUT', hasAttribute: () => {} };
|
||||
this.inputSetter = { hook: { trigger: this.trigger } };
|
||||
|
||||
InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem);
|
||||
});
|
||||
|
||||
it('should set the value of the hook.trigger', function() {
|
||||
expect(this.trigger.value).toBe(this.newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if the input tag is not INPUT', function() {
|
||||
beforeEach(function() {
|
||||
this.input = { textContent: 'oldValue', tagName: 'SPAN', hasAttribute: () => {} };
|
||||
this.config = { valueAttribute: {}, input: this.input };
|
||||
|
||||
InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem);
|
||||
});
|
||||
|
||||
it('should set the textContent of the input', function() {
|
||||
expect(this.input.textContent).toBe(this.newValue);
|
||||
});
|
||||
});
|
||||
|
||||
describe('if there is an inputAttribute', function() {
|
||||
beforeEach(function() {
|
||||
this.selectedItem = { getAttribute: () => {} };
|
||||
this.input = { id: 'oldValue', hasAttribute: () => {}, setAttribute: () => {} };
|
||||
this.inputSetter = { hook: { trigger: {} } };
|
||||
this.newValue = 'newValue';
|
||||
this.inputAttribute = 'id';
|
||||
this.config = {
|
||||
valueAttribute: {},
|
||||
input: this.input,
|
||||
inputAttribute: this.inputAttribute,
|
||||
};
|
||||
|
||||
spyOn(this.selectedItem, 'getAttribute').and.returnValue(this.newValue);
|
||||
spyOn(this.input, 'hasAttribute').and.returnValue(true);
|
||||
spyOn(this.input, 'setAttribute');
|
||||
|
||||
InputSetter.setInput.call(this.inputSetter, this.config, this.selectedItem);
|
||||
});
|
||||
|
||||
it('should call setAttribute', function() {
|
||||
expect(this.input.setAttribute).toHaveBeenCalledWith(this.inputAttribute, this.newValue);
|
||||
});
|
||||
|
||||
it('should not set the value or textContent of the input', function() {
|
||||
expect(this.input.value).not.toBe('newValue');
|
||||
expect(this.input.textContent).not.toBe('newValue');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('destroy', function() {
|
||||
beforeEach(function() {
|
||||
this.inputSetter = jasmine.createSpyObj('inputSetter', ['removeEvents']);
|
||||
|
||||
InputSetter.destroy.call(this.inputSetter);
|
||||
});
|
||||
|
||||
it('should call .removeEvents', function() {
|
||||
expect(this.inputSetter.removeEvents).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -54,6 +54,14 @@ describe Gitlab::Gfm::UploadsRewriter do
|
|||
expect(new_paths).not_to include image_uploader.secret
|
||||
expect(new_paths).not_to include zip_uploader.secret
|
||||
end
|
||||
|
||||
it 'skips nil files do' do
|
||||
allow_next_instance_of(UploaderFinder) do |finder|
|
||||
allow(finder).to receive(:execute).and_return(nil)
|
||||
end
|
||||
|
||||
expect(new_files).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -68,16 +76,6 @@ describe Gitlab::Gfm::UploadsRewriter do
|
|||
expect(moved_text.scan(/\A\[.*?\]/).count).to eq(1)
|
||||
end
|
||||
|
||||
context 'path traversal in file name' do
|
||||
let(:text) do
|
||||
"![a](/uploads/11111111111111111111111111111111/../../../../../../../../../../../../../../etc/passwd)"
|
||||
end
|
||||
|
||||
it 'throw an error' do
|
||||
expect { rewriter.rewrite(new_project) }.to raise_error(an_instance_of(StandardError).and(having_attributes(message: "Invalid path")))
|
||||
end
|
||||
end
|
||||
|
||||
context "file are stored locally" do
|
||||
include_examples "files are accessible"
|
||||
end
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::ProcessMemoryCache::Helper, :use_clean_rails_memory_store_caching do
|
||||
let(:minimal_test_class) do
|
||||
Class.new do
|
||||
include Gitlab::ProcessMemoryCache::Helper
|
||||
|
||||
def cached_content
|
||||
fetch_memory_cache(:cached_content_instance_key) { expensive_computation }
|
||||
end
|
||||
|
||||
def clear_cached_content
|
||||
invalidate_memory_cache(:cached_content_instance_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
stub_const("MinimalTestClass", minimal_test_class)
|
||||
end
|
||||
|
||||
subject { MinimalTestClass.new }
|
||||
|
||||
describe '.fetch_memory_cache' do
|
||||
it 'memoizes the result' do
|
||||
is_expected.to receive(:expensive_computation).once.and_return(1)
|
||||
|
||||
2.times do
|
||||
expect(subject.cached_content).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'resets the cache when the shared key is missing', :aggregate_failures do
|
||||
expect(Rails.cache).to receive(:read).with(:cached_content_instance_key).twice.and_return(nil)
|
||||
is_expected.to receive(:expensive_computation).thrice.and_return(1, 2, 3)
|
||||
|
||||
3.times do |index|
|
||||
expect(subject.cached_content).to eq(index + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.invalidate_memory_cache' do
|
||||
it 'invalidates the cache' do
|
||||
is_expected.to receive(:expensive_computation).twice.and_return(1, 2)
|
||||
|
||||
expect { subject.clear_cached_content }.to change { subject.cached_content }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -39,7 +39,7 @@ describe Ci::InstanceVariable do
|
|||
it { expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable) }
|
||||
|
||||
it 'memoizes the result' do
|
||||
expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original
|
||||
expect(described_class).to receive(:unscoped).once.and_call_original
|
||||
|
||||
2.times do
|
||||
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
|
||||
|
@ -65,15 +65,6 @@ describe Ci::InstanceVariable do
|
|||
|
||||
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable, variable)
|
||||
end
|
||||
|
||||
it 'resets the cache when the shared key is missing' do
|
||||
expect(Rails.cache).to receive(:read).with(:ci_instance_variable_changed_at).twice.and_return(nil)
|
||||
expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).thrice.and_call_original
|
||||
|
||||
3.times do
|
||||
expect(described_class.all_cached).to contain_exactly(protected_variable, unprotected_variable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.unprotected_cached', :use_clean_rails_memory_store_caching do
|
||||
|
@ -83,7 +74,7 @@ describe Ci::InstanceVariable do
|
|||
it { expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable) }
|
||||
|
||||
it 'memoizes the result' do
|
||||
expect(described_class).to receive(:store_cache).with(:ci_instance_variable_data).once.and_call_original
|
||||
expect(described_class).to receive(:unscoped).once.and_call_original
|
||||
|
||||
2.times do
|
||||
expect(described_class.unprotected_cached).to contain_exactly(unprotected_variable)
|
||||
|
|
Loading…
Reference in New Issue