Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-25 12:08:23 +00:00
parent 03c3f9f501
commit b5249f2d99
41 changed files with 1257 additions and 492 deletions

View File

@ -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?

View File

@ -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

View File

@ -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();
}
}

View File

@ -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(),
});
}

View File

@ -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) => {

View File

@ -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',

View File

@ -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'),

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.')

View File

@ -0,0 +1,5 @@
---
title: Add finder for group-level runners
merge_request: 29283
author: Arthur de Lapertosa Lisboa
type: added

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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();
});
});
});

View File

@ -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');

View File

@ -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);
},
);
});
});

View File

@ -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([]);
});
});
});

View File

@ -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();
});
});

View File

@ -72,6 +72,7 @@ describe('Multi-file editor library', () => {
expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, {
...defaultEditorOptions,
ignoreTrimWhitespace: false,
quickSuggestions: false,
occurrencesHighlight: false,
renderSideBySide: false,

View File

@ -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);
});
});

View File

@ -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();
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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)