Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d87918510a
commit
5564275a0b
|
|
@ -2,6 +2,32 @@
|
|||
.if-canonical-dot-com-gitlab-org-group-master-refs: &if-canonical-dot-com-gitlab-org-group-master-refs
|
||||
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && $CI_COMMIT_REF_NAME == "master"'
|
||||
|
||||
# Make sure to update all the similar patterns in other CI config files if you modify these patterns
|
||||
.code-backstage-qa-patterns: &code-backstage-qa-patterns
|
||||
- ".gitlab/ci/**/*"
|
||||
- ".{eslintignore,gitattributes,nvmrc,prettierrc,stylelintrc,yamllint}"
|
||||
- ".{codeclimate,eslintrc,gitlab-ci,haml-lint,haml-lint_todo,rubocop,rubocop_todo,scss-lint}.yml"
|
||||
- ".csscomb.json"
|
||||
- "Dockerfile.assets"
|
||||
- "*_VERSION"
|
||||
- "Gemfile{,.lock}"
|
||||
- "Rakefile"
|
||||
- "{babel.config,jest.config}.js"
|
||||
- "config.ru"
|
||||
- "{package.json,yarn.lock}"
|
||||
- "{,ee/}{app,bin,config,db,haml_lint,lib,locale,public,scripts,symbol,vendor}/**/*"
|
||||
- "doc/api/graphql/reference/*" # Files in this folder are auto-generated
|
||||
# Backstage changes
|
||||
- "Dangerfile"
|
||||
- "danger/**/*"
|
||||
- "{,ee/}fixtures/**/*"
|
||||
- "{,ee/}rubocop/**/*"
|
||||
- "{,ee/}spec/**/*"
|
||||
- "doc/README.md" # Some RSpec test rely on this file
|
||||
# QA changes
|
||||
- ".dockerignore"
|
||||
- "qa/**/*"
|
||||
|
||||
pages:
|
||||
extends:
|
||||
- .default-tags
|
||||
|
|
@ -9,6 +35,7 @@ pages:
|
|||
- .default-cache
|
||||
rules:
|
||||
- <<: *if-canonical-dot-com-gitlab-org-group-master-refs
|
||||
changes: *code-backstage-qa-patterns
|
||||
when: on_success
|
||||
stage: pages
|
||||
dependencies: ["coverage", "karma", "gitlab:assets:compile pull-cache"]
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ setup-test-env:
|
|||
- rspec_profiling/
|
||||
- tmp/capybara/
|
||||
- tmp/memory_test/
|
||||
- junit_rspec.xml
|
||||
reports:
|
||||
junit: junit_rspec.xml
|
||||
|
||||
|
|
|
|||
5
Gemfile
5
Gemfile
|
|
@ -488,3 +488,8 @@ gem 'liquid', '~> 4.0'
|
|||
gem 'lru_redux'
|
||||
|
||||
gem 'erubi', '~> 1.9.0'
|
||||
|
||||
# Locked as long as quoted-printable encoding issues are not resolved
|
||||
# Monkey-patched in `config/initializers/mail_encoding_patch.rb`
|
||||
# See https://gitlab.com/gitlab-org/gitlab/issues/197386
|
||||
gem 'mail', '= 2.7.1'
|
||||
|
|
|
|||
|
|
@ -1283,6 +1283,7 @@ DEPENDENCIES
|
|||
lograge (~> 0.5)
|
||||
loofah (~> 2.2)
|
||||
lru_redux
|
||||
mail (= 2.7.1)
|
||||
mail_room (~> 0.10.0)
|
||||
marginalia (~> 1.8.0)
|
||||
memory_profiler (~> 0.9)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ const Api = {
|
|||
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
|
||||
adminStatisticsPath: '/api/:version/application/statistics',
|
||||
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
|
||||
lsifPath: '/api/:version/projects/:id/commits/:commit_id/lsif/info',
|
||||
|
||||
group(groupId, callback) {
|
||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||
|
|
@ -457,6 +458,14 @@ const Api = {
|
|||
return axios.get(url);
|
||||
},
|
||||
|
||||
lsifData(projectPath, commitId, path) {
|
||||
const url = Api.buildUrl(this.lsifPath)
|
||||
.replace(':id', encodeURIComponent(projectPath))
|
||||
.replace(':commit_id', commitId);
|
||||
|
||||
return axios.get(url, { params: { path } });
|
||||
},
|
||||
|
||||
buildUrl(url) {
|
||||
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="issue-count">
|
||||
<div class="issue-count text-nowrap">
|
||||
<span class="js-issue-size" :class="{ 'text-danger': issuesExceedMax }">
|
||||
{{ issuesSize }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import Popover from './popover.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Popover,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['currentDefinition', 'currentDefinitionPosition']),
|
||||
},
|
||||
mounted() {
|
||||
this.blobViewer = document.querySelector('.blob-viewer');
|
||||
|
||||
this.addGlobalEventListeners();
|
||||
this.fetchData();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.removeGlobalEventListeners();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchData', 'showDefinition']),
|
||||
addGlobalEventListeners() {
|
||||
if (this.blobViewer) {
|
||||
this.blobViewer.addEventListener('click', this.showDefinition);
|
||||
}
|
||||
},
|
||||
removeGlobalEventListeners() {
|
||||
if (this.blobViewer) {
|
||||
this.blobViewer.removeEventListener('click', this.showDefinition);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<popover
|
||||
v-if="currentDefinition"
|
||||
:position="currentDefinitionPosition"
|
||||
:data="currentDefinition"
|
||||
/>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
},
|
||||
props: {
|
||||
position: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
offsetLeft: 0,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
positionStyles() {
|
||||
return {
|
||||
left: `${this.position.x - this.offsetLeft}px`,
|
||||
top: `${this.position.y + this.position.height}px`,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
position: {
|
||||
handler() {
|
||||
this.$nextTick(() => this.updateOffsetLeft());
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateOffsetLeft() {
|
||||
this.offsetLeft = Math.max(
|
||||
0,
|
||||
this.$el.offsetLeft + this.$el.offsetWidth - window.innerWidth + 20,
|
||||
);
|
||||
},
|
||||
},
|
||||
colorScheme: gon?.user_color_scheme,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:style="positionStyles"
|
||||
class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show"
|
||||
>
|
||||
<div :style="{ left: `${offsetLeft}px` }" class="arrow"></div>
|
||||
<div v-for="(hover, index) in data.hover" :key="index" class="border-bottom">
|
||||
<pre
|
||||
v-if="hover.language"
|
||||
ref="code-output"
|
||||
:class="$options.colorScheme"
|
||||
class="border-0 bg-transparent m-0 code highlight"
|
||||
v-html="hover.value"
|
||||
></pre>
|
||||
<p v-else ref="doc-output" class="p-3 m-0">
|
||||
{{ hover.value }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="data.definition_url" class="popover-body">
|
||||
<gl-button :href="data.definition_url" target="_blank" class="w-100" variant="default">
|
||||
{{ __('Go to definition') }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import store from './store';
|
||||
import App from './components/app.vue';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-code-navigation');
|
||||
|
||||
store.dispatch('setInitialData', el.dataset);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
render(h) {
|
||||
return h(App);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
import api from '~/api';
|
||||
import { __ } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
import * as types from './mutation_types';
|
||||
import { getCurrentHoverElement, setCurrentHoverElement, addInteractionClass } from '../utils';
|
||||
|
||||
export default {
|
||||
setInitialData({ commit }, data) {
|
||||
commit(types.SET_INITIAL_DATA, data);
|
||||
},
|
||||
requestDataError({ commit }) {
|
||||
commit(types.REQUEST_DATA_ERROR);
|
||||
createFlash(__('An error occurred loading code navigation'));
|
||||
},
|
||||
fetchData({ commit, dispatch, state }) {
|
||||
commit(types.REQUEST_DATA);
|
||||
|
||||
api
|
||||
.lsifData(state.projectPath, state.commitId, state.path)
|
||||
.then(({ data }) => {
|
||||
const normalizedData = data.reduce((acc, d) => {
|
||||
if (d.hover) {
|
||||
acc[`${d.start_line}:${d.start_char}`] = d;
|
||||
addInteractionClass(d);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
commit(types.REQUEST_DATA_SUCCESS, normalizedData);
|
||||
})
|
||||
.catch(() => dispatch('requestDataError'));
|
||||
},
|
||||
showDefinition({ commit, state }, { target: el }) {
|
||||
let definition;
|
||||
let position;
|
||||
|
||||
if (!state.data) return;
|
||||
|
||||
const isCurrentElementPopoverOpen = el.classList.contains('hll');
|
||||
|
||||
if (getCurrentHoverElement()) {
|
||||
getCurrentHoverElement().classList.remove('hll');
|
||||
}
|
||||
|
||||
if (el.classList.contains('js-code-navigation') && !isCurrentElementPopoverOpen) {
|
||||
const { lineIndex, charIndex } = el.dataset;
|
||||
|
||||
position = {
|
||||
x: el.offsetLeft,
|
||||
y: el.offsetTop,
|
||||
height: el.offsetHeight,
|
||||
};
|
||||
definition = state.data[`${lineIndex}:${charIndex}`];
|
||||
|
||||
el.classList.add('hll');
|
||||
|
||||
setCurrentHoverElement(el);
|
||||
}
|
||||
|
||||
commit(types.SET_CURRENT_DEFINITION, { definition, position });
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import Vuex from 'vuex';
|
||||
import createState from './state';
|
||||
import actions from './actions';
|
||||
import mutations from './mutations';
|
||||
|
||||
export default new Vuex.Store({
|
||||
actions,
|
||||
mutations,
|
||||
state: createState(),
|
||||
});
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
|
||||
export const REQUEST_DATA = 'REQUEST_DATA';
|
||||
export const REQUEST_DATA_SUCCESS = 'REQUEST_DATA_SUCCESS';
|
||||
export const REQUEST_DATA_ERROR = 'REQUEST_DATA_ERROR';
|
||||
export const SET_CURRENT_DEFINITION = 'SET_CURRENT_DEFINITION';
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_INITIAL_DATA](state, { projectPath, commitId, blobPath }) {
|
||||
state.projectPath = projectPath;
|
||||
state.commitId = commitId;
|
||||
state.blobPath = blobPath;
|
||||
},
|
||||
[types.REQUEST_DATA](state) {
|
||||
state.loading = true;
|
||||
},
|
||||
[types.REQUEST_DATA_SUCCESS](state, data) {
|
||||
state.loading = false;
|
||||
state.data = data;
|
||||
},
|
||||
[types.REQUEST_DATA_ERROR](state) {
|
||||
state.loading = false;
|
||||
},
|
||||
[types.SET_CURRENT_DEFINITION](state, { definition, position }) {
|
||||
state.currentDefinition = definition;
|
||||
state.currentDefinitionPosition = position;
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
export default () => ({
|
||||
projectPath: null,
|
||||
commitId: null,
|
||||
blobPath: null,
|
||||
loading: false,
|
||||
data: null,
|
||||
currentDefinition: null,
|
||||
currentDefinitionPosition: null,
|
||||
});
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
export const cachedData = new Map();
|
||||
|
||||
export const getCurrentHoverElement = () => cachedData.get('current');
|
||||
export const setCurrentHoverElement = el => cachedData.set('current', el);
|
||||
|
||||
export const addInteractionClass = d => {
|
||||
let charCount = 0;
|
||||
const line = document.getElementById(`LC${d.start_line + 1}`);
|
||||
const el = [...line.childNodes].find(({ textContent }) => {
|
||||
if (charCount === d.start_char) return true;
|
||||
charCount += textContent.length;
|
||||
return false;
|
||||
});
|
||||
|
||||
if (el) {
|
||||
el.setAttribute('data-char-index', d.start_char);
|
||||
el.setAttribute('data-line-index', d.start_line);
|
||||
el.classList.add('cursor-pointer', 'code-navigation', 'js-code-navigation');
|
||||
}
|
||||
};
|
||||
|
|
@ -30,4 +30,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
}
|
||||
|
||||
GpgBadges.fetch();
|
||||
|
||||
if (gon.features?.codeNavigation) {
|
||||
// eslint-disable-next-line promise/catch-or-return
|
||||
import('~/code_navigation').then(m => m.default());
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ export default {
|
|||
</a>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === $options.fieldTypes.miliseconds">{{
|
||||
sprintf(__('%{value} ms'), { value: field.value })
|
||||
<template v-else-if="field.type === $options.fieldTypes.seconds">{{
|
||||
sprintf(__('%{value} s'), { value: field.value })
|
||||
}}</template>
|
||||
|
||||
<template v-else-if="field.type === $options.fieldTypes.text">
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export const fieldTypes = {
|
||||
codeBock: 'codeBlock',
|
||||
link: 'link',
|
||||
miliseconds: 'miliseconds',
|
||||
seconds: 'seconds',
|
||||
text: 'text',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ export default () => ({
|
|||
execution_time: {
|
||||
value: null,
|
||||
text: s__('Reports|Execution time'),
|
||||
type: fieldTypes.miliseconds,
|
||||
type: fieldTypes.seconds,
|
||||
},
|
||||
failure: {
|
||||
value: null,
|
||||
|
|
|
|||
|
|
@ -54,11 +54,17 @@ const populateUserInfo = user => {
|
|||
);
|
||||
};
|
||||
|
||||
const initializedPopovers = new Map();
|
||||
|
||||
export default (elements = document.querySelectorAll('.js-user-link')) => {
|
||||
const userLinks = Array.from(elements);
|
||||
const UserPopoverComponent = Vue.extend(UserPopover);
|
||||
|
||||
return userLinks.map(el => {
|
||||
const UserPopoverComponent = Vue.extend(UserPopover);
|
||||
if (initializedPopovers.has(el)) {
|
||||
return initializedPopovers.get(el);
|
||||
}
|
||||
|
||||
const user = {
|
||||
location: null,
|
||||
bio: null,
|
||||
|
|
@ -73,6 +79,8 @@ export default (elements = document.querySelectorAll('.js-user-link')) => {
|
|||
},
|
||||
});
|
||||
|
||||
initializedPopovers.set(el, renderedPopover);
|
||||
|
||||
renderedPopover.$mount();
|
||||
|
||||
el.addEventListener('mouseenter', ({ target }) => {
|
||||
|
|
|
|||
|
|
@ -499,3 +499,15 @@ span.idiff {
|
|||
background-color: transparent;
|
||||
border: transparent;
|
||||
}
|
||||
|
||||
.code-navigation {
|
||||
border-bottom: 1px $gray-darkest dashed;
|
||||
|
||||
&:hover {
|
||||
border-bottom-color: $almost-black;
|
||||
}
|
||||
}
|
||||
|
||||
.code-navigation-popover {
|
||||
max-width: 450px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
@for $i from 1 through 12 {
|
||||
#{'.tab-width-#{$i}'} {
|
||||
-moz-tab-size: $i;
|
||||
tab-size: $i;
|
||||
}
|
||||
}
|
||||
|
||||
.border-width-1px { border-width: 1px; }
|
||||
.border-bottom-width-1px { border-bottom-width: 1px; }
|
||||
.border-style-dashed { border-style: dashed; }
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
|
|||
:time_display_relative,
|
||||
:time_format_in_24h,
|
||||
:show_whitespace_in_diffs,
|
||||
:tab_width,
|
||||
:sourcegraph_enabled,
|
||||
:render_whitespace_in_code
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ class Projects::BlobController < Projects::ApplicationController
|
|||
before_action :validate_diff_params, only: :diff
|
||||
before_action :set_last_commit_sha, only: [:edit, :update]
|
||||
|
||||
before_action only: :show do
|
||||
push_frontend_feature_flag(:code_navigation, @project)
|
||||
end
|
||||
|
||||
def new
|
||||
commit unless @repository.empty?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -63,6 +63,10 @@ module PreferencesHelper
|
|||
Gitlab::ColorSchemes.for_user(current_user).css_class
|
||||
end
|
||||
|
||||
def user_tab_width
|
||||
Gitlab::TabWidth.css_class_for_user(current_user)
|
||||
end
|
||||
|
||||
def language_choices
|
||||
Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -706,6 +706,10 @@ module ProjectsHelper
|
|||
Feature.enabled?(:vue_file_list, @project)
|
||||
end
|
||||
|
||||
def native_code_navigation_enabled?(project)
|
||||
Feature.enabled?(:code_navigation, project)
|
||||
end
|
||||
|
||||
def show_visibility_confirm_modal?(project)
|
||||
project.unlink_forks_upon_visibility_decrease_enabled? && project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && project.forks_count > 0
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ class DeployToken < ApplicationRecord
|
|||
has_many :project_deploy_tokens, inverse_of: :deploy_token
|
||||
has_many :projects, through: :project_deploy_tokens
|
||||
|
||||
has_many :group_deploy_tokens, inverse_of: :deploy_token
|
||||
has_many :groups, through: :group_deploy_tokens
|
||||
|
||||
validate :no_groups, unless: :group_type?
|
||||
validate :no_projects, unless: :project_type?
|
||||
validate :ensure_at_least_one_scope
|
||||
validates :username,
|
||||
length: { maximum: 255 },
|
||||
|
|
@ -24,6 +29,7 @@ class DeployToken < ApplicationRecord
|
|||
message: "can contain only letters, digits, '_', '-', '+', and '.'"
|
||||
}
|
||||
|
||||
validates :deploy_token_type, presence: true
|
||||
enum deploy_token_type: {
|
||||
group_type: 1,
|
||||
project_type: 2
|
||||
|
|
@ -56,18 +62,31 @@ class DeployToken < ApplicationRecord
|
|||
end
|
||||
|
||||
def has_access_to?(requested_project)
|
||||
active? && project == requested_project
|
||||
return false unless active?
|
||||
return false unless holder
|
||||
|
||||
holder.has_access_to?(requested_project)
|
||||
end
|
||||
|
||||
# This is temporal. Currently we limit DeployToken
|
||||
# to a single project, later we're going to extend
|
||||
# that to be for multiple projects and namespaces.
|
||||
# to a single project or group, later we're going to
|
||||
# extend that to be for multiple projects and namespaces.
|
||||
def project
|
||||
strong_memoize(:project) do
|
||||
projects.first
|
||||
end
|
||||
end
|
||||
|
||||
def holder
|
||||
strong_memoize(:holder) do
|
||||
if project_type?
|
||||
project_deploy_tokens.first
|
||||
elsif group_type?
|
||||
group_deploy_tokens.first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expires_at
|
||||
expires_at = read_attribute(:expires_at)
|
||||
expires_at != Forever.date ? expires_at : nil
|
||||
|
|
@ -92,4 +111,12 @@ class DeployToken < ApplicationRecord
|
|||
def default_username
|
||||
"gitlab+deploy-token-#{id}" if persisted?
|
||||
end
|
||||
|
||||
def no_groups
|
||||
errors.add(:deploy_token, 'cannot have groups assigned') if group_deploy_tokens.any?
|
||||
end
|
||||
|
||||
def no_projects
|
||||
errors.add(:deploy_token, 'cannot have projects assigned') if project_deploy_tokens.any?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GroupDeployToken < ApplicationRecord
|
||||
belongs_to :group, class_name: '::Group'
|
||||
belongs_to :deploy_token, inverse_of: :group_deploy_tokens
|
||||
|
||||
validates :deploy_token, presence: true
|
||||
validates :group, presence: true
|
||||
validates :deploy_token_id, uniqueness: { scope: [:group_id] }
|
||||
|
||||
def has_access_to?(requested_project)
|
||||
return false unless Feature.enabled?(:allow_group_deploy_token, default: true)
|
||||
|
||||
requested_project_group = requested_project&.group
|
||||
return false unless requested_project_group
|
||||
return true if requested_project_group.id == group_id
|
||||
|
||||
requested_project_group
|
||||
.ancestors
|
||||
.where(id: group_id)
|
||||
.exists?
|
||||
end
|
||||
end
|
||||
|
|
@ -7,4 +7,8 @@ class ProjectDeployToken < ApplicationRecord
|
|||
validates :deploy_token, presence: true
|
||||
validates :project, presence: true
|
||||
validates :deploy_token_id, uniqueness: { scope: [:project_id] }
|
||||
|
||||
def has_access_to?(requested_project)
|
||||
requested_project == project
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ class User < ApplicationRecord
|
|||
|
||||
MINIMUM_INACTIVE_DAYS = 180
|
||||
|
||||
enum bot_type: ::UserBotTypeEnums.bots
|
||||
|
||||
# Override Devise::Models::Trackable#update_tracked_fields!
|
||||
# to limit database writes to at most once every hour
|
||||
# rubocop: disable CodeReuse/ServiceClass
|
||||
|
|
@ -246,6 +248,7 @@ class User < ApplicationRecord
|
|||
delegate :time_display_relative, :time_display_relative=, to: :user_preference
|
||||
delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference
|
||||
delegate :show_whitespace_in_diffs, :show_whitespace_in_diffs=, to: :user_preference
|
||||
delegate :tab_width, :tab_width=, to: :user_preference
|
||||
delegate :sourcegraph_enabled, :sourcegraph_enabled=, to: :user_preference
|
||||
delegate :setup_for_company, :setup_for_company=, to: :user_preference
|
||||
delegate :render_whitespace_in_code, :render_whitespace_in_code=, to: :user_preference
|
||||
|
|
@ -322,6 +325,8 @@ class User < ApplicationRecord
|
|||
scope :with_emails, -> { preload(:emails) }
|
||||
scope :with_dashboard, -> (dashboard) { where(dashboard: dashboard) }
|
||||
scope :with_public_profile, -> { where(private_profile: false) }
|
||||
scope :bots, -> { where.not(bot_type: nil) }
|
||||
scope :humans, -> { where(bot_type: nil) }
|
||||
|
||||
scope :with_expiring_and_not_notified_personal_access_tokens, ->(at) do
|
||||
where('EXISTS (?)',
|
||||
|
|
@ -598,6 +603,15 @@ class User < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def alert_bot
|
||||
email_pattern = "alert%s@#{Settings.gitlab.host}"
|
||||
|
||||
unique_internal(where(bot_type: :alert_bot), 'alert-bot', email_pattern) do |u|
|
||||
u.bio = 'The GitLab alert bot'
|
||||
u.name = 'GitLab Alert Bot'
|
||||
end
|
||||
end
|
||||
|
||||
# Return true if there is only single non-internal user in the deployment,
|
||||
# ghost user is ignored.
|
||||
def single_user?
|
||||
|
|
@ -613,16 +627,20 @@ class User < ApplicationRecord
|
|||
username
|
||||
end
|
||||
|
||||
def bot?
|
||||
bot_type.present?
|
||||
end
|
||||
|
||||
def internal?
|
||||
ghost?
|
||||
ghost? || bot?
|
||||
end
|
||||
|
||||
def self.internal
|
||||
where(ghost: true)
|
||||
where(ghost: true).or(bots)
|
||||
end
|
||||
|
||||
def self.non_internal
|
||||
without_ghosts
|
||||
without_ghosts.humans
|
||||
end
|
||||
|
||||
#
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module UserBotTypeEnums
|
||||
def self.bots
|
||||
# When adding a new key, please ensure you are not conflicting with EE-only keys in app/models/user_bot_types_enums.rb
|
||||
{
|
||||
alert_bot: 2
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
UserBotTypeEnums.prepend_if_ee('EE::UserBotTypeEnums')
|
||||
|
|
@ -9,7 +9,13 @@ class UserPreference < ApplicationRecord
|
|||
belongs_to :user
|
||||
|
||||
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
|
||||
validates :tab_width, numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: Gitlab::TabWidth::MIN,
|
||||
less_than_or_equal_to: Gitlab::TabWidth::MAX
|
||||
}
|
||||
|
||||
default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
|
||||
default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false
|
||||
default_value_for :time_display_relative, value: true, allows_nil: false
|
||||
default_value_for :time_format_in_24h, value: false, allows_nil: false
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ class BasePolicy < DeclarativePolicy::Base
|
|||
::Gitlab::ExternalAuthorization.perform_check?
|
||||
end
|
||||
|
||||
with_options scope: :user, score: 0
|
||||
condition(:alert_bot) { @user&.alert_bot? }
|
||||
|
||||
rule { external_authorization_enabled & ~can?(:read_all_resources) }.policy do
|
||||
prevent :read_cross_project
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ module PolicyActor
|
|||
def can_create_group
|
||||
false
|
||||
end
|
||||
|
||||
def alert_bot?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
PolicyActor.prepend_if_ee('EE::PolicyActor')
|
||||
|
|
|
|||
|
|
@ -515,6 +515,8 @@ class ProjectPolicy < BasePolicy
|
|||
end
|
||||
|
||||
def lookup_access_level!
|
||||
return ::Gitlab::Access::REPORTER if alert_bot?
|
||||
|
||||
# NOTE: max_member_access has its own cache
|
||||
project.team.max_member_access(@user.id)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
!!! 5
|
||||
%html{ lang: I18n.locale, class: page_classes }
|
||||
= render "layouts/head"
|
||||
%body{ class: "#{user_application_theme} #{@body_class} #{client_class_list}", data: body_data }
|
||||
%body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} #{client_class_list}", data: body_data }
|
||||
= render "layouts/init_auto_complete" if @gfm_form
|
||||
= render "layouts/init_client_detection_flags"
|
||||
= render 'peek/bar'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
!!! 5
|
||||
%html{ lang: I18n.locale, class: page_class }
|
||||
= render "layouts/head"
|
||||
%body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
|
||||
%body{ class: "#{user_application_theme} #{user_tab_width} #{@body_class} fullscreen-layout", data: { page: body_data_page } }
|
||||
= render 'peek/bar'
|
||||
= header_message
|
||||
= render partial: "layouts/header/default", locals: { project: @project, group: @group }
|
||||
|
|
|
|||
|
|
@ -69,6 +69,15 @@
|
|||
= f.check_box :show_whitespace_in_diffs, class: 'form-check-input'
|
||||
= f.label :show_whitespace_in_diffs, class: 'form-check-label' do
|
||||
= s_('Preferences|Show whitespace changes in diffs')
|
||||
.form-group
|
||||
= f.label :tab_width, s_('Preferences|Tab width'), class: 'label-bold'
|
||||
= f.number_field :tab_width,
|
||||
class: 'form-control',
|
||||
min: Gitlab::TabWidth::MIN,
|
||||
max: Gitlab::TabWidth::MAX,
|
||||
required: true
|
||||
.form-text.text-muted
|
||||
= s_('Preferences|Must be a number between %{min} and %{max}') % { min: Gitlab::TabWidth::MIN, max: Gitlab::TabWidth::MAX }
|
||||
|
||||
.col-sm-12
|
||||
%hr
|
||||
|
|
|
|||
|
|
@ -12,5 +12,9 @@ if ('<%= current_user.layout %>' === 'fluid') {
|
|||
// Re-enable the "Save" button
|
||||
$('input[type=submit]').enable()
|
||||
|
||||
// Show the notice flash message
|
||||
new Flash('<%= flash.discard(:notice) %>', 'notice')
|
||||
// Show flash messages
|
||||
<% if flash.notice %>
|
||||
new Flash('<%= flash.discard(:notice) %>', 'notice')
|
||||
<% elsif flash.alert %>
|
||||
new Flash('<%= flash.discard(:alert) %>', 'alert')
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
= render "projects/blob/auxiliary_viewer", blob: blob
|
||||
|
||||
#blob-content-holder.blob-content-holder
|
||||
- if native_code_navigation_enabled?(@project)
|
||||
#js-code-navigation{ data: { commit_id: blob.commit_id, path: blob.path, project_path: @project.full_path } }
|
||||
%article.file-holder
|
||||
= render 'projects/blob/header', blob: blob
|
||||
= render 'projects/blob/content', blob: blob
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add tab width option to user preferences
|
||||
merge_request: 22063
|
||||
author: Alexander Oleynikov
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix issue count wrapping on board list
|
||||
merge_request:
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update deploy token architecture to introduce group-level deploy tokens.
|
||||
merge_request: 23460
|
||||
author:
|
||||
type: added
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Label MR test modal execution time as seconds
|
||||
merge_request: 24019
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix JIRA DVCS retrieving repositories
|
||||
merge_request: 23180
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add index to audit_events (entity_id, entity_type, id)
|
||||
merge_request: 23998
|
||||
author:
|
||||
type: performance
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix duplicated user popovers
|
||||
merge_request: 24405
|
||||
author:
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix quoted-printable encoding for unicode and newlines in mails
|
||||
merge_request: 24153
|
||||
author: Diego Louzán
|
||||
type: fixed
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
# Monkey patch mail 2.7.1 to fix quoted-printable issues with newlines
|
||||
# The issues upstream invalidate SMIME signatures under some conditions
|
||||
# This was working properly in 2.6.6
|
||||
#
|
||||
# See https://gitlab.com/gitlab-org/gitlab/issues/197386
|
||||
# See https://github.com/mikel/mail/issues/1190
|
||||
|
||||
module Mail
|
||||
module Encodings
|
||||
# PATCH
|
||||
# This reverts https://github.com/mikel/mail/pull/1113, which solves some
|
||||
# encoding issues with binary attachments encoded in quoted-printable, but
|
||||
# unfortunately breaks re-encoding of messages
|
||||
class QuotedPrintable < SevenBit
|
||||
def self.decode(str)
|
||||
::Mail::Utilities.to_lf str.gsub(/(?:=0D=0A|=0D|=0A)\r\n/, "\r\n").unpack1("M*")
|
||||
end
|
||||
|
||||
def self.encode(str)
|
||||
::Mail::Utilities.to_crlf([::Mail::Utilities.to_lf(str)].pack("M"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Body
|
||||
def encoded(transfer_encoding = nil, charset = nil)
|
||||
# PATCH
|
||||
# Use provided parameter charset (from parent Message) if not nil,
|
||||
# otherwise use own self.charset
|
||||
# Required because the Message potentially has on its headers the charset
|
||||
# that needs to be used (e.g. 'Content-Type: text/plain; charset=UTF-8')
|
||||
charset = self.charset if charset.nil?
|
||||
|
||||
if multipart?
|
||||
self.sort_parts!
|
||||
encoded_parts = parts.map { |p| p.encoded }
|
||||
([preamble] + encoded_parts).join(crlf_boundary) + end_boundary + epilogue.to_s
|
||||
else
|
||||
dec = Mail::Encodings.get_encoding(encoding)
|
||||
enc = if Utilities.blank?(transfer_encoding)
|
||||
dec
|
||||
else
|
||||
negotiate_best_encoding(transfer_encoding)
|
||||
end
|
||||
|
||||
if dec.nil?
|
||||
# Cannot decode, so skip normalization
|
||||
raw_source
|
||||
else
|
||||
# Decode then encode to normalize and allow transforming
|
||||
# from base64 to Q-P and vice versa
|
||||
decoded = dec.decode(raw_source)
|
||||
|
||||
if defined?(Encoding) && charset && charset != "US-ASCII"
|
||||
# PATCH
|
||||
# We need to force the encoding: in the case of quoted-printable
|
||||
# this will throw an exception otherwise, because `decoded` will have
|
||||
# an encoding of BINARY (or its equivalent ASCII-8BIT),
|
||||
# coming from QuotedPrintable#decode, and inside it from String#unpack1
|
||||
decoded = decoded.force_encoding(charset)
|
||||
decoded.force_encoding('BINARY') unless Encoding.find(charset).ascii_compatible?
|
||||
end
|
||||
|
||||
enc.encode(decoded)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Message
|
||||
def encoded
|
||||
ready_to_send!
|
||||
buffer = header.encoded
|
||||
buffer << "\r\n"
|
||||
# PATCH
|
||||
# Pass the Message charset down to the contained Body, the headers
|
||||
# potentially contain the charset needed to be applied
|
||||
buffer << body.encoded(content_transfer_encoding, charset)
|
||||
buffer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSamlProviderProhibitedOuterForks < ActiveRecord::Migration[5.2]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_column_with_default :saml_providers, :prohibited_outer_forks, :boolean, default: false, allow_null: true
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :saml_providers, :prohibited_outer_forks
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTabWidthToUserPreferences < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column(:user_preferences, :tab_width, :integer, limit: 2)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateGroupDeployTokens < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
create_table :group_deploy_tokens do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
|
||||
t.references :group, index: false, null: false, foreign_key: { to_table: :namespaces, on_delete: :cascade }
|
||||
t.references :deploy_token, null: false, foreign_key: { on_delete: :cascade }
|
||||
|
||||
t.index [:group_id, :deploy_token_id], unique: true, name: 'index_group_deploy_tokens_on_group_and_deploy_token_ids'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexOnAuditEventsIdDesc < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
OLD_INDEX_NAME = 'index_audit_events_on_entity_id_and_entity_type'
|
||||
NEW_INDEX_NAME = 'index_audit_events_on_entity_id_and_entity_type_and_id_desc'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :audit_events, [:entity_id, :entity_type, :id], name: NEW_INDEX_NAME,
|
||||
order: { entity_id: :asc, entity_type: :asc, id: :desc }
|
||||
|
||||
remove_concurrent_index_by_name :audit_events, OLD_INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_index :audit_events, [:entity_id, :entity_type], name: OLD_INDEX_NAME
|
||||
|
||||
remove_concurrent_index_by_name :audit_events, NEW_INDEX_NAME
|
||||
end
|
||||
end
|
||||
15
db/schema.rb
15
db/schema.rb
|
|
@ -465,7 +465,7 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
|
|||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.index ["created_at", "author_id"], name: "analytics_index_audit_events_on_created_at_and_author_id"
|
||||
t.index ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type"
|
||||
t.index ["entity_id", "entity_type", "id"], name: "index_audit_events_on_entity_id_and_entity_type_and_id_desc", order: { id: :desc }
|
||||
end
|
||||
|
||||
create_table "award_emoji", id: :serial, force: :cascade do |t|
|
||||
|
|
@ -1979,6 +1979,15 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
|
|||
t.index ["user_id"], name: "index_group_deletion_schedules_on_user_id"
|
||||
end
|
||||
|
||||
create_table "group_deploy_tokens", force: :cascade do |t|
|
||||
t.datetime_with_timezone "created_at", null: false
|
||||
t.datetime_with_timezone "updated_at", null: false
|
||||
t.bigint "group_id", null: false
|
||||
t.bigint "deploy_token_id", null: false
|
||||
t.index ["deploy_token_id"], name: "index_group_deploy_tokens_on_deploy_token_id"
|
||||
t.index ["group_id", "deploy_token_id"], name: "index_group_deploy_tokens_on_group_and_deploy_token_ids", unique: true
|
||||
end
|
||||
|
||||
create_table "group_group_links", force: :cascade do |t|
|
||||
t.datetime_with_timezone "created_at", null: false
|
||||
t.datetime_with_timezone "updated_at", null: false
|
||||
|
|
@ -3735,6 +3744,7 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
|
|||
t.string "sso_url", null: false
|
||||
t.boolean "enforced_sso", default: false, null: false
|
||||
t.boolean "enforced_group_managed_accounts", default: false, null: false
|
||||
t.boolean "prohibited_outer_forks", default: false, null: false
|
||||
t.index ["group_id"], name: "index_saml_providers_on_group_id"
|
||||
end
|
||||
|
||||
|
|
@ -4133,6 +4143,7 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
|
|||
t.boolean "sourcegraph_enabled"
|
||||
t.boolean "setup_for_company"
|
||||
t.boolean "render_whitespace_in_code"
|
||||
t.integer "tab_width", limit: 2
|
||||
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true
|
||||
end
|
||||
|
||||
|
|
@ -4691,6 +4702,8 @@ ActiveRecord::Schema.define(version: 2020_02_04_131054) do
|
|||
add_foreign_key "group_custom_attributes", "namespaces", column: "group_id", on_delete: :cascade
|
||||
add_foreign_key "group_deletion_schedules", "namespaces", column: "group_id", on_delete: :cascade
|
||||
add_foreign_key "group_deletion_schedules", "users", name: "fk_11e3ebfcdd", on_delete: :cascade
|
||||
add_foreign_key "group_deploy_tokens", "deploy_tokens", on_delete: :cascade
|
||||
add_foreign_key "group_deploy_tokens", "namespaces", column: "group_id", on_delete: :cascade
|
||||
add_foreign_key "group_group_links", "namespaces", column: "shared_group_id", on_delete: :cascade
|
||||
add_foreign_key "group_group_links", "namespaces", column: "shared_with_group_id", on_delete: :cascade
|
||||
add_foreign_key "identities", "saml_providers", name: "fk_aade90f0fc", on_delete: :cascade
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ Parameters:
|
|||
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
|
||||
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (Maintainers), or `developer` (Developers + Maintainers). |
|
||||
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
|
||||
| `subgroup_creation_level` | integer | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
|
||||
| `subgroup_creation_level` | string | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
|
||||
| `emails_disabled` | boolean | no | Disable email notifications |
|
||||
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
|
||||
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
|
||||
|
|
@ -533,7 +533,7 @@ PUT /groups/:id
|
|||
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
|
||||
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (Maintainers), or `developer` (Developers + Maintainers). |
|
||||
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
|
||||
| `subgroup_creation_level` | integer | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
|
||||
| `subgroup_creation_level` | string | no | Allowed to create subgroups. Can be `owner` (Owners), or `maintainer` (Maintainers). |
|
||||
| `emails_disabled` | boolean | no | Disable email notifications |
|
||||
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
|
||||
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
|
||||
|
|
|
|||
|
|
@ -1970,7 +1970,7 @@ job:
|
|||
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
|
||||
|
||||
`expire_in` allows you to specify how long artifacts should live before they
|
||||
expire and therefore deleted, counting from the time they are uploaded and
|
||||
expire and are therefore deleted, counting from the time they are uploaded and
|
||||
stored on GitLab. If the expiry time is not defined, it defaults to the
|
||||
[instance wide setting](../../user/admin_area/settings/continuous_integration.md#default-artifacts-expiration-core-only)
|
||||
(30 days by default, forever on GitLab.com).
|
||||
|
|
|
|||
|
|
@ -762,6 +762,39 @@ networkPolicy:
|
|||
app.gitlab.com/managed_by: gitlab
|
||||
```
|
||||
|
||||
#### Web Application Firewall (ModSecurity) customization
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/44) in GitLab 12.8.
|
||||
|
||||
Customization on an [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) or on a deployment base is available for clusters with [ModSecurity installed](../../user/clusters/applications.md#web-application-firewall-modsecurity).
|
||||
|
||||
To enable ModSecurity with Auto Deploy, you need to create a `.gitlab/auto-deploy-values.yaml` file in your project with the following attributes.
|
||||
|
||||
|Attribute | Description | Default |
|
||||
-----------|-------------|---------|
|
||||
|`enabled` | Enables custom configuration for modsecurity, defaulting to the [Core Rule Set](https://coreruleset.org/) | `false` |
|
||||
|`secRuleEngine` | Configures the [rules engine](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#secruleengine) | `DetectionOnly` |
|
||||
|`secRules` | Creates one or more additional [rule](https://github.com/SpiderLabs/ModSecurity/wiki/Reference-Manual-(v2.x)#SecRule) | `nil` |
|
||||
|
||||
In the following `auto-deploy-values.yaml` example, some custom settings
|
||||
are enabled for ModSecurity. Those include setting its engine to
|
||||
process rules instead of only logging them, while adding two specific
|
||||
rules which are header-based:
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
modSecurity:
|
||||
enabled: true
|
||||
secRuleEngine: "On"
|
||||
secRules:
|
||||
- variable: "REQUEST_HEADERS:User-Agent"
|
||||
operator: "printer"
|
||||
action: "log,deny,id:'2010',status:403,msg:'printer is an invalid agent'"
|
||||
- variable: "REQUEST_HEADERS:Content-Type"
|
||||
operator: "text/plain"
|
||||
action: "log,deny,id:'2011',status:403,msg:'Text is not supported as content type'"
|
||||
```
|
||||
|
||||
#### Running commands in the container
|
||||
|
||||
Applications built with [Auto Build](#auto-build) using Herokuish, the default
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
The Dependency list allows you to see your project's dependencies, and key
|
||||
details about them, including their known vulnerabilities. To see it,
|
||||
navigate to **Security & Compliance > Dependency List** in your project's
|
||||
sidebar.
|
||||
sidebar. This information is sometimes referred to as a Software Bill of Materials or SBoM / BOM.
|
||||
|
||||
## Requirements
|
||||
|
||||
|
|
|
|||
|
|
@ -454,6 +454,12 @@ CI/CD configuration file to turn it on. Results are available in the SAST report
|
|||
|
||||
GitLab currently includes [Gitleaks](https://github.com/zricethezav/gitleaks) and [TruffleHog](https://github.com/dxa4481/truffleHog) checks.
|
||||
|
||||
NOTE: **Note:**
|
||||
The secrets analyzer will ignore "Password in URL" vulnerabilities if the password begins
|
||||
with a dollar sign (`$`) as this likely indicates the password being used is an environment
|
||||
variable. For example, `https://username:$password@example.com/path/to/repo` will not be
|
||||
detected, whereas `https://username:password@example.com/path/to/repo` would be detected.
|
||||
|
||||
## Security Dashboard
|
||||
|
||||
The Security Dashboard is a good place to get an overview of all the security
|
||||
|
|
|
|||
|
|
@ -108,6 +108,15 @@ You can choose between 3 options:
|
|||
- Readme
|
||||
- Activity
|
||||
|
||||
### Tab width
|
||||
|
||||
You can set the displayed width of tab characters across various parts of
|
||||
GitLab, for example, blobs, diffs, and snippets.
|
||||
|
||||
NOTE: **Note:**
|
||||
Some parts of GitLab do not respect this setting, including the WebIDE, file
|
||||
editor and Markdown editor.
|
||||
|
||||
## Localization
|
||||
|
||||
### Language
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ GitLab provides an easy way to connect Sentry to your project:
|
|||
Make sure to give the token at least the following scopes: `event:read` and `project:read`.
|
||||
1. Navigate to your project’s **Settings > Operations**.
|
||||
1. Ensure that the **Active** checkbox is set.
|
||||
1. In the **Sentry API URL** field, enter your Sentry hostname. For example, `https://sentry.example.com`.
|
||||
1. In the **Sentry API URL** field, enter your Sentry hostname. For example, enter `https://sentry.example.com` if this is the address at which your Sentry instance is available. For the SaaS version of Sentry, the hostname will be `https://sentry.io`.
|
||||
1. In the **Auth Token** field, enter the token you previously generated.
|
||||
1. Click the **Connect** button to test the connection to Sentry and populate the **Project** dropdown.
|
||||
1. From the **Project** dropdown, choose a Sentry project to link to your GitLab project.
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ module Gitlab
|
|||
lfs_token_check(login, password, project) ||
|
||||
oauth_access_token_check(login, password) ||
|
||||
personal_access_token_check(password) ||
|
||||
deploy_token_check(login, password) ||
|
||||
deploy_token_check(login, password, project) ||
|
||||
user_with_password_for_git(login, password) ||
|
||||
Gitlab::Auth::Result.new
|
||||
|
||||
|
|
@ -208,7 +208,7 @@ module Gitlab
|
|||
end.uniq
|
||||
end
|
||||
|
||||
def deploy_token_check(login, password)
|
||||
def deploy_token_check(login, password, project)
|
||||
return unless password.present?
|
||||
|
||||
token = DeployToken.active.find_by_token(password)
|
||||
|
|
@ -219,7 +219,7 @@ module Gitlab
|
|||
scopes = abilities_for_scopes(token.scopes)
|
||||
|
||||
if valid_scoped_token?(token, all_available_scopes)
|
||||
Gitlab::Auth::Result.new(token, token.project, :deploy_token, scopes)
|
||||
Gitlab::Auth::Result.new(token, project, :deploy_token, scopes)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ module Gitlab
|
|||
cert: certificate.cert,
|
||||
key: certificate.key,
|
||||
data: message.encoded)
|
||||
|
||||
signed_email = Mail.new(signed_message)
|
||||
|
||||
overwrite_body(message, signed_email)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module TabWidth
|
||||
extend self
|
||||
|
||||
MIN = 1
|
||||
MAX = 12
|
||||
DEFAULT = 8
|
||||
|
||||
def css_class_for_user(user)
|
||||
return css_class_for_value(DEFAULT) unless user
|
||||
|
||||
css_class_for_value(user.tab_width)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def css_class_for_value(value)
|
||||
raise ArgumentError unless in_range?(value)
|
||||
|
||||
"tab-width-#{value}"
|
||||
end
|
||||
|
||||
def in_range?(value)
|
||||
(MIN..MAX).cover?(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -484,7 +484,7 @@ msgstr ""
|
|||
msgid "%{username}'s avatar"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{value} ms"
|
||||
msgid "%{value} s"
|
||||
msgstr ""
|
||||
|
||||
msgid "%{verb} %{time_spent_value} spent time."
|
||||
|
|
@ -1670,6 +1670,9 @@ msgstr ""
|
|||
msgid "An error occurred fetching the dropdown data."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred loading code navigation"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred previewing the blob"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -9297,6 +9300,9 @@ msgstr ""
|
|||
msgid "Go to commits"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to definition"
|
||||
msgstr ""
|
||||
|
||||
msgid "Go to environments"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13963,6 +13969,9 @@ msgstr ""
|
|||
msgid "Preferences|Layout width"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Must be a number between %{min} and %{max}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Navigation theme"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -13981,6 +13990,9 @@ msgstr ""
|
|||
msgid "Preferences|Syntax highlighting theme"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|Tab width"
|
||||
msgstr ""
|
||||
|
||||
msgid "Preferences|These settings will update how dates and times are displayed for you."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19227,6 +19239,9 @@ msgstr ""
|
|||
msgid "This GitLab instance is licensed at the %{insufficient_license} tier. Geo is only available for users who have at least a Premium license."
|
||||
msgstr ""
|
||||
|
||||
msgid "This Project is currently archived and read-only. Please unarchive the project first if you want to resume Pull mirroring"
|
||||
msgstr ""
|
||||
|
||||
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ module QA
|
|||
context 'Verify', :docker do
|
||||
describe 'Pipeline creation and processing' do
|
||||
let(:executor) { "qa-runner-#{Time.now.to_i}" }
|
||||
let(:max_wait) { 30 }
|
||||
|
||||
let(:project) do
|
||||
Resource::Project.fabricate_via_api! do |project|
|
||||
|
|
@ -68,11 +69,11 @@ module QA
|
|||
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
|
||||
|
||||
Page::Project::Pipeline::Show.perform do |pipeline|
|
||||
expect(pipeline).to be_running(wait: 30)
|
||||
expect(pipeline).to have_build('test-success', status: :success)
|
||||
expect(pipeline).to have_build('test-failure', status: :failed)
|
||||
expect(pipeline).to have_build('test-tags', status: :pending)
|
||||
expect(pipeline).to have_build('test-artifacts', status: :success)
|
||||
expect(pipeline).to be_running(wait: max_wait)
|
||||
expect(pipeline).to have_build('test-success', status: :success, wait: max_wait)
|
||||
expect(pipeline).to have_build('test-failure', status: :failed, wait: max_wait)
|
||||
expect(pipeline).to have_build('test-tags', status: :pending, wait: max_wait)
|
||||
expect(pipeline).to have_build('test-artifacts', status: :success, wait: max_wait)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ describe Profiles::PreferencesController do
|
|||
theme_id: '2',
|
||||
first_day_of_week: '1',
|
||||
preferred_language: 'jp',
|
||||
tab_width: '5',
|
||||
render_whitespace_in_code: 'true'
|
||||
}.with_indifferent_access
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ FactoryBot.define do
|
|||
read_registry { true }
|
||||
revoked { false }
|
||||
expires_at { 5.days.from_now }
|
||||
deploy_token_type { DeployToken.deploy_token_types[:project_type] }
|
||||
|
||||
trait :revoked do
|
||||
revoked { true }
|
||||
|
|
@ -21,5 +22,13 @@ FactoryBot.define do
|
|||
trait :expired do
|
||||
expires_at { Date.today - 1.month }
|
||||
end
|
||||
|
||||
trait :group do
|
||||
deploy_token_type { DeployToken.deploy_token_types[:group_type] }
|
||||
end
|
||||
|
||||
trait :project do
|
||||
deploy_token_type { DeployToken.deploy_token_types[:project_type] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :group_deploy_token do
|
||||
group
|
||||
deploy_token
|
||||
end
|
||||
end
|
||||
|
|
@ -23,6 +23,10 @@ FactoryBot.define do
|
|||
after(:build) { |user, _| user.block! }
|
||||
end
|
||||
|
||||
trait :bot do
|
||||
bot_type { User.bot_types[:alert_bot] }
|
||||
end
|
||||
|
||||
trait :external do
|
||||
external { true }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,53 +3,53 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'Group navbar' do
|
||||
it_behaves_like 'verified navigation bar' do
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
let(:user) { create(:user) }
|
||||
let(:group) { create(:group) }
|
||||
|
||||
let(:analytics_nav_item) do
|
||||
{
|
||||
nav_item: _('Analytics'),
|
||||
nav_sub_items: [
|
||||
_('Contribution Analytics')
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
let(:structure) do
|
||||
[
|
||||
{
|
||||
nav_item: _('Group overview'),
|
||||
nav_sub_items: [
|
||||
_('Details'),
|
||||
_('Activity')
|
||||
]
|
||||
},
|
||||
{
|
||||
nav_item: _('Issues'),
|
||||
nav_sub_items: [
|
||||
_('List'),
|
||||
_('Board'),
|
||||
_('Labels'),
|
||||
_('Milestones')
|
||||
]
|
||||
},
|
||||
{
|
||||
nav_item: _('Merge Requests'),
|
||||
nav_sub_items: []
|
||||
},
|
||||
{
|
||||
nav_item: _('Kubernetes'),
|
||||
nav_sub_items: []
|
||||
},
|
||||
(analytics_nav_item if Gitlab.ee?),
|
||||
{
|
||||
nav_item: _('Members'),
|
||||
nav_sub_items: []
|
||||
}
|
||||
let(:analytics_nav_item) do
|
||||
{
|
||||
nav_item: _('Analytics'),
|
||||
nav_sub_items: [
|
||||
_('Contribution Analytics')
|
||||
]
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
let(:structure) do
|
||||
[
|
||||
{
|
||||
nav_item: _('Group overview'),
|
||||
nav_sub_items: [
|
||||
_('Details'),
|
||||
_('Activity')
|
||||
]
|
||||
},
|
||||
{
|
||||
nav_item: _('Issues'),
|
||||
nav_sub_items: [
|
||||
_('List'),
|
||||
_('Board'),
|
||||
_('Labels'),
|
||||
_('Milestones')
|
||||
]
|
||||
},
|
||||
{
|
||||
nav_item: _('Merge Requests'),
|
||||
nav_sub_items: []
|
||||
},
|
||||
{
|
||||
nav_item: _('Kubernetes'),
|
||||
nav_sub_items: []
|
||||
},
|
||||
(analytics_nav_item if Gitlab.ee?),
|
||||
{
|
||||
nav_item: _('Members'),
|
||||
nav_sub_items: []
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
it_behaves_like 'verified navigation bar' do
|
||||
before do
|
||||
group.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
|
@ -57,4 +57,21 @@ describe 'Group navbar' do
|
|||
visit group_path(group)
|
||||
end
|
||||
end
|
||||
|
||||
if Gitlab.ee?
|
||||
context 'when productivity analytics is available' do
|
||||
before do
|
||||
stub_licensed_features(productivity_analytics: true)
|
||||
|
||||
analytics_nav_item[:nav_sub_items] << _('Productivity Analytics')
|
||||
|
||||
group.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
||||
visit group_path(group)
|
||||
end
|
||||
|
||||
it_behaves_like 'verified navigation bar'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(web_ide_default: false, single_mr_diff_view: false)
|
||||
stub_feature_flags(web_ide_default: false, single_mr_diff_view: false, code_navigation: false)
|
||||
|
||||
target_project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
|
|
|||
|
|
@ -29,4 +29,31 @@ describe 'User edit preferences profile' do
|
|||
|
||||
expect(field).not_to be_checked
|
||||
end
|
||||
|
||||
describe 'User changes tab width to acceptable value' do
|
||||
it 'shows success message' do
|
||||
fill_in 'Tab width', with: 9
|
||||
click_button 'Save changes'
|
||||
|
||||
expect(page).to have_content('Preferences saved.')
|
||||
end
|
||||
|
||||
it 'saves the value' do
|
||||
tab_width_field = page.find_field('Tab width')
|
||||
|
||||
expect do
|
||||
tab_width_field.fill_in with: 6
|
||||
click_button 'Save changes'
|
||||
end.to change { tab_width_field.value }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'User changes tab width to unacceptable value' do
|
||||
it 'shows error message' do
|
||||
fill_in 'Tab width', with: -1
|
||||
click_button 'Save changes'
|
||||
|
||||
expect(page).to have_content('Failed to save preferences')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ describe 'File blob', :js do
|
|||
wait_for_requests
|
||||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(code_navigation: false)
|
||||
end
|
||||
|
||||
context 'Ruby file' do
|
||||
before do
|
||||
visit_blob('files/ruby/popen.rb')
|
||||
|
|
|
|||
|
|
@ -69,6 +69,8 @@ describe 'Editing file blob', :js do
|
|||
|
||||
context 'from blob file path' do
|
||||
before do
|
||||
stub_feature_flags(code_navigation: false)
|
||||
|
||||
visit project_blob_path(project, tree_join(branch, file_path))
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ describe 'User creates blob in new project', :js do
|
|||
|
||||
shared_examples 'creating a file' do
|
||||
before do
|
||||
stub_feature_flags(code_navigation: false)
|
||||
sign_in(user)
|
||||
visit project_path(project)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ describe 'Projects > Files > User creates files', :js do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(web_ide_default: false)
|
||||
stub_feature_flags(web_ide_default: false, code_navigation: false)
|
||||
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ describe 'Projects > Files > User deletes files', :js do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(code_navigation: false)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ describe 'Projects > Files > User replaces files', :js do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_feature_flags(code_navigation: false)
|
||||
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Code navigation popover component renders popover 1`] = `
|
||||
<div
|
||||
class="popover code-navigation-popover popover-font-size-normal gl-popover bs-popover-bottom show"
|
||||
style="left: 0px; top: 0px;"
|
||||
>
|
||||
<div
|
||||
class="arrow"
|
||||
style="left: 0px;"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="border-bottom"
|
||||
>
|
||||
<pre
|
||||
class="border-0 bg-transparent m-0 code highlight"
|
||||
>
|
||||
console.log
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="popover-body"
|
||||
>
|
||||
<gl-button-stub
|
||||
class="w-100"
|
||||
href="http://test.com"
|
||||
size="md"
|
||||
target="_blank"
|
||||
variant="default"
|
||||
>
|
||||
|
||||
Go to definition
|
||||
|
||||
</gl-button-stub>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import Vuex from 'vuex';
|
||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import createState from '~/code_navigation/store/state';
|
||||
import App from '~/code_navigation/components/app.vue';
|
||||
import Popover from '~/code_navigation/components/popover.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
const fetchData = jest.fn();
|
||||
const showDefinition = jest.fn();
|
||||
let wrapper;
|
||||
|
||||
localVue.use(Vuex);
|
||||
|
||||
function factory(initialState = {}) {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
...createState(),
|
||||
...initialState,
|
||||
},
|
||||
actions: {
|
||||
fetchData,
|
||||
showDefinition,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = shallowMount(App, { store, localVue });
|
||||
}
|
||||
|
||||
describe('Code navigation app component', () => {
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('fetches data on mount', () => {
|
||||
factory();
|
||||
|
||||
expect(fetchData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('hides popover when no definition set', () => {
|
||||
factory();
|
||||
|
||||
expect(wrapper.find(Popover).exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders popover when definition set', () => {
|
||||
factory({
|
||||
currentDefinition: { hover: 'console' },
|
||||
currentDefinitionPosition: { x: 0 },
|
||||
});
|
||||
|
||||
expect(wrapper.find(Popover).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('calls showDefinition when clicking blob viewer', () => {
|
||||
setFixtures('<div class="blob-viewer"></div>');
|
||||
|
||||
factory();
|
||||
|
||||
document.querySelector('.blob-viewer').click();
|
||||
|
||||
expect(showDefinition).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Popover from '~/code_navigation/components/popover.vue';
|
||||
|
||||
const MOCK_CODE_DATA = Object.freeze({
|
||||
hover: [
|
||||
{
|
||||
language: 'javascript',
|
||||
value: 'console.log',
|
||||
},
|
||||
],
|
||||
definition_url: 'http://test.com',
|
||||
});
|
||||
|
||||
const MOCK_DOCS_DATA = Object.freeze({
|
||||
hover: [
|
||||
{
|
||||
language: null,
|
||||
value: 'console.log',
|
||||
},
|
||||
],
|
||||
definition_url: 'http://test.com',
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
|
||||
function factory(position, data) {
|
||||
wrapper = shallowMount(Popover, { propsData: { position, data } });
|
||||
}
|
||||
|
||||
describe('Code navigation popover component', () => {
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders popover', () => {
|
||||
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA);
|
||||
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('code output', () => {
|
||||
it('renders code output', () => {
|
||||
factory({ x: 0, y: 0, height: 0 }, MOCK_CODE_DATA);
|
||||
|
||||
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(true);
|
||||
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('documentation output', () => {
|
||||
it('renders code output', () => {
|
||||
factory({ x: 0, y: 0, height: 0 }, MOCK_DOCS_DATA);
|
||||
|
||||
expect(wrapper.find({ ref: 'code-output' }).exists()).toBe(false);
|
||||
expect(wrapper.find({ ref: 'doc-output' }).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import actions from '~/code_navigation/store/actions';
|
||||
import createFlash from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
|
||||
|
||||
jest.mock('~/flash');
|
||||
jest.mock('~/code_navigation/utils');
|
||||
|
||||
describe('Code navigation actions', () => {
|
||||
describe('setInitialData', () => {
|
||||
it('commits SET_INITIAL_DATA', done => {
|
||||
testAction(
|
||||
actions.setInitialData,
|
||||
{ projectPath: 'test' },
|
||||
{},
|
||||
[{ type: 'SET_INITIAL_DATA', payload: { projectPath: 'test' } }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestDataError', () => {
|
||||
it('commits REQUEST_DATA_ERROR', () =>
|
||||
testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []));
|
||||
|
||||
it('creates a flash message', () =>
|
||||
testAction(actions.requestDataError, null, {}, [{ type: 'REQUEST_DATA_ERROR' }], []).then(
|
||||
() => {
|
||||
expect(createFlash).toHaveBeenCalled();
|
||||
},
|
||||
));
|
||||
});
|
||||
|
||||
describe('fetchData', () => {
|
||||
let mock;
|
||||
const state = {
|
||||
projectPath: 'gitlab-org/gitlab',
|
||||
commitId: '123',
|
||||
blobPath: 'index',
|
||||
};
|
||||
const apiUrl = '/api/1/projects/gitlab-org%2Fgitlab/commits/123/lsif/info';
|
||||
|
||||
beforeEach(() => {
|
||||
window.gon = { api_version: '1' };
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(apiUrl).replyOnce(200, [
|
||||
{
|
||||
start_line: 0,
|
||||
start_char: 0,
|
||||
hover: { value: '123' },
|
||||
},
|
||||
{
|
||||
start_line: 1,
|
||||
start_char: 0,
|
||||
hover: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('commits REQUEST_DATA_SUCCESS with normalized data', done => {
|
||||
testAction(
|
||||
actions.fetchData,
|
||||
null,
|
||||
state,
|
||||
[
|
||||
{ type: 'REQUEST_DATA' },
|
||||
{
|
||||
type: 'REQUEST_DATA_SUCCESS',
|
||||
payload: { '0:0': { start_line: 0, start_char: 0, hover: { value: '123' } } },
|
||||
},
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('calls addInteractionClass with data', done => {
|
||||
testAction(
|
||||
actions.fetchData,
|
||||
null,
|
||||
state,
|
||||
[
|
||||
{ type: 'REQUEST_DATA' },
|
||||
{
|
||||
type: 'REQUEST_DATA_SUCCESS',
|
||||
payload: { '0:0': { start_line: 0, start_char: 0, hover: { value: '123' } } },
|
||||
},
|
||||
],
|
||||
[],
|
||||
)
|
||||
.then(() => {
|
||||
expect(addInteractionClass).toHaveBeenCalledWith({
|
||||
start_line: 0,
|
||||
start_char: 0,
|
||||
hover: { value: '123' },
|
||||
});
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(apiUrl).replyOnce(500);
|
||||
});
|
||||
|
||||
it('dispatches requestDataError', done => {
|
||||
testAction(
|
||||
actions.fetchData,
|
||||
null,
|
||||
state,
|
||||
[{ type: 'REQUEST_DATA' }],
|
||||
[{ type: 'requestDataError' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('showDefinition', () => {
|
||||
let target;
|
||||
|
||||
beforeEach(() => {
|
||||
target = document.createElement('div');
|
||||
});
|
||||
|
||||
it('returns early when no data exists', done => {
|
||||
testAction(actions.showDefinition, { target }, {}, [], [], done);
|
||||
});
|
||||
|
||||
it('commits SET_CURRENT_DEFINITION when target is not code navitation element', done => {
|
||||
testAction(
|
||||
actions.showDefinition,
|
||||
{ target },
|
||||
{ data: {} },
|
||||
[
|
||||
{
|
||||
type: 'SET_CURRENT_DEFINITION',
|
||||
payload: { definition: undefined, position: undefined },
|
||||
},
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('commits SET_CURRENT_DEFINITION with LSIF data', done => {
|
||||
target.classList.add('js-code-navigation');
|
||||
target.setAttribute('data-line-index', '0');
|
||||
target.setAttribute('data-char-index', '0');
|
||||
|
||||
testAction(
|
||||
actions.showDefinition,
|
||||
{ target },
|
||||
{ data: { '0:0': { hover: 'test' } } },
|
||||
[
|
||||
{
|
||||
type: 'SET_CURRENT_DEFINITION',
|
||||
payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
|
||||
},
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('adds hll class to target element', () => {
|
||||
target.classList.add('js-code-navigation');
|
||||
target.setAttribute('data-line-index', '0');
|
||||
target.setAttribute('data-char-index', '0');
|
||||
|
||||
return testAction(
|
||||
actions.showDefinition,
|
||||
{ target },
|
||||
{ data: { '0:0': { hover: 'test' } } },
|
||||
[
|
||||
{
|
||||
type: 'SET_CURRENT_DEFINITION',
|
||||
payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
|
||||
},
|
||||
],
|
||||
[],
|
||||
).then(() => {
|
||||
expect(target.classList).toContain('hll');
|
||||
});
|
||||
});
|
||||
|
||||
it('caches current target element', () => {
|
||||
target.classList.add('js-code-navigation');
|
||||
target.setAttribute('data-line-index', '0');
|
||||
target.setAttribute('data-char-index', '0');
|
||||
|
||||
return testAction(
|
||||
actions.showDefinition,
|
||||
{ target },
|
||||
{ data: { '0:0': { hover: 'test' } } },
|
||||
[
|
||||
{
|
||||
type: 'SET_CURRENT_DEFINITION',
|
||||
payload: { definition: { hover: 'test' }, position: { height: 0, x: 0, y: 0 } },
|
||||
},
|
||||
],
|
||||
[],
|
||||
).then(() => {
|
||||
expect(setCurrentHoverElement).toHaveBeenCalledWith(target);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import mutations from '~/code_navigation/store/mutations';
|
||||
import createState from '~/code_navigation/store/state';
|
||||
|
||||
let state;
|
||||
|
||||
describe('Code navigation mutations', () => {
|
||||
beforeEach(() => {
|
||||
state = createState();
|
||||
});
|
||||
|
||||
describe('SET_INITIAL_DATA', () => {
|
||||
it('sets initial data', () => {
|
||||
mutations.SET_INITIAL_DATA(state, {
|
||||
projectPath: 'test',
|
||||
commitId: '123',
|
||||
blobPath: 'index.js',
|
||||
});
|
||||
|
||||
expect(state.projectPath).toBe('test');
|
||||
expect(state.commitId).toBe('123');
|
||||
expect(state.blobPath).toBe('index.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_DATA', () => {
|
||||
it('sets loading true', () => {
|
||||
mutations.REQUEST_DATA(state);
|
||||
|
||||
expect(state.loading).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_DATA_SUCCESS', () => {
|
||||
it('sets loading false', () => {
|
||||
mutations.REQUEST_DATA_SUCCESS(state, ['test']);
|
||||
|
||||
expect(state.loading).toBe(false);
|
||||
});
|
||||
|
||||
it('sets data', () => {
|
||||
mutations.REQUEST_DATA_SUCCESS(state, ['test']);
|
||||
|
||||
expect(state.data).toEqual(['test']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REQUEST_DATA_ERROR', () => {
|
||||
it('sets loading false', () => {
|
||||
mutations.REQUEST_DATA_ERROR(state);
|
||||
|
||||
expect(state.loading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_CURRENT_DEFINITION', () => {
|
||||
it('sets current definition and position', () => {
|
||||
mutations.SET_CURRENT_DEFINITION(state, { definition: 'test', position: { x: 0 } });
|
||||
|
||||
expect(state.currentDefinition).toBe('test');
|
||||
expect(state.currentDefinitionPosition).toEqual({ x: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import {
|
||||
cachedData,
|
||||
getCurrentHoverElement,
|
||||
setCurrentHoverElement,
|
||||
addInteractionClass,
|
||||
} from '~/code_navigation/utils';
|
||||
|
||||
afterEach(() => {
|
||||
if (cachedData.has('current')) {
|
||||
cachedData.delete('current');
|
||||
}
|
||||
});
|
||||
|
||||
describe('getCurrentHoverElement', () => {
|
||||
it.each`
|
||||
value
|
||||
${'test'}
|
||||
${undefined}
|
||||
`('it returns cached current key', ({ value }) => {
|
||||
if (value) {
|
||||
cachedData.set('current', value);
|
||||
}
|
||||
|
||||
expect(getCurrentHoverElement()).toEqual(value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCurrentHoverElement', () => {
|
||||
it('sets cached current key', () => {
|
||||
setCurrentHoverElement('test');
|
||||
|
||||
expect(getCurrentHoverElement()).toEqual('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addInteractionClass', () => {
|
||||
beforeEach(() => {
|
||||
setFixtures(
|
||||
'<div id="LC1"><span>console</span><span>.</span><span>log</span></div><div id="LC2"><span>function</span></div>',
|
||||
);
|
||||
});
|
||||
|
||||
it.each`
|
||||
line | char | index
|
||||
${0} | ${0} | ${0}
|
||||
${0} | ${8} | ${2}
|
||||
${1} | ${0} | ${0}
|
||||
`(
|
||||
'it sets code navigation attributes for line $line and character $char',
|
||||
({ line, char, index }) => {
|
||||
addInteractionClass({ start_line: line, start_char: char });
|
||||
|
||||
expect(document.querySelectorAll(`#LC${line + 1} span`)[index].classList).toContain(
|
||||
'js-code-navigation',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
require 'mail'
|
||||
require_relative '../../config/initializers/mail_encoding_patch.rb'
|
||||
|
||||
describe 'Mail quoted-printable transfer encoding patch and Unicode characters' do
|
||||
shared_examples 'email encoding' do |email|
|
||||
it 'enclosing in a new object does not change the encoded original' do
|
||||
new_email = Mail.new(email)
|
||||
|
||||
expect(new_email.subject).to eq(email.subject)
|
||||
expect(new_email.from).to eq(email.from)
|
||||
expect(new_email.to).to eq(email.to)
|
||||
expect(new_email.content_type).to eq(email.content_type)
|
||||
expect(new_email.content_transfer_encoding).to eq(email.content_transfer_encoding)
|
||||
|
||||
expect(new_email.encoded).to eq(email.encoded)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a text email' do
|
||||
context 'with a body that encodes to exactly 74 characters (final newline)' do
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
content_type 'text/plain; charset=UTF-8'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-1\n"
|
||||
end
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
end
|
||||
|
||||
context 'with a body that encodes to exactly 74 characters (no final newline)' do
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
content_type 'text/plain; charset=UTF-8'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-12"
|
||||
end
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
end
|
||||
|
||||
context 'with a body that encodes to exactly 75 characters' do
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
content_type 'text/plain; charset=UTF-8'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "-123456789-123456789-123456789-123456789-123456789-123456789-123456789-12\n"
|
||||
end
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an html email' do
|
||||
context 'with a body that encodes to exactly 74 characters (final newline)' do
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
content_type 'text/html; charset=UTF-8'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-1234</p>\n"
|
||||
end
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
end
|
||||
|
||||
context 'with a body that encodes to exactly 74 characters (no final newline)' do
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
content_type 'text/html; charset=UTF-8'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-12345</p>"
|
||||
end
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
end
|
||||
|
||||
context 'with a body that encodes to exactly 75 characters' do
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
content_type 'text/html; charset=UTF-8'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "<p>-123456789-123456789-123456789-123456789-123456789-123456789-12345</p>\n"
|
||||
end
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
end
|
||||
end
|
||||
|
||||
context 'a multipart email' do
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
end
|
||||
|
||||
text_part = Mail::Part.new do
|
||||
content_type 'text/plain; charset=UTF-8'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "\r\n\r\n@john.doe, now known as John Dóe has accepted your invitation to join the Administrator / htmltest project.\r\n\r\nhttp://169.254.169.254:3000/root/htmltest\r\n\r\n-- \r\nYou're receiving this email because of your account on 169.254.169.254.\r\n\r\n\r\n\r\n"
|
||||
end
|
||||
|
||||
html_part = Mail::Part.new do
|
||||
content_type 'text/html; charset=UTF-8'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "\r\n\r\n@john.doe, now known as John Dóe has accepted your invitation to join the Administrator / htmltest project.\r\n\r\nhttp://169.254.169.254:3000/root/htmltest\r\n\r\n-- \r\nYou're receiving this email because of your account on 169.254.169.254.\r\n\r\n\r\n\r\n"
|
||||
end
|
||||
|
||||
email.text_part = text_part
|
||||
email.html_part = html_part
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
end
|
||||
|
||||
context 'with non UTF-8 charset' do
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
content_type 'text/plain; charset=windows-1251'
|
||||
content_transfer_encoding 'quoted-printable'
|
||||
body "This line is very long and will be put in multiple quoted-printable lines. Some Russian character: Д\n\n\n".encode('windows-1251')
|
||||
end
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
|
||||
it 'can be decoded back' do
|
||||
expect(Mail.new(email).body.decoded.dup.force_encoding('windows-1251').encode('utf-8')).to include('Some Russian character: Д')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with binary content' do
|
||||
context 'can be encoded with \'base64\' content-transfer-encoding' do
|
||||
image = File.binread('spec/fixtures/rails_sample.jpg')
|
||||
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
end
|
||||
|
||||
part = Mail::Part.new
|
||||
part.body = [image].pack('m')
|
||||
part.content_type = 'image/jpg'
|
||||
part.content_transfer_encoding = 'base64'
|
||||
|
||||
email.parts << part
|
||||
|
||||
it_behaves_like 'email encoding', email
|
||||
|
||||
it 'binary contents are not modified' do
|
||||
expect(email.parts.first.decoded).to eq(image)
|
||||
|
||||
# Enclosing in a new Mail object does not corrupt encoded data
|
||||
expect(Mail.new(email).parts.first.decoded).to eq(image)
|
||||
end
|
||||
end
|
||||
|
||||
context 'encoding fails with \'quoted-printable\' content-transfer-encoding' do
|
||||
image = File.binread('spec/fixtures/rails_sample.jpg')
|
||||
|
||||
email = Mail.new do
|
||||
to 'jane.doe@example.com'
|
||||
from 'John Dóe <john.doe@example.com>'
|
||||
subject 'Encoding tést'
|
||||
end
|
||||
|
||||
part = Mail::Part.new
|
||||
part.body = [image].pack('M*')
|
||||
part.content_type = 'image/jpg'
|
||||
part.content_transfer_encoding = 'quoted-printable'
|
||||
|
||||
email.parts << part
|
||||
|
||||
# The Mail patch in `config/initializers/mail_encoding_patch.rb` fixes
|
||||
# encoding of non-binary content. The failure below is expected since we
|
||||
# reverted some upstream changes in order to properly support SMIME signatures
|
||||
# See https://gitlab.com/gitlab-org/gitlab/issues/197386
|
||||
it 'content cannot be decoded back' do
|
||||
# Headers are ok
|
||||
expect(email.subject).to eq(email.subject)
|
||||
expect(email.from).to eq(email.from)
|
||||
expect(email.to).to eq(email.to)
|
||||
expect(email.content_type).to eq(email.content_type)
|
||||
expect(email.content_transfer_encoding).to eq(email.content_transfer_encoding)
|
||||
|
||||
# Content cannot be recovered
|
||||
expect(email.parts.first.decoded).not_to eq(image)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -42,8 +42,8 @@ describe('Grouped Test Reports Modal', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('renders miliseconds', () => {
|
||||
expect(vm.$el.textContent).toContain(`${modalDataStructure.execution_time.value} ms`);
|
||||
it('renders seconds', () => {
|
||||
expect(vm.$el.textContent).toContain(`${modalDataStructure.execution_time.value} s`);
|
||||
});
|
||||
|
||||
it('render title', () => {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,13 @@ describe('User Popovers', () => {
|
|||
expect(document.querySelectorAll(selector).length).toBe(popovers.length);
|
||||
});
|
||||
|
||||
it('does not initialize the user popovers twice for the same element', () => {
|
||||
const newPopovers = initUserPopovers(document.querySelectorAll(selector));
|
||||
const samePopovers = popovers.every((popover, index) => newPopovers[index] === popover);
|
||||
|
||||
expect(samePopovers).toBe(true);
|
||||
});
|
||||
|
||||
describe('when user link emits mouseenter event', () => {
|
||||
let userLink;
|
||||
|
||||
|
|
|
|||
|
|
@ -460,6 +460,20 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the deploy token is of group type' do
|
||||
let(:project_with_group) { create(:project, group: create(:group)) }
|
||||
let(:deploy_token) { create(:deploy_token, :group, read_repository: true, groups: [project_with_group.group]) }
|
||||
let(:login) { deploy_token.username }
|
||||
|
||||
subject { gl_auth.find_for_git_client(login, deploy_token.token, project: project_with_group, ip: 'ip') }
|
||||
|
||||
it 'succeeds when login and a group deploy token are valid' do
|
||||
auth_success = Gitlab::Auth::Result.new(deploy_token, project_with_group, :deploy_token, [:download_code, :read_container_image])
|
||||
|
||||
expect(subject).to eq(auth_success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the deploy token has read_registry as a scope' do
|
||||
let(:deploy_token) { create(:deploy_token, read_repository: false, projects: [project]) }
|
||||
let(:login) { deploy_token.username }
|
||||
|
|
@ -469,10 +483,10 @@ describe Gitlab::Auth, :use_clean_rails_memory_store_caching do
|
|||
stub_container_registry_config(enabled: true)
|
||||
end
|
||||
|
||||
it 'succeeds when login and token are valid' do
|
||||
it 'succeeds when login and a project token are valid' do
|
||||
auth_success = Gitlab::Auth::Result.new(deploy_token, project, :deploy_token, [:read_container_image])
|
||||
|
||||
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: nil, ip: 'ip'))
|
||||
expect(gl_auth.find_for_git_client(login, deploy_token.token, project: project, ip: 'ip'))
|
||||
.to eq(auth_success)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,14 @@ describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
|
|||
Gitlab::Email::Smime::Certificate.new(@cert[:key], @cert[:cert])
|
||||
end
|
||||
|
||||
let(:mail_body) { "signed hello with Unicode €áø and\r\n newlines\r\n" }
|
||||
|
||||
let(:mail) do
|
||||
ActionMailer::Base.mail(to: 'test@example.com', from: 'info@example.com', body: 'signed hello')
|
||||
ActionMailer::Base.mail(to: 'test@example.com',
|
||||
from: 'info@example.com',
|
||||
content_transfer_encoding: 'quoted-printable',
|
||||
content_type: 'text/plain; charset=UTF-8',
|
||||
body: mail_body)
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
@ -46,9 +52,16 @@ describe Gitlab::Email::Hook::SmimeSignatureInterceptor do
|
|||
ca_cert: root_certificate.cert,
|
||||
signed_data: mail.encoded)
|
||||
|
||||
# re-verify signature from a new Mail object content
|
||||
# See https://gitlab.com/gitlab-org/gitlab/issues/197386
|
||||
Gitlab::Email::Smime::Signer.verify_signature(
|
||||
cert: certificate.cert,
|
||||
ca_cert: root_certificate.cert,
|
||||
signed_data: Mail.new(mail).encoded)
|
||||
|
||||
# envelope in a Mail object and obtain the body
|
||||
decoded_mail = Mail.new(p7enc.data)
|
||||
|
||||
expect(decoded_mail.body.encoded).to eq('signed hello')
|
||||
expect(decoded_mail.body.decoded.dup.force_encoding(decoded_mail.charset)).to eq(mail_body)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'fast_spec_helper'
|
||||
|
||||
describe Gitlab::TabWidth, lib: true do
|
||||
describe '.css_class_for_user' do
|
||||
it 'returns default CSS class when user is nil' do
|
||||
css_class = described_class.css_class_for_user(nil)
|
||||
|
||||
expect(css_class).to eq('tab-width-8')
|
||||
end
|
||||
|
||||
it "returns CSS class for user's tab width", :aggregate_failures do
|
||||
[1, 6, 12].each do |i|
|
||||
user = double('user', tab_width: i)
|
||||
css_class = described_class.css_class_for_user(user)
|
||||
|
||||
expect(css_class).to eq("tab-width-#{i}")
|
||||
end
|
||||
end
|
||||
|
||||
it 'raises if tab width is out of valid range', :aggregate_failures do
|
||||
[0, 13, 'foo', nil].each do |i|
|
||||
expect do
|
||||
user = double('user', tab_width: i)
|
||||
described_class.css_class_for_user(user)
|
||||
end.to raise_error(ArgumentError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -7,6 +7,8 @@ describe DeployToken do
|
|||
|
||||
it { is_expected.to have_many :project_deploy_tokens }
|
||||
it { is_expected.to have_many(:projects).through(:project_deploy_tokens) }
|
||||
it { is_expected.to have_many :group_deploy_tokens }
|
||||
it { is_expected.to have_many(:groups).through(:group_deploy_tokens) }
|
||||
|
||||
it_behaves_like 'having unique enum values'
|
||||
|
||||
|
|
@ -17,6 +19,29 @@ describe DeployToken do
|
|||
it { is_expected.to allow_value('GitLab+deploy_token-3.14').for(:username) }
|
||||
it { is_expected.not_to allow_value('<script>').for(:username).with_message(username_format_message) }
|
||||
it { is_expected.not_to allow_value('').for(:username).with_message(username_format_message) }
|
||||
it { is_expected.to validate_presence_of(:deploy_token_type) }
|
||||
end
|
||||
|
||||
describe 'deploy_token_type validations' do
|
||||
context 'when a deploy token is associated to a group' do
|
||||
it 'does not allow setting a project to it' do
|
||||
group_token = create(:deploy_token, :group)
|
||||
group_token.projects << build(:project)
|
||||
|
||||
expect(group_token).not_to be_valid
|
||||
expect(group_token.errors.full_messages).to include('Deploy token cannot have projects assigned')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a deploy token is associated to a project' do
|
||||
it 'does not allow setting a group to it' do
|
||||
project_token = create(:deploy_token)
|
||||
project_token.groups << build(:group)
|
||||
|
||||
expect(project_token).not_to be_valid
|
||||
expect(project_token.errors.full_messages).to include('Deploy token cannot have groups assigned')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ensure_token' do
|
||||
|
|
@ -125,33 +150,148 @@ describe DeployToken do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#holder' do
|
||||
subject { deploy_token.holder }
|
||||
|
||||
context 'when the token is of project type' do
|
||||
it 'returns the relevant holder token' do
|
||||
expect(subject).to eq(deploy_token.project_deploy_tokens.first)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the token is of group type' do
|
||||
let(:group) { create(:group) }
|
||||
let(:deploy_token) { create(:deploy_token, :group) }
|
||||
|
||||
it 'returns the relevant holder token' do
|
||||
expect(subject).to eq(deploy_token.group_deploy_tokens.first)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#has_access_to?' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
subject { deploy_token.has_access_to?(project) }
|
||||
|
||||
context 'when deploy token is active and related to project' do
|
||||
let(:deploy_token) { create(:deploy_token, projects: [project]) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when deploy token is active but not related to project' do
|
||||
let(:deploy_token) { create(:deploy_token) }
|
||||
context 'when a project is not passed in' do
|
||||
let(:project) { nil }
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
|
||||
context 'when deploy token is revoked and related to project' do
|
||||
let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) }
|
||||
context 'when a project is passed in' do
|
||||
context 'when deploy token is active and related to project' do
|
||||
let(:deploy_token) { create(:deploy_token, projects: [project]) }
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'when deploy token is revoked and not related to the project' do
|
||||
let(:deploy_token) { create(:deploy_token, :revoked) }
|
||||
context 'when deploy token is active but not related to project' do
|
||||
let(:deploy_token) { create(:deploy_token) }
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
|
||||
context 'when deploy token is revoked and related to project' do
|
||||
let(:deploy_token) { create(:deploy_token, :revoked, projects: [project]) }
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
|
||||
context 'when deploy token is revoked and not related to the project' do
|
||||
let(:deploy_token) { create(:deploy_token, :revoked) }
|
||||
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
|
||||
context 'and when the token is of group type' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
let(:deploy_token) { create(:deploy_token, :group) }
|
||||
|
||||
before do
|
||||
deploy_token.groups << group
|
||||
end
|
||||
|
||||
context 'and the allow_group_deploy_token feature flag is turned off' do
|
||||
it 'is false' do
|
||||
stub_feature_flags(allow_group_deploy_token: false)
|
||||
|
||||
is_expected.to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the allow_group_deploy_token feature flag is turned on' do
|
||||
before do
|
||||
stub_feature_flags(allow_group_deploy_token: true)
|
||||
end
|
||||
|
||||
context 'and the passed-in project does not belong to any group' do
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
|
||||
context 'and the passed-in project belongs to the token group' do
|
||||
it 'is true' do
|
||||
group.projects << project
|
||||
|
||||
is_expected.to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the passed-in project belongs to a subgroup' do
|
||||
let(:child_group) { create(:group, parent_id: group.id) }
|
||||
let(:grandchild_group) { create(:group, parent_id: child_group.id) }
|
||||
|
||||
before do
|
||||
grandchild_group.projects << project
|
||||
end
|
||||
|
||||
context 'and the token group is an ancestor (grand-parent) of this group' do
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'and the token group is not ancestor of this group' do
|
||||
let(:child2_group) { create(:group, parent_id: group.id) }
|
||||
|
||||
it 'is false' do
|
||||
deploy_token.groups = [child2_group]
|
||||
|
||||
is_expected.to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the passed-in project does not belong to the token group' do
|
||||
it { is_expected.to be_falsy }
|
||||
end
|
||||
|
||||
context 'and the project belongs to a group that is parent of the token group' do
|
||||
let(:super_group) { create(:group) }
|
||||
let(:deploy_token) { create(:deploy_token, :group) }
|
||||
let(:group) { create(:group, parent_id: super_group.id) }
|
||||
|
||||
it 'is false' do
|
||||
super_group.projects << project
|
||||
|
||||
is_expected.to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'and the token is of project type' do
|
||||
let(:deploy_token) { create(:deploy_token, projects: [project]) }
|
||||
|
||||
context 'and the passed-in project is the same as the token project' do
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'and the passed-in project is not the same as the token project' do
|
||||
subject { deploy_token.has_access_to?(create(:project)) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -183,7 +323,7 @@ describe DeployToken do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when passign a value' do
|
||||
context 'when passing a value' do
|
||||
let(:expires_at) { Date.today + 5.months }
|
||||
let(:deploy_token) { create(:deploy_token, expires_at: expires_at) }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GroupDeployToken, type: :model do
|
||||
let(:group) { create(:group) }
|
||||
let(:deploy_token) { create(:deploy_token) }
|
||||
|
||||
subject(:group_deploy_token) { create(:group_deploy_token, group: group, deploy_token: deploy_token) }
|
||||
|
||||
it { is_expected.to belong_to :group }
|
||||
it { is_expected.to belong_to :deploy_token }
|
||||
|
||||
it { is_expected.to validate_presence_of :deploy_token }
|
||||
it { is_expected.to validate_presence_of :group }
|
||||
it { is_expected.to validate_uniqueness_of(:deploy_token_id).scoped_to(:group_id) }
|
||||
end
|
||||
|
|
@ -85,4 +85,19 @@ describe UserPreference do
|
|||
expect(user_preference.timezone).to eq(Time.zone.tzinfo.name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tab_width' do
|
||||
it 'is set to 8 by default' do
|
||||
# Intentionally not using factory here to test the constructor.
|
||||
pref = UserPreference.new
|
||||
expect(pref.tab_width).to eq(8)
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to validate_numericality_of(:tab_width)
|
||||
.only_integer
|
||||
.is_greater_than_or_equal_to(1)
|
||||
.is_less_than_or_equal_to(12)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ describe User, :do_not_mock_admin_mode do
|
|||
|
||||
describe 'delegations' do
|
||||
it { is_expected.to delegate_method(:path).to(:namespace).with_prefix }
|
||||
|
||||
it { is_expected.to delegate_method(:tab_width).to(:user_preference) }
|
||||
it { is_expected.to delegate_method(:tab_width=).to(:user_preference).with_arguments(5) }
|
||||
end
|
||||
|
||||
describe 'associations' do
|
||||
|
|
@ -4126,4 +4129,41 @@ describe User, :do_not_mock_admin_mode do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'internal methods' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let!(:ghost) { described_class.ghost }
|
||||
let!(:alert_bot) { described_class.alert_bot }
|
||||
let!(:non_internal) { [user] }
|
||||
let!(:internal) { [ghost, alert_bot] }
|
||||
|
||||
it 'returns non internal users' do
|
||||
expect(described_class.internal).to eq(internal)
|
||||
expect(internal.all?(&:internal?)).to eq(true)
|
||||
end
|
||||
|
||||
it 'returns internal users' do
|
||||
expect(described_class.non_internal).to eq(non_internal)
|
||||
expect(non_internal.all?(&:internal?)).to eq(false)
|
||||
end
|
||||
|
||||
describe '#bot?' do
|
||||
it 'marks bot users' do
|
||||
expect(user.bot?).to eq(false)
|
||||
expect(ghost.bot?).to eq(false)
|
||||
|
||||
expect(alert_bot.bot?).to eq(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'bots & humans' do
|
||||
it 'returns corresponding users' do
|
||||
human = create(:user)
|
||||
bot = create(:user, :bot)
|
||||
|
||||
expect(described_class.humans).to match_array([human])
|
||||
expect(described_class.bots).to match_array([bot])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -559,4 +559,18 @@ describe ProjectPolicy do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'alert bot' do
|
||||
let(:current_user) { User.alert_bot }
|
||||
|
||||
subject { described_class.new(current_user, project) }
|
||||
|
||||
it { is_expected.to be_allowed(:reporter_access) }
|
||||
|
||||
context 'within a private project' do
|
||||
let(:project) { create(:project, :private) }
|
||||
|
||||
it { is_expected.to be_allowed(:admin_issue) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in New Issue