Commit 5564275a authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent d8791851
......@@ -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
......
......@@ -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>
......
<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>
<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>
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);
},
});
};
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 });
},
};
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(),
});
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';
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;
},
};
export default () => ({
projectPath: null,
commitId: null,
blobPath: null,
loading: false,
data: null,
currentDefinition: null,
currentDefinitionPosition: null,
});
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">
......
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
# 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
#
......
# 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'
......
!!! 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
---
title: Add tab width option to user preferences
merge_request: 22063
author: Alexander Oleynikov
type: added
---
title: Fix issue count wrapping on board list
merge_request:
author:
type: fixed
---
title: Update deploy token architecture to introduce group-level deploy tokens.
merge_request: 23460
author:
type: added
---
title: Label MR test modal execution time as seconds
merge_request: 24019
author:
type: fixed
---
title: Fix JIRA DVCS retrieving repositories
merge_request: 23180
author:
type: fixed
---
title: Add index to audit_events (entity_id, entity_type, id)
merge_request: 23998
author:
type: performance
---
title: Fix duplicated user popovers
merge_request: 24405
author:
type: fixed
---
title: Fix quoted-printable encoding for unicode and newlines in mails
merge_request: 24153
author: Diego Louzán
type: fixed
# 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
# 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
# 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
# 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
# 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
......@@ -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)
......
# 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
# 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,7 +3,6 @@
require 'spec_helper'
describe 'Group navbar' do
it_behaves_like 'verified navigation bar' do
let(:user) { create(:user) }
let(:group) { create(:group) }
......@@ -50,11 +49,29 @@ describe 'Group navbar' do
]
end
it_behaves_like 'verified navigation bar' do
before do
group.add_maintainer(user)
sign_in(user)
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
......
// 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>
`;
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();
});
});
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);
});
});
});
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);
});
});
});
});
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 });
});
});
});
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',
);
},
);
});
# 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
# 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,11 +150,37 @@ 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 a project is not passed in' do
let(:project) { nil }
it { is_expected.to be_falsy }
end
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]) }
......@@ -153,6 +204,95 @@ describe DeployToken do
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
describe '#expires_at' do
......@@ -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) }
......
# 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
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment