Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-02-10 09:08:56 +00:00
parent 2aaef94c80
commit b4ded0ba7b
43 changed files with 636 additions and 281 deletions

View File

@ -75,16 +75,18 @@
changes: *code-backstage-qa-patterns
when: on_success
.rails:rules:master-refs:
.rails:rules:master-refs-code-backstage-qa:
rules:
- <<: *if-master-refs
changes: *code-backstage-qa-patterns
when: on_success
.rails:rules:master-refs-ee-only:
.rails:rules:master-refs-code-backstage-qa-ee-only:
rules:
- <<: *if-not-ee
when: never
- <<: *if-master-refs
changes: *code-backstage-qa-patterns
when: on_success
.rails:rules:ee-only:
@ -330,12 +332,12 @@ coverage:
rspec quarantine pg9:
extends:
- .rspec-base-quarantine
- .rails:rules:master-refs
- .rails:rules:master-refs-code-backstage-qa
.rspec-base-pg10:
extends:
- .rspec-base
- .rails:rules:master-refs
- .rails:rules:master-refs-code-backstage-qa
- .use-pg10
rspec unit pg10:
@ -357,7 +359,7 @@ rspec system pg10:
rspec-ee quarantine pg9:
extends:
- .rspec-base-quarantine
- .rails:rules:master-refs-ee-only
- .rails:rules:master-refs-code-backstage-qa-ee-only
variables:
RSPEC_OPTS: "--tag quarantine -- ee/spec/"
@ -365,25 +367,25 @@ rspec-ee migration pg10:
extends:
- .rspec-ee-base-pg10
- .rspec-base-migration
- .rails:rules:master-refs
- .rails:rules:master-refs-code-backstage-qa
parallel: 2
rspec-ee unit pg10:
extends:
- .rspec-ee-base-pg10
- .rails:rules:master-refs
- .rails:rules:master-refs-code-backstage-qa
parallel: 10
rspec-ee integration pg10:
extends:
- .rspec-ee-base-pg10
- .rails:rules:master-refs
- .rails:rules:master-refs-code-backstage-qa
parallel: 3
rspec-ee system pg10:
extends:
- .rspec-ee-base-pg10
- .rails:rules:master-refs
- .rails:rules:master-refs-code-backstage-qa
parallel: 5
# ee + master-only jobs #
#########################

View File

@ -1 +1 @@
1.85.0
1.86.0

View File

@ -0,0 +1,75 @@
<script>
import { GlButton, GlButtonGroup, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import {
RICH_BLOB_VIEWER,
RICH_BLOB_VIEWER_TITLE,
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
} from './constants';
export default {
components: {
GlIcon,
GlButtonGroup,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
blob: {
type: Object,
required: true,
},
},
data() {
return {
viewer: this.blob.richViewer ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
};
},
computed: {
isSimpleViewer() {
return this.viewer === SIMPLE_BLOB_VIEWER;
},
isRichViewer() {
return this.viewer === RICH_BLOB_VIEWER;
},
},
methods: {
switchToViewer(viewer) {
if (viewer !== this.viewer) {
this.viewer = viewer;
this.$emit('switch-viewer', viewer);
}
},
},
SIMPLE_BLOB_VIEWER,
RICH_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
RICH_BLOB_VIEWER_TITLE,
};
</script>
<template>
<gl-button-group class="js-blob-viewer-switcher ml-2">
<gl-button
v-gl-tooltip.hover
:aria-label="$options.SIMPLE_BLOB_VIEWER_TITLE"
:title="$options.SIMPLE_BLOB_VIEWER_TITLE"
:selected="isSimpleViewer"
:class="{ active: isSimpleViewer }"
@click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
>
<gl-icon name="code" :size="14" />
</gl-button>
<gl-button
v-gl-tooltip.hover
:aria-label="$options.RICH_BLOB_VIEWER_TITLE"
:title="$options.RICH_BLOB_VIEWER_TITLE"
:selected="isRichViewer"
:class="{ active: isRichViewer }"
@click="switchToViewer($options.RICH_BLOB_VIEWER)"
>
<gl-icon name="document" :size="14" />
</gl-button>
</gl-button-group>
</template>

View File

@ -3,3 +3,9 @@ import { __ } from '~/locale';
export const BTN_COPY_CONTENTS_TITLE = __('Copy file contents');
export const BTN_RAW_TITLE = __('Open raw');
export const BTN_DOWNLOAD_TITLE = __('Download');
export const SIMPLE_BLOB_VIEWER = 'simple';
export const SIMPLE_BLOB_VIEWER_TITLE = __('Display source');
export const RICH_BLOB_VIEWER = 'rich';
export const RICH_BLOB_VIEWER_TITLE = __('Display rendered file');

View File

@ -161,50 +161,7 @@ class List {
}
addMultipleIssues(issues, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
const listHasIssues = issues.every(issue => this.findIssue(issue.id));
if (!listHasIssues) {
if (newIndex !== undefined) {
if (this.issues[newIndex - 1]) {
moveBeforeId = this.issues[newIndex - 1].id;
}
if (this.issues[newIndex]) {
moveAfterId = this.issues[newIndex].id;
}
this.issues.splice(newIndex, 0, ...issues);
} else {
this.issues.push(...issues);
}
if (this.label) {
issues.forEach(issue => issue.addLabel(this.label));
}
if (this.assignee) {
if (listFrom && listFrom.type === 'assignee') {
issues.forEach(issue => issue.removeAssignee(listFrom.assignee));
}
issues.forEach(issue => issue.addAssignee(this.assignee));
}
if (IS_EE && this.milestone) {
if (listFrom && listFrom.type === 'milestone') {
issues.forEach(issue => issue.removeMilestone(listFrom.milestone));
}
issues.forEach(issue => issue.addMilestone(this.milestone));
}
if (listFrom) {
this.issuesSize += issues.length;
this.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
}
}
boardsStore.addMultipleListIssues(this, issues, listFrom, newIndex);
}
addIssue(issue, listFrom, newIndex) {

View File

@ -131,6 +131,53 @@ const boardsStore = {
listFrom.update();
},
addMultipleListIssues(list, issues, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
const listHasIssues = issues.every(issue => list.findIssue(issue.id));
if (!listHasIssues) {
if (newIndex !== undefined) {
if (list.issues[newIndex - 1]) {
moveBeforeId = list.issues[newIndex - 1].id;
}
if (list.issues[newIndex]) {
moveAfterId = list.issues[newIndex].id;
}
list.issues.splice(newIndex, 0, ...issues);
} else {
list.issues.push(...issues);
}
if (list.label) {
issues.forEach(issue => issue.addLabel(list.label));
}
if (list.assignee) {
if (listFrom && listFrom.type === 'assignee') {
issues.forEach(issue => issue.removeAssignee(listFrom.assignee));
}
issues.forEach(issue => issue.addAssignee(list.assignee));
}
if (IS_EE && list.milestone) {
if (listFrom && listFrom.type === 'milestone') {
issues.forEach(issue => issue.removeMilestone(listFrom.milestone));
}
issues.forEach(issue => issue.addMilestone(list.milestone));
}
if (listFrom) {
list.issuesSize += issues.length;
list.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
}
}
},
startMoving(list, issue) {
Object.assign(this.moving, { list, issue });
},

View File

@ -1,5 +1,3 @@
import 'core-js/stable';
// Browser polyfills
import 'formdata-polyfill';
import './polyfills/custom_event';

View File

@ -168,6 +168,7 @@ export default {
'setIndexPath',
'fetchPaginatedResults',
'updateStatus',
'removeIgnoredResolvedErrors',
]),
setSearchText(text) {
this.errorSearchQuery = text;
@ -196,9 +197,9 @@ export default {
updateIssueStatus(errorId, status) {
this.updateStatus({
endpoint: this.getIssueUpdatePath(errorId),
redirectUrl: this.listPath,
status,
});
this.removeIgnoredResolvedErrors(errorId);
},
},
};
@ -235,7 +236,6 @@ export default {
</gl-dropdown>
<div class="filtered-search-input-container flex-fill">
<gl-form-input
v-model="errorSearchQuery"
class="pl-2 filtered-search"
:disabled="loading"
:placeholder="__('Search or filter results…')"

View File

@ -100,4 +100,8 @@ export const fetchPaginatedResults = ({ commit, dispatch }, cursor) => {
dispatch('startPolling');
};
export const removeIgnoredResolvedErrors = ({ commit }, error) => {
commit(types.REMOVE_IGNORED_RESOLVED_ERRORS, error);
};
export default () => {};

View File

@ -9,3 +9,4 @@ export const SET_ENDPOINT = 'SET_ENDPOINT';
export const SET_SORT_FIELD = 'SET_SORT_FIELD';
export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
export const SET_CURSOR = 'SET_CURSOR';
export const REMOVE_IGNORED_RESOLVED_ERRORS = 'REMOVE_IGNORED_RESOLVED_ERRORS';

View File

@ -59,4 +59,7 @@ export default {
[types.SET_ENDPOINT](state, endpoint) {
state.endpoint = endpoint;
},
[types.REMOVE_IGNORED_RESOLVED_ERRORS](state, error) {
state.errors = state.errors.filter(err => err.id !== error);
},
};

View File

@ -1,4 +1,4 @@
import _ from 'underscore';
import { isMatch } from 'lodash';
import { __, s__, sprintf } from '~/locale';
import { getDisplayName } from '../utils';
@ -7,7 +7,7 @@ export const hasProjects = state => Boolean(state.projects) && state.projects.le
export const isProjectInvalid = (state, getters) =>
Boolean(state.selectedProject) &&
getters.hasProjects &&
!state.projects.some(project => _.isMatch(state.selectedProject, project));
!state.projects.some(project => isMatch(state.selectedProject, project));
export const dropdownLabel = (state, getters) => {
if (state.selectedProject !== null) {

View File

@ -1,4 +1,4 @@
import _ from 'underscore';
import { pick } from 'lodash';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
import { projectKeys } from '../utils';
@ -12,7 +12,7 @@ export default {
.map(convertObjectPropsToCamelCase)
// The `pick` strips out extra properties returned from Sentry.
// Such properties could be problematic later, e.g. when checking whether `projects` contains `selectedProject`
.map(project => _.pick(project, projectKeys));
.map(project => pick(project, projectKeys));
},
[types.RESET_CONNECT](state) {
state.connectSuccessful = false;
@ -29,10 +29,7 @@ export default {
state.operationsSettingsEndpoint = operationsSettingsEndpoint;
if (project) {
state.selectedProject = _.pick(
convertObjectPropsToCamelCase(JSON.parse(project)),
projectKeys,
);
state.selectedProject = pick(convertObjectPropsToCamelCase(JSON.parse(project)), projectKeys);
}
},
[types.UPDATE_API_HOST](state, apiHost) {

View File

@ -0,0 +1,6 @@
fragment Author on User {
avatarUrl
name
username
webUrl
}

View File

@ -10,6 +10,7 @@ import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
import TestReports from './components/test_reports/test_reports.vue';
import testReportsStore from './stores/test_reports';
import axios from '~/lib/utils/axios_utils';
Vue.use(Translate);
@ -111,5 +112,12 @@ export default () => {
return createElement('test-reports');
},
});
axios
.get(dataset.testReportEndpoint)
.then(({ data }) => {
document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
})
.catch(() => {});
}
};

View File

@ -1,8 +0,0 @@
fragment Author on Snippet {
author {
name,
avatarUrl,
username,
webUrl
}
}

View File

@ -1,6 +1,6 @@
#import '../fragments/snippetBase.fragment.graphql'
#import '../fragments/project.fragment.graphql'
#import '../fragments/author.fragment.graphql'
#import "~/graphql_shared/fragments/author.fragment.graphql"
query GetSnippetQuery($ids: [ID!]) {
snippets(ids: $ids) {
@ -8,7 +8,9 @@ query GetSnippetQuery($ids: [ID!]) {
node {
...SnippetBase
...Project
...Author
author {
...Author
}
}
}
}

View File

@ -8,16 +8,13 @@ class KeysFinder
'md5' => 'fingerprint'
}.freeze
def initialize(current_user, params)
@current_user = current_user
def initialize(params)
@params = params
end
def execute
raise GitLabAccessDeniedError unless current_user.admin?
keys = by_key_type
keys = by_user(keys)
keys = by_users(keys)
keys = sort(keys)
by_fingerprint(keys)
@ -25,7 +22,7 @@ class KeysFinder
private
attr_reader :current_user, :params
attr_reader :params
def by_key_type
if params[:key_type] == 'ssh'
@ -39,10 +36,10 @@ class KeysFinder
keys.order_last_used_at_desc
end
def by_user(keys)
return keys unless params[:user]
def by_users(keys)
return keys unless params[:users]
keys.for_user(params[:user])
keys.for_user(params[:users])
end
def by_fingerprint(keys)

View File

@ -18,7 +18,7 @@
%li.js-tests-tab-link
= link_to test_report_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-tests', action: 'test_report', toggle: 'tab' }, class: 'test-tab' do
= s_('TestReports|Tests')
%span.badge.badge-pill= pipeline.test_reports.total_count
%span.badge.badge-pill.js-test-report-badge-counter
= render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
.tab-content

View File

@ -0,0 +1,5 @@
---
title: Support for table of contents tag in GitLab Flavored Markdown
merge_request: 24196
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: removes store logic from issue board models
merge_request: 21404
author: nuwe1
type: other

View File

@ -0,0 +1,5 @@
---
title: Upgrade to Gitaly v1.86.0
merge_request: 24610
author:
type: changed

View File

@ -106,6 +106,7 @@ recorded:
- Grant OAuth access
- Started/stopped user impersonation
- Changed username
- User was deleted
It is possible to filter particular actions by choosing an audit data type from
the filter dropdown box. You can further filter by specific group, project or user

View File

@ -81,6 +81,20 @@ Since use of the group-managed account requires the use of SSO, users of group-m
- The user will be unable to access the group (their credentials will no longer work on the identity provider when prompted to SSO).
- Contributions in the group (e.g. issues, merge requests) will remain intact.
##### Credentials inventory for Group-managed accounts **(ULTIMATE)**
> [Introduced in GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/38133)
Owners who manage user accounts in a group can view the following details of personal access tokens and SSH keys:
- Owners
- Scopes
- Usage patterns
To access the Credentials inventory of a group, navigate to **{shield}** **Security & Compliance > Credentials** in your group's sidebar.
This feature is similar to the [Credentials inventory for self-managed instances](../../admin_area/credentials_inventory.md).
#### Assertions
When using group-managed accounts, the following user details need to be passed to GitLab as SAML

View File

@ -26,7 +26,7 @@ module API
get do
authenticated_with_can_read_all_resources!
key = KeysFinder.new(current_user, params).execute
key = KeysFinder.new(params).execute
not_found!('Key') unless key

View File

@ -25,12 +25,10 @@ module Banzai
# * [[http://example.com/images/logo.png]]
# * [[http://example.com/images/logo.png|alt=Logo]]
#
# - Insert a Table of Contents list:
#
# * [[_TOC_]]
#
# Based on Gollum::Filter::Tags
#
# Note: the table of contents tag is now handled by TableOfContentsTagFilter
#
# Context options:
# :project_wiki (required) - Current project wiki.
#
@ -64,23 +62,11 @@ module Banzai
def call
doc.search(".//text()").each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
next unless node.content =~ TAGS_PATTERN
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
# needs special-case handling
if toc_tag?(node)
process_toc_tag(node)
else
content = node.content
html = process_tag($1)
next unless content =~ TAGS_PATTERN
html = process_tag($1)
if html && html != node.content
node.replace(html)
end
end
node.replace(html) if html && html != node.content
end
doc
@ -88,12 +74,6 @@ module Banzai
private
# Replace an entire `[[<em>TOC</em>]]` node with the result generated by
# TableOfContentsFilter
def process_toc_tag(node)
node.parent.parent.replace(result[:toc].presence || '')
end
# Process a single tag into its final HTML form.
#
# tag - The String tag contents (the stuff inside the double brackets).
@ -129,12 +109,6 @@ module Banzai
end
end
def toc_tag?(node)
node.content == 'TOC' &&
node.parent.name == 'em' &&
node.parent.parent.text == '[[TOC]]'
end
def image?(path)
path =~ ALLOWED_IMAGE_EXTENSIONS
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Banzai
module Filter
# Using `[[_TOC_]]`, inserts a Table of Contents list.
# This syntax is based on the Gollum syntax. This way we have
# some consistency between with wiki and normal markdown.
# If there ever emerges a markdown standard, we can implement
# that here.
#
# The support for this has been removed from GollumTagsFilter
#
# Based on Banzai::Filter::GollumTagsFilter
class TableOfContentsTagFilter < HTML::Pipeline::Filter
TEXT_QUERY = %q(descendant-or-self::text()[ancestor::p and contains(., 'TOC')])
def call
return doc if context[:no_header_anchors]
doc.xpath(TEXT_QUERY).each do |node|
# A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
# before this one, it will be converted into `[[<em>TOC</em>]]`, so it
# needs special-case handling
process_toc_tag(node) if toc_tag?(node)
end
doc
end
private
# Replace an entire `[[<em>TOC</em>]]` node with the result generated by
# TableOfContentsFilter
def process_toc_tag(node)
node.parent.parent.replace(result[:toc].presence || '')
end
def toc_tag?(node)
node.content == 'TOC' &&
node.parent.name == 'em' &&
node.parent.parent.text == '[[TOC]]'
end
end
end
end

View File

@ -32,6 +32,7 @@ module Banzai
Filter::InlineMetricsFilter,
Filter::InlineGrafanaMetricsFilter,
Filter::TableOfContentsFilter,
Filter::TableOfContentsTagFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
Filter::SuggestionFilter,

View File

@ -11,6 +11,7 @@ class Feature
inforef_uploadpack_cache
commit_without_batch_check
use_core_delta_islands
use_git_protocol_v2
].freeze
DEFAULT_ON_FLAGS = Set.new([]).freeze

View File

@ -6732,6 +6732,12 @@ msgstr ""
msgid "Display name"
msgstr ""
msgid "Display rendered file"
msgstr ""
msgid "Display source"
msgstr ""
msgid "Displays dependencies and known vulnerabilities, based on the %{linkStart}latest pipeline%{linkEnd} scan"
msgstr ""

View File

@ -356,6 +356,18 @@ describe 'Pipeline', :js do
end
end
context 'test tabs' do
let(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
it 'shows badge counter in Tests tab' do
visit_pipeline
wait_for_requests
expect(pipeline.test_reports.total_count).to eq(4)
expect(page.find('.js-test-report-badge-counter').text).to eq(pipeline.test_reports.total_count.to_s)
end
end
context 'retrying jobs' do
before do
visit_pipeline

View File

@ -3,7 +3,7 @@
require 'spec_helper'
describe KeysFinder do
subject { described_class.new(user, params).execute }
subject { described_class.new(params).execute }
let(:user) { create(:user) }
let(:params) { {} }
@ -20,159 +20,149 @@ describe KeysFinder do
let!(:key_2) { create(:personal_key, last_used_at: nil, user: user) }
let!(:key_3) { create(:personal_key, last_used_at: 2.days.ago) }
context 'with a regular user' do
it 'raises GitLabAccessDeniedError' do
expect { subject }.to raise_error(KeysFinder::GitLabAccessDeniedError)
context 'key_type' do
let!(:deploy_key) { create(:deploy_key) }
context 'when `key_type` is `ssh`' do
before do
params[:key_type] = 'ssh'
end
it 'returns only SSH keys' do
expect(subject).to contain_exactly(key_1, key_2, key_3)
end
end
context 'when `key_type` is not specified' do
it 'returns all types of keys' do
expect(subject).to contain_exactly(key_1, key_2, key_3, deploy_key)
end
end
end
context 'with an admin user' do
let(:user) {create(:admin)}
context 'key_type' do
let!(:deploy_key) { create(:deploy_key) }
context 'when `key_type` is `ssh`' do
context 'fingerprint' do
context 'with invalid fingerprint' do
context 'with invalid MD5 fingerprint' do
before do
params[:key_type] = 'ssh'
params[:fingerprint] = '11:11:11:11'
end
it 'returns only SSH keys' do
expect(subject).to contain_exactly(key_1, key_2, key_3)
it 'raises InvalidFingerprint' do
expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
end
end
context 'when `key_type` is not specified' do
it 'returns all types of keys' do
expect(subject).to contain_exactly(key_1, key_2, key_3, deploy_key)
end
end
end
context 'fingerprint' do
context 'with invalid fingerprint' do
context 'with invalid MD5 fingerprint' do
before do
params[:fingerprint] = '11:11:11:11'
end
it 'raises InvalidFingerprint' do
expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
end
end
context 'with invalid SHA fingerprint' do
before do
params[:fingerprint] = 'nUhzNyftwAAKs7HufskYTte2g'
end
it 'raises InvalidFingerprint' do
expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
end
end
end
context 'with valid fingerprints' do
let!(:deploy_key) do
create(:deploy_key,
user: user,
key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1017k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=',
fingerprint: '8a:4a:12:92:0b:50:47:02:d4:5a:8e:a9:44:4e:08:b4',
fingerprint_sha256: '4DPHOVNh53i9dHb5PpY2vjfyf5qniTx1/pBFPoZLDdk')
end
context 'personal key with valid MD5 params' do
context 'with an existent fingerprint' do
before do
params[:fingerprint] = 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1'
end
it 'returns the key' do
expect(subject).to eq(key_1)
expect(subject.user).to eq(user)
end
end
context 'deploy key with an existent fingerprint' do
before do
params[:fingerprint] = '8a:4a:12:92:0b:50:47:02:d4:5a:8e:a9:44:4e:08:b4'
end
it 'returns the key' do
expect(subject).to eq(deploy_key)
expect(subject.user).to eq(user)
end
end
context 'with a non-existent fingerprint' do
before do
params[:fingerprint] = 'bb:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d2'
end
it 'returns nil' do
expect(subject).to be_nil
end
end
end
context 'personal key with valid SHA256 params' do
context 'with an existent fingerprint' do
before do
params[:fingerprint] = 'SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
end
it 'returns key' do
expect(subject).to eq(key_1)
expect(subject.user).to eq(user)
end
end
context 'deploy key with an existent fingerprint' do
before do
params[:fingerprint] = 'SHA256:4DPHOVNh53i9dHb5PpY2vjfyf5qniTx1/pBFPoZLDdk'
end
it 'returns key' do
expect(subject).to eq(deploy_key)
expect(subject.user).to eq(user)
end
end
context 'with a non-existent fingerprint' do
before do
params[:fingerprint] = 'SHA256:xTjuFqftwADy8AH3wFY31tAKs7HufskYTte2aXi/mNp'
end
it 'returns nil' do
expect(subject).to be_nil
end
end
end
end
end
context 'user' do
context 'without user' do
it 'contains ssh_keys of all users in the system' do
expect(subject).to contain_exactly(key_1, key_2, key_3)
end
end
context 'with user' do
context 'with invalid SHA fingerprint' do
before do
params[:user] = user
params[:fingerprint] = 'nUhzNyftwAAKs7HufskYTte2g'
end
it 'contains ssh_keys of only the specified users' do
expect(subject).to contain_exactly(key_1, key_2)
it 'raises InvalidFingerprint' do
expect { subject }.to raise_error(KeysFinder::InvalidFingerprint)
end
end
end
context 'sort order' do
it 'sorts in last_used_at_desc order' do
expect(subject).to eq([key_3, key_1, key_2])
context 'with valid fingerprints' do
let!(:deploy_key) do
create(:deploy_key,
user: user,
key: 'ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1017k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=',
fingerprint: '8a:4a:12:92:0b:50:47:02:d4:5a:8e:a9:44:4e:08:b4',
fingerprint_sha256: '4DPHOVNh53i9dHb5PpY2vjfyf5qniTx1/pBFPoZLDdk')
end
context 'personal key with valid MD5 params' do
context 'with an existent fingerprint' do
before do
params[:fingerprint] = 'ba:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d1'
end
it 'returns the key' do
expect(subject).to eq(key_1)
expect(subject.user).to eq(user)
end
end
context 'deploy key with an existent fingerprint' do
before do
params[:fingerprint] = '8a:4a:12:92:0b:50:47:02:d4:5a:8e:a9:44:4e:08:b4'
end
it 'returns the key' do
expect(subject).to eq(deploy_key)
expect(subject.user).to eq(user)
end
end
context 'with a non-existent fingerprint' do
before do
params[:fingerprint] = 'bb:81:59:68:d7:6c:cd:02:02:bf:6a:9b:55:4e:af:d2'
end
it 'returns nil' do
expect(subject).to be_nil
end
end
end
context 'personal key with valid SHA256 params' do
context 'with an existent fingerprint' do
before do
params[:fingerprint] = 'SHA256:nUhzNyftwADy8AH3wFY31tAKs7HufskYTte2aXo/lCg'
end
it 'returns key' do
expect(subject).to eq(key_1)
expect(subject.user).to eq(user)
end
end
context 'deploy key with an existent fingerprint' do
before do
params[:fingerprint] = 'SHA256:4DPHOVNh53i9dHb5PpY2vjfyf5qniTx1/pBFPoZLDdk'
end
it 'returns key' do
expect(subject).to eq(deploy_key)
expect(subject.user).to eq(user)
end
end
context 'with a non-existent fingerprint' do
before do
params[:fingerprint] = 'SHA256:xTjuFqftwADy8AH3wFY31tAKs7HufskYTte2aXi/mNp'
end
it 'returns nil' do
expect(subject).to be_nil
end
end
end
end
end
context 'user' do
context 'without user' do
it 'contains ssh_keys of all users in the system' do
expect(subject).to contain_exactly(key_1, key_2, key_3)
end
end
context 'with user' do
before do
params[:users] = user
end
it 'contains ssh_keys of only the specified users' do
expect(subject).to contain_exactly(key_1, key_2)
end
end
end
context 'sort order' do
it 'sorts in last_used_at_desc order' do
expect(subject).to eq([key_3, key_1, key_2])
end
end
end

View File

@ -0,0 +1,109 @@
import { mount } from '@vue/test-utils';
import BlobHeaderViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import {
RICH_BLOB_VIEWER,
RICH_BLOB_VIEWER_TITLE,
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
} from '~/blob/components/constants';
import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { Blob } from './mock_data';
describe('Blob Header Viewer Switcher', () => {
let wrapper;
function createComponent(props = {}) {
wrapper = mount(BlobHeaderViewerSwitcher, {
propsData: {
blob: Object.assign({}, Blob, props),
},
});
}
afterEach(() => {
wrapper.destroy();
});
describe('intiialization', () => {
it('is initialized with rich viewer as preselected when richViewer exists', () => {
createComponent();
expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
});
it('is initialized with simple viewer as preselected when richViewer does not exists', () => {
createComponent({ richViewer: null });
expect(wrapper.vm.viewer).toBe(SIMPLE_BLOB_VIEWER);
});
});
describe('rendering', () => {
let btnGroup;
let buttons;
beforeEach(() => {
createComponent();
btnGroup = wrapper.find(GlButtonGroup);
buttons = wrapper.findAll(GlButton);
});
it('renders gl-button-group component', () => {
expect(btnGroup.exists()).toBe(true);
});
it('renders exactly 2 buttons with predefined actions', () => {
expect(buttons.length).toBe(2);
[SIMPLE_BLOB_VIEWER_TITLE, RICH_BLOB_VIEWER_TITLE].forEach((title, i) => {
expect(buttons.at(i).attributes('title')).toBe(title);
});
});
});
describe('viewer changes', () => {
let buttons;
let simpleBtn;
let richBtn;
beforeEach(() => {
createComponent();
buttons = wrapper.findAll(GlButton);
simpleBtn = buttons.at(0);
richBtn = buttons.at(1);
});
it('does not switch the viewer if the selected one is already active', () => {
jest.spyOn(wrapper.vm, '$emit');
expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
richBtn.vm.$emit('click');
expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
expect(wrapper.vm.$emit).not.toHaveBeenCalled();
});
it('emits an event when a Simple Viewer button is clicked', () => {
jest.spyOn(wrapper.vm, '$emit');
simpleBtn.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.viewer).toBe(SIMPLE_BLOB_VIEWER);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('switch-viewer', SIMPLE_BLOB_VIEWER);
});
});
it('emits an event when a Rich Viewer button is clicked', () => {
jest.spyOn(wrapper.vm, '$emit');
wrapper.setData({ viewer: SIMPLE_BLOB_VIEWER });
return wrapper.vm
.$nextTick()
.then(() => {
richBtn.vm.$emit('click');
})
.then(() => {
expect(wrapper.vm.viewer).toBe(RICH_BLOB_VIEWER);
expect(wrapper.vm.$emit).toHaveBeenCalledWith('switch-viewer', RICH_BLOB_VIEWER);
});
});
});
});

View File

@ -1,5 +1,5 @@
import Vue from 'vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mountComponent from 'helpers/vue_mount_component_helper';
import IssueCardInnerScopedLabel from '~/boards/components/issue_card_inner_scoped_label.vue';
describe('IssueCardInnerScopedLabel Component', () => {

View File

@ -62,6 +62,7 @@ describe('ErrorTrackingList', () => {
sortByField: jest.fn(),
fetchPaginatedResults: jest.fn(),
updateStatus: jest.fn(),
removeIgnoredResolvedErrors: jest.fn(),
};
const state = {
@ -221,6 +222,8 @@ describe('ErrorTrackingList', () => {
});
describe('When the ignore button on an error is clicked', () => {
const ignoreErrorButton = () => wrapper.find({ ref: 'ignoreError' });
beforeEach(() => {
store.state.list.loading = false;
store.state.list.errors = errorsList;
@ -235,20 +238,30 @@ describe('ErrorTrackingList', () => {
});
it('sends the "ignored" status and error ID', () => {
wrapper.find({ ref: 'ignoreError' }).trigger('click');
ignoreErrorButton().trigger('click');
expect(actions.updateStatus).toHaveBeenCalledWith(
expect.anything(),
{
endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
redirectUrl: '/error_tracking',
status: 'ignored',
},
undefined,
);
});
it('calls an action to remove the item from the list', () => {
ignoreErrorButton().trigger('click');
expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(
expect.anything(),
'1',
undefined,
);
});
});
describe('When the resolve button on an error is clicked', () => {
const resolveErrorButton = () => wrapper.find({ ref: 'resolveError' });
beforeEach(() => {
store.state.list.loading = false;
store.state.list.errors = errorsList;
@ -263,17 +276,25 @@ describe('ErrorTrackingList', () => {
});
it('sends "resolved" status and error ID', () => {
wrapper.find({ ref: 'resolveError' }).trigger('click');
resolveErrorButton().trigger('click');
expect(actions.updateStatus).toHaveBeenCalledWith(
expect.anything(),
{
endpoint: `/project/test/-/error_tracking/${errorsList[0].id}.json`,
redirectUrl: '/error_tracking',
status: 'resolved',
},
undefined,
);
});
it('calls an action to remove the item from the list', () => {
resolveErrorButton().trigger('click');
expect(actions.removeIgnoredResolvedErrors).toHaveBeenCalledWith(
expect.anything(),
'1',
undefined,
);
});
});
describe('When error tracking is disabled and user is not allowed to enable it', () => {

View File

@ -5,6 +5,7 @@ import * as types from '~/error_tracking/store/list/mutation_types';
const ADD_RECENT_SEARCH = mutations[types.ADD_RECENT_SEARCH];
const CLEAR_RECENT_SEARCHES = mutations[types.CLEAR_RECENT_SEARCHES];
const LOAD_RECENT_SEARCHES = mutations[types.LOAD_RECENT_SEARCHES];
const REMOVE_IGNORED_RESOLVED_ERRORS = mutations[types.REMOVE_IGNORED_RESOLVED_ERRORS];
describe('Error tracking mutations', () => {
describe('SET_ERRORS', () => {
@ -114,5 +115,29 @@ describe('Error tracking mutations', () => {
expect(localStorage.getItem).toHaveBeenCalledWith('recent-searches/project/errors.json');
});
});
describe('REMOVE_IGNORED_RESOLVED_ERRORS', () => {
it('removes ignored or resolved errors from list', () => {
state.errors = [
{
id: 1,
status: 'unresolved',
},
{
id: 2,
status: 'ignored',
},
{
id: 3,
status: 'unresolved',
},
];
const ignoredError = state.errors[2].id;
REMOVE_IGNORED_RESOLVED_ERRORS(state, ignoredError);
expect(state.errors).not.toContain(ignoredError);
});
});
});
});

View File

@ -100,19 +100,4 @@ describe Banzai::Filter::GollumTagsFilter do
expect(doc.at_css('code').text).to eq '[[link-in-backticks]]'
end
end
context 'table of contents' do
it 'replaces [[<em>TOC</em>]] with ToC result' do
doc = described_class.call("<p>[[<em>TOC</em>]]</p>", { project_wiki: project_wiki }, { toc: "FOO" })
expect(doc.to_html).to eq("FOO")
end
it 'handles an empty ToC result' do
input = "<p>[[<em>TOC</em>]]</p>"
doc = described_class.call(input, project_wiki: project_wiki)
expect(doc.to_html).to eq ''
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'spec_helper'
describe Banzai::Filter::TableOfContentsTagFilter do
include FilterSpecHelper
context 'table of contents' do
let(:html) { '<p>[[<em>TOC</em>]]</p>' }
it 'replaces [[<em>TOC</em>]] with ToC result' do
doc = filter(html, {}, { toc: "FOO" })
expect(doc.to_html).to eq("FOO")
end
it 'handles an empty ToC result' do
doc = filter(html)
expect(doc.to_html).to eq ''
end
end
end

View File

@ -99,4 +99,35 @@ describe Banzai::Pipeline::FullPipeline do
end
end
end
describe 'table of contents' do
let(:project) { create(:project, :public) }
let(:markdown) do
<<-MARKDOWN.strip_heredoc
[[_TOC_]]
# Header
MARKDOWN
end
let(:invalid_markdown) do
<<-MARKDOWN.strip_heredoc
test [[_TOC_]]
# Header
MARKDOWN
end
it 'inserts a table of contents' do
output = described_class.to_html(markdown, project: project)
expect(output).to include("<ul class=\"section-nav\">")
expect(output).to include("<li><a href=\"#header\">Header</a></li>")
end
it 'does not insert a table of contents' do
output = described_class.to_html(invalid_markdown, project: project)
expect(output).to include("test [[<em>TOC</em>]]")
end
end
end

View File

@ -326,7 +326,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true')
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true')
expect(user.reload.last_activity_on).to eql(Date.today)
end
end
@ -346,7 +346,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true')
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true')
expect(user.reload.last_activity_on).to be_nil
end
end
@ -594,7 +594,7 @@ describe API::Internal::Base do
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(project.repository.gitaly_repository.relative_path)
expect(json_response["gitaly"]["address"]).to eq(Gitlab::GitalyClient.address(project.repository_storage))
expect(json_response["gitaly"]["token"]).to eq(Gitlab::GitalyClient.token(project.repository_storage))
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true')
expect(json_response["gitaly"]["features"]).to eq('gitaly-feature-inforef-uploadpack-cache' => 'true', 'gitaly-feature-cache-invalidator' => 'true', 'gitaly-feature-commit-without-batch-check' => 'true', 'gitaly-feature-use-core-delta-islands' => 'true', 'gitaly-feature-use-git-protocol-v2' => 'true')
end
end

View File

@ -121,10 +121,17 @@ describe Users::DestroyService do
before do
solo_owned.group_members = [member]
service.execute(user)
end
it 'returns the user with attached errors' do
expect(service.execute(user)).to be(user)
expect(user.errors.full_messages).to eq([
'You must transfer ownership or delete groups before you can remove user'
])
end
it 'does not delete the user' do
service.execute(user)
expect(User.find(user.id)).to eq user
end
end

View File

@ -15,7 +15,7 @@ module FilterSpecHelper
# context - Hash context for the filter. (default: {project: project})
#
# Returns a Nokogiri::XML::DocumentFragment
def filter(html, context = {})
def filter(html, context = {}, result = nil)
if defined?(project)
context.reverse_merge!(project: project)
end
@ -25,7 +25,7 @@ module FilterSpecHelper
context = context.merge(render_context: render_context)
described_class.call(html, context)
described_class.call(html, context, result)
end
# Get an instance of the Filter class