Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-16 09:09:36 +00:00
parent 1de9854406
commit a57cec4bb8
50 changed files with 847 additions and 97 deletions

View File

@ -16,6 +16,50 @@ function decodeUrlParameter(val) {
return decodeURIComponent(val.replace(/\+/g, '%20'));
}
/**
* Safely encodes a string to be used as a path
*
* Note: This function DOES encode typical URL parts like ?, =, &, #, and +
* If you need to use search parameters or URL fragments, they should be
* added AFTER calling this function, not before.
*
* Usecase: An image filename is stored verbatim, and you need to load it in
* the browser.
*
* Example: /some_path/file #1.jpg ==> /some_path/file%20%231.jpg
* Example: /some-path/file! Final!.jpg ==> /some-path/file%21%20Final%21.jpg
*
* Essentially, if a character *could* present a problem in a URL, it's escaped
* to the hexadecimal representation instead. This means it's a bit more
* aggressive than encodeURIComponent: that built-in function doesn't
* encode some characters that *could* be problematic, so this function
* adds them (#, !, ~, *, ', (, and )).
* Additionally, encodeURIComponent *does* encode `/`, but we want safer
* URLs, not non-functional URLs, so this function DEcodes slashes ('%2F').
*
* @param {String} potentiallyUnsafePath
* @returns {String}
*/
export function encodeSaferUrl(potentiallyUnsafePath) {
const unencode = ['%2F'];
const encode = ['#', '!', '~', '\\*', "'", '\\(', '\\)'];
let saferPath = encodeURIComponent(potentiallyUnsafePath);
unencode.forEach((code) => {
saferPath = saferPath.replace(new RegExp(code, 'g'), decodeURIComponent(code));
});
encode.forEach((code) => {
const encodedValue = code
.codePointAt(code.length - 1)
.toString(16)
.toUpperCase();
saferPath = saferPath.replace(new RegExp(code, 'g'), `%${encodedValue}`);
});
return saferPath;
}
export function cleanLeadingSeparator(path) {
return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
}

View File

@ -0,0 +1,27 @@
<script>
import { GlLink } from '@gitlab/ui';
import { ACTION_TEXT } from '../constants';
export default {
components: { GlLink },
i18n: {
ACTION_TEXT,
},
props: {
actions: {
required: true,
type: Object,
},
},
};
</script>
<template>
<ul>
<li v-for="(value, action) in actions" :key="action">
<span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span>
<span v-else>
<gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link>
</span>
</li>
</ul>
</template>

View File

@ -0,0 +1,27 @@
<script>
import { GlLink } from '@gitlab/ui';
import { ACTION_TEXT } from '../constants';
export default {
components: { GlLink },
i18n: {
ACTION_TEXT,
},
props: {
actions: {
required: true,
type: Object,
},
},
};
</script>
<template>
<ul>
<li v-for="(value, action) in actions" :key="action">
<span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span>
<span v-else>
<gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link>
</span>
</li>
</ul>
</template>

View File

@ -0,0 +1,12 @@
import { s__ } from '~/locale';
export const ACTION_TEXT = {
gitWrite: s__('LearnGitLab|Create a repository'),
userAdded: s__('LearnGitLab|Invite your colleagues'),
pipelineCreated: s__('LearnGitLab|Set-up CI/CD'),
trialStarted: s__('LearnGitLab|Start a free trial of GitLab Gold'),
codeOwnersEnabled: s__('LearnGitLab|Add code owners'),
requiredMrApprovalsEnabled: s__('LearnGitLab|Enable require merge approvals'),
mergeRequestCreated: s__('LearnGitLab|Submit a merge request (MR)'),
securityScanEnabled: s__('LearnGitLab|Run a Security scan using CI/CD'),
};

View File

@ -0,0 +1,25 @@
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlabA from '../components/learn_gitlab_a.vue';
import LearnGitlabB from '../components/learn_gitlab_b.vue';
function initLearnGitlab() {
const el = document.getElementById('js-learn-gitlab-app');
if (!el) {
return false;
}
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const { learnGitlabA } = gon.experiments;
return new Vue({
el,
render(createElement) {
return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } });
},
});
}
initLearnGitlab();

View File

@ -11,7 +11,6 @@ export default class AccessDropdown {
const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
this.options = options;
this.hasLicense = hasLicense;
this.deployKeysOnProtectedBranchesEnabled = gon.features.deployKeysOnProtectedBranches;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
@ -330,11 +329,7 @@ export default class AccessDropdown {
);
})
.catch(() => {
if (this.deployKeysOnProtectedBranchesEnabled) {
createFlash({ message: __('Failed to load groups, users and deploy keys.') });
} else {
createFlash({ message: __('Failed to load groups & users.') });
}
createFlash({ message: __('Failed to load groups, users and deploy keys.') });
});
} else {
this.getDeployKeys(query)
@ -445,35 +440,33 @@ export default class AccessDropdown {
}
}
if (this.deployKeysOnProtectedBranchesEnabled) {
const deployKeys = deployKeysResponse.map((response) => {
const {
id,
fingerprint,
title,
owner: { avatar_url, name, username },
} = response;
const deployKeys = deployKeysResponse.map((response) => {
const {
id,
fingerprint,
title,
owner: { avatar_url, name, username },
} = response;
const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
return {
id,
title: title.concat(' ', shortFingerprint),
avatar_url,
fullname: name,
username,
type: LEVEL_TYPES.DEPLOY_KEY,
};
});
return {
id,
title: title.concat(' ', shortFingerprint),
avatar_url,
fullname: name,
username,
type: LEVEL_TYPES.DEPLOY_KEY,
};
});
if (this.accessLevel === ACCESS_LEVELS.PUSH) {
if (deployKeys.length) {
consolidatedData = consolidatedData.concat(
[{ type: 'divider' }],
[{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
deployKeys,
);
}
if (this.accessLevel === ACCESS_LEVELS.PUSH) {
if (deployKeys.length) {
consolidatedData = consolidatedData.concat(
[{ type: 'divider' }],
[{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
deployKeys,
);
}
}
@ -501,19 +494,15 @@ export default class AccessDropdown {
}
getDeployKeys(query) {
if (this.deployKeysOnProtectedBranchesEnabled) {
return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
}
return Promise.resolve({ data: [] });
return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
}
buildUrl(urlRoot, url) {

View File

@ -1,6 +1,7 @@
<script>
import { throttle } from 'lodash';
import { numberToHumanSize } from '../../../../lib/utils/number_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { encodeSaferUrl } from '~/lib/utils/url_utility';
export default {
props: {
@ -43,6 +44,9 @@ export default {
hasDimensions() {
return this.width && this.height;
},
safePath() {
return encodeSaferUrl(this.path);
},
},
beforeDestroy() {
window.removeEventListener('resize', this.resizeThrottled, false);
@ -84,7 +88,7 @@ export default {
<template>
<div data-testid="image-viewer" data-qa-selector="image_viewer_container">
<div :class="innerCssClasses" class="position-relative">
<img ref="contentImg" :src="path" @load="onImgLoad" />
<img ref="contentImg" :src="safePath" @load="onImgLoad" />
<slot
name="image-overlay"
:rendered-width="renderedWidth"

View File

@ -66,7 +66,11 @@ module BoardsResponses
end
def respond_with_board
respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# rubocop:disable Gitlab/ModuleWithInstanceVariables
return render_404 unless @board
respond_with(@board)
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
def serialize_as_json(resource)

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class Projects::LearnGitlabController < Projects::ApplicationController
before_action :authenticate_user!
before_action :check_experiment_enabled?
feature_category :users
def index
push_frontend_experiment(:learn_gitlab_a, subject: current_user)
push_frontend_experiment(:learn_gitlab_b, subject: current_user)
end
private
def check_experiment_enabled?
return access_denied! unless helpers.learn_gitlab_experiment_enabled?(project)
end
end

View File

@ -7,7 +7,6 @@ module Projects
before_action :define_variables, only: [:create_deploy_token]
before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
push_frontend_feature_flag(:deploy_keys_on_protected_branches, @project)
end
feature_category :source_code_management, [:show, :cleanup]

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
module LearnGitlabHelper
def learn_gitlab_experiment_enabled?(project)
return false unless current_user
return false unless experiment_enabled_for_user?
learn_gitlab_onboarding_available?(project)
end
def onboarding_actions_data(project)
attributes = onboarding_progress(project).attributes.symbolize_keys
action_urls.map do |action, url|
[
action,
url: url,
completed: attributes[OnboardingProgress.column_name(action)].present?
]
end.to_h
end
private
ACTION_ISSUE_IDS = {
git_write: 2,
pipeline_created: 4,
merge_request_created: 6,
user_added: 7,
trial_started: 13,
required_mr_approvals_enabled: 15,
code_owners_enabled: 16
}.freeze
ACTION_DOC_URLS = {
security_scan_enabled: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports'
}.freeze
def action_urls
ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }.merge(ACTION_DOC_URLS)
end
def learn_gitlab_project
@learn_gitlab_project ||= LearnGitlab.new(current_user).project
end
def onboarding_progress(project)
OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord
end
def experiment_enabled_for_user?
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user) ||
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
end
def learn_gitlab_onboarding_available?(project)
OnboardingProgress.onboarding?(project.namespace) &&
LearnGitlab.new(current_user).available?
end
end

View File

@ -433,6 +433,8 @@ module ProjectsHelper
nav_tabs += package_nav_tabs(project, current_user)
nav_tabs << :learn_gitlab if learn_gitlab_experiment_enabled?(project)
nav_tabs
end
# rubocop:enable Metrics/CyclomaticComplexity

View File

@ -2,6 +2,8 @@
module Clusters
module Applications
# DEPRECATED for removal in %14.0
# See https://gitlab.com/groups/gitlab-org/-/epics/4280
class CertManager < ApplicationRecord
VERSION = 'v0.10.1'
CRD_VERSION = '0.10'

View File

@ -2,6 +2,8 @@
module Clusters
module Applications
# DEPRECATED for removal in %14.0
# See https://gitlab.com/groups/gitlab-org/-/epics/4280
class Crossplane < ApplicationRecord
VERSION = '0.4.1'

View File

@ -2,6 +2,8 @@
module Clusters
module Applications
# DEPRECATED for removal in %14.0
# See https://gitlab.com/groups/gitlab-org/-/epics/4280
class Ingress < ApplicationRecord
VERSION = '1.40.2'
INGRESS_CONTAINER_NAME = 'nginx-ingress-controller'

View File

@ -4,6 +4,8 @@ require 'securerandom'
module Clusters
module Applications
# DEPRECATED for removal in %14.0
# See https://gitlab.com/groups/gitlab-org/-/epics/4280
class Jupyter < ApplicationRecord
VERSION = '0.9.0'

View File

@ -2,6 +2,8 @@
module Clusters
module Applications
# DEPRECATED for removal in %14.0
# See https://gitlab.com/groups/gitlab-org/-/epics/4280
class Knative < ApplicationRecord
VERSION = '0.10.0'
REPOSITORY = 'https://charts.gitlab.io'

View File

@ -47,6 +47,10 @@ class OnboardingProgress < ApplicationRecord
safe_find_or_create_by(namespace: namespace)
end
def onboarding?(namespace)
where(namespace: namespace).any?
end
def register(namespace, action)
return unless root_namespace?(namespace) && ACTIONS.include?(action)

View File

@ -19,7 +19,7 @@ class ProtectedBranch::PushAccessLevel < ApplicationRecord
end
def check_access(user)
if Feature.enabled?(:deploy_keys_on_protected_branches, project) && user && deploy_key.present?
if user && deploy_key.present?
return true if user.can?(:read_project, project) && enabled_deploy_key_for_user?(deploy_key, user)
end

View File

@ -33,6 +33,13 @@
= link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
%span= _('Releases')
- if project_nav_tab? :learn_gitlab
= nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
= link_to project_learn_gitlab_path(@project) do
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
= _('Learn GitLab')
- if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do

View File

@ -0,0 +1,4 @@
- breadcrumb_title _("Learn GitLab")
- page_title _("Learn GitLab")
#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json } }

View File

@ -1,5 +1,3 @@
- select_mode_for_dropdown = Feature.enabled?(:deploy_keys_on_protected_branches, @project) ? 'js-multiselect' : ''
- content_for :merge_access_levels do
.merge_access_levels-container
= dropdown_tag('Select',
@ -9,7 +7,7 @@
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: "js-allowed-to-push qa-allowed-to-push-select #{select_mode_for_dropdown} wide",
options: { toggle_class: "js-allowed-to-push qa-allowed-to-push-select js-multiselect wide",
dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown rspec-allowed-to-push-dropdown capitalize-header',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})

View File

@ -1,5 +1,3 @@
- select_mode_for_dropdown = Feature.enabled?(:deploy_keys_on_protected_branches, protected_branch.project) ? 'js-multiselect' : ''
- merge_access_levels = protected_branch.merge_access_levels.for_role
- push_access_levels = protected_branch.push_access_levels.for_role
@ -25,7 +23,7 @@
%td.push_access_levels-container
= hidden_field_tag "allowed_to_push_#{protected_branch.id}", push_access_levels.first&.access_level
= dropdown_tag( (push_access_levels.first&.humanize || 'Select') ,
options: { toggle_class: "js-allowed-to-push #{select_mode_for_dropdown}", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
options: { toggle_class: "js-allowed-to-push js-multiselect", dropdown_class: 'dropdown-menu-selectable js-allowed-to-push-container capitalize-header',
data: { field_name: "allowed_to_push_#{protected_branch.id}", preselected_items: access_levels_data(push_access_levels) }})
- if user_push_access_levels.any?
%p.small

View File

@ -0,0 +1,5 @@
---
title: Deprecate GitLab-managed (v1) apps that will be removed in 14.0
merge_request: 54162
author:
type: deprecated

View File

@ -0,0 +1,5 @@
---
title: Allow deploy keys to push to a protected branch
merge_request: 53812
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix some image diff URLs with special characters causing the diff to not show
merge_request: 53638
author:
type: fixed

View File

@ -1,8 +1,8 @@
---
name: deploy_keys_on_protected_branches
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35638
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247866
milestone: '13.5'
type: development
group: group::release
name: learn_gitlab_a_experiment_percentage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53089
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281022
milestone: '13.9'
type: experiment
group: group::conversion
default_enabled: false

View File

@ -0,0 +1,8 @@
---
name: learn_gitlab_b_experiment_percentage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53089
rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/306
milestone: '13.9'
type: experiment
group: group::conversion
default_enabled: false

View File

@ -87,6 +87,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
get :learn_gitlab, action: :index, controller: 'learn_gitlab'
namespace :ci do
resource :lint, only: [:show, :create]
resource :pipeline_editor, only: [:show], controller: :pipeline_editor, path: 'editor'

View File

@ -24,8 +24,8 @@ and [code review guidelines](https://docs.gitlab.com/ee/development/code_review.
Please consider assigning a reviewer or maintainer who is a
[domain expert](https://about.gitlab.com/handbook/engineering/projects/#gitlab) in the area of the merge request.
Once you've decided who will review this merge request, mention them as you
normally would! Danger does not automatically notify them for you.
Once you've decided who will review this merge request, assign them as a reviewer!
Danger does not automatically notify them for you.
| Category | Reviewer | Maintainer |
| -------- | -------- | ---------- |

View File

@ -80,6 +80,7 @@ they are set to Maintainers by default.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/30769) in GitLab 13.7.
> - This feature is being selectively deployed in GitLab.com 13.7, and may not be available for all users.
> - This feature is available for all users in GitLab 13.9.
You can allow specific machines to access protected branches in your repository with
[deploy keys](deploy_keys/index.md). This can be useful for your CI/CD workflow,

View File

@ -262,6 +262,7 @@ For GitLab.com, it is set to 10 MB.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/290813) in GitLab 13.8.
> - Revised CSV column headers [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299247) in GitLab 13.9.
> - Ability to select which fields to export [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/290823) in GitLab 13.9.
You can export GitLab requirements to a
[CSV file](https://en.wikipedia.org/wiki/Comma-separated_values) sent to your default notification
@ -276,7 +277,14 @@ Users with Reporter or higher [permissions](../../permissions.md) can export req
To export requirements:
1. In a project, go to **Requirements**.
1. Select the **Export as CSV** icon (**{export}**) in the top right. A confirmation modal appears.
1. In the top right, select the **Export as CSV** icon (**{export}**).
A confirmation modal appears.
1. Under **Advanced export options**, select which fields to export.
All fields are selected by default. To exclude a field from being exported, clear the checkbox next to it.
1. Select **Export requirements**. The exported CSV file is sent to the email address associated with your user.
### Exported CSV file format

View File

@ -95,6 +95,12 @@ module Gitlab
trial_onboarding_issues: {
tracking_category: 'Growth::Conversion::Experiment::TrialOnboardingIssues'
},
learn_gitlab_a: {
tracking_category: 'Growth::Conversion::Experiment::LearnGitLabA'
},
learn_gitlab_b: {
tracking_category: 'Growth::Activation::Experiment::LearnGitLabB'
},
in_product_marketing_emails: {
tracking_category: 'Growth::Activation::Experiment::InProductMarketingEmails'
}

View File

@ -319,10 +319,8 @@ module Gitlab
end
def check_change_access!
return if deploy_key? && !deploy_keys_on_protected_branches_enabled?
if changes == ANY
can_push = (deploy_key? && deploy_keys_on_protected_branches_enabled?) ||
can_push = deploy_key? ||
user_can_push? ||
project&.any_branch_allows_collaboration?(user_access.user)
@ -453,7 +451,7 @@ module Gitlab
CiAccess.new
elsif user && request_from_ci_build?
BuildAccess.new(user, container: container)
elsif deploy_key? && deploy_keys_on_protected_branches_enabled?
elsif deploy_key?
DeployKeyAccess.new(deploy_key, container: container)
else
UserAccess.new(user, container: container)
@ -532,10 +530,6 @@ module Gitlab
def size_checker
container.repository_size_checker
end
def deploy_keys_on_protected_branches_enabled?
Feature.enabled?(:deploy_keys_on_protected_branches, project)
end
end
end

View File

@ -738,9 +738,6 @@ msgstr ""
msgid "%{reportType} detected %{totalStart}no%{totalEnd} vulnerabilities."
msgstr ""
msgid "%{requirementCount} requirements have been selected for export. These will be sent to %{email} as an attachment once finished."
msgstr ""
msgid "%{retryButtonStart}Try again%{retryButtonEnd} or %{newFileButtonStart}attach a new file%{newFileButtonEnd}."
msgstr ""
@ -2552,6 +2549,9 @@ msgstr ""
msgid "Advanced Settings"
msgstr ""
msgid "Advanced export options"
msgstr ""
msgid "Advanced permissions, Large File Storage and Two-Factor authentication settings."
msgstr ""
@ -12125,6 +12125,9 @@ msgstr ""
msgid "Export"
msgstr ""
msgid "Export %{requirementsCount} requirements?"
msgstr ""
msgid "Export as CSV"
msgstr ""
@ -12320,9 +12323,6 @@ msgstr ""
msgid "Failed to load group activity metrics. Please try again."
msgstr ""
msgid "Failed to load groups & users."
msgstr ""
msgid "Failed to load groups, users and deploy keys."
msgstr ""
@ -17445,6 +17445,30 @@ msgstr ""
msgid "Learn more."
msgstr ""
msgid "LearnGitLab|Add code owners"
msgstr ""
msgid "LearnGitLab|Create a repository"
msgstr ""
msgid "LearnGitLab|Enable require merge approvals"
msgstr ""
msgid "LearnGitLab|Invite your colleagues"
msgstr ""
msgid "LearnGitLab|Run a Security scan using CI/CD"
msgstr ""
msgid "LearnGitLab|Set-up CI/CD"
msgstr ""
msgid "LearnGitLab|Start a free trial of GitLab Gold"
msgstr ""
msgid "LearnGitLab|Submit a merge request (MR)"
msgstr ""
msgid "Leave"
msgstr ""
@ -22166,6 +22190,9 @@ msgstr ""
msgid "Please select at least one filter to see results"
msgstr ""
msgid "Please select what should be included in each exported requirement."
msgstr ""
msgid "Please set a new password before proceeding."
msgstr ""
@ -29784,6 +29811,9 @@ msgstr ""
msgid "These variables are inherited from the parent group."
msgstr ""
msgid "These will be sent to %{email} in an attachment once finished."
msgstr ""
msgid "Third Party Advisory Link"
msgstr ""

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::LearnGitlabController do
describe 'GET #index' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:learn_gitlab_experiment_enabled) { true }
let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
subject { get :index, params: params }
before do
allow(controller.helpers).to receive(:learn_gitlab_experiment_enabled?).and_return(learn_gitlab_experiment_enabled)
end
context 'unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
end
context 'authenticated user' do
before do
sign_in(user)
end
it { is_expected.to render_template(:index) }
it 'pushes experiment to frontend' do
expect(controller).to receive(:push_frontend_experiment).with(:learn_gitlab_a, subject: user)
expect(controller).to receive(:push_frontend_experiment).with(:learn_gitlab_b, subject: user)
subject
end
context 'learn_gitlab experiment not enabled' do
let(:learn_gitlab_experiment_enabled) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
end
end

View File

@ -9,10 +9,6 @@ RSpec.describe 'Protected Branches', :js do
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
before do
stub_feature_flags(deploy_keys_on_protected_branches: false)
end
context 'logged in as developer' do
before do
project.add_developer(user)
@ -174,7 +170,7 @@ RSpec.describe 'Protected Branches', :js do
stub_licensed_features(protected_refs_for_users: false)
end
include_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
include_examples 'Deploy keys with protected branches' do
let(:all_dropdown_sections) { %w(Roles Deploy\ Keys) }
end
end

View File

@ -880,4 +880,37 @@ describe('URL utility', () => {
expect(urlUtils.getURLOrigin(url)).toBe(expectation);
});
});
describe('encodeSaferUrl', () => {
it.each`
character | input | output
${' '} | ${'/url/hello 1.jpg'} | ${'/url/hello%201.jpg'}
${'#'} | ${'/url/hello#1.jpg'} | ${'/url/hello%231.jpg'}
${'!'} | ${'/url/hello!.jpg'} | ${'/url/hello%21.jpg'}
${'~'} | ${'/url/hello~.jpg'} | ${'/url/hello%7E.jpg'}
${'*'} | ${'/url/hello*.jpg'} | ${'/url/hello%2A.jpg'}
${"'"} | ${"/url/hello'.jpg"} | ${'/url/hello%27.jpg'}
${'('} | ${'/url/hello(.jpg'} | ${'/url/hello%28.jpg'}
${')'} | ${'/url/hello).jpg'} | ${'/url/hello%29.jpg'}
${'?'} | ${'/url/hello?.jpg'} | ${'/url/hello%3F.jpg'}
${'='} | ${'/url/hello=.jpg'} | ${'/url/hello%3D.jpg'}
${'+'} | ${'/url/hello+.jpg'} | ${'/url/hello%2B.jpg'}
${'&'} | ${'/url/hello&.jpg'} | ${'/url/hello%26.jpg'}
`(
'properly escapes `$character` characters while retaining the integrity of the URL',
({ input, output }) => {
expect(urlUtils.encodeSaferUrl(input)).toBe(output);
},
);
it.each`
character | input
${'/, .'} | ${'/url/hello.png'}
${'\\d'} | ${'/url/hello123.png'}
${'-'} | ${'/url/hello-123.png'}
${'_'} | ${'/url/hello_123.png'}
`('makes no changes to unproblematic characters ($character)', ({ input }) => {
expect(urlUtils.encodeSaferUrl(input)).toBe(input);
});
});
});

View File

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Learn GitLab Design A should render the loading state 1`] = `
<ul>
<li>
<span>
Create a repository
</span>
</li>
<li>
<span>
Invite your colleagues
</span>
</li>
<li>
<span>
Set-up CI/CD
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Start a free trial of GitLab Gold
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Add code owners
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Enable require merge approvals
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Submit a merge request (MR)
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Run a Security scan using CI/CD
</gl-link-stub>
</span>
</li>
</ul>
`;

View File

@ -0,0 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Learn GitLab Design B should render the loading state 1`] = `
<ul>
<li>
<span>
Create a repository
</span>
</li>
<li>
<span>
Invite your colleagues
</span>
</li>
<li>
<span>
Set-up CI/CD
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Start a free trial of GitLab Gold
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Add code owners
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Enable require merge approvals
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Submit a merge request (MR)
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Run a Security scan using CI/CD
</gl-link-stub>
</span>
</li>
</ul>
`;

View File

@ -0,0 +1,63 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
const TEST_ACTIONS = {
gitWrite: {
url: 'http://example.com/',
completed: true,
},
userAdded: {
url: 'http://example.com/',
completed: true,
},
pipelineCreated: {
url: 'http://example.com/',
completed: true,
},
trialStarted: {
url: 'http://example.com/',
completed: false,
},
codeOwnersEnabled: {
url: 'http://example.com/',
completed: false,
},
requiredMrApprovalsEnabled: {
url: 'http://example.com/',
completed: false,
},
mergeRequestCreated: {
url: 'http://example.com/',
completed: false,
},
securityScanEnabled: {
url: 'http://example.com/',
completed: false,
},
};
describe('Learn GitLab Design A', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const createWrapper = () => {
wrapper = extendedWrapper(
shallowMount(LearnGitlabA, {
propsData: {
actions: TEST_ACTIONS,
},
}),
);
};
it('should render the loading state', () => {
createWrapper();
expect(wrapper.element).toMatchSnapshot();
});
});

View File

@ -0,0 +1,63 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
const TEST_ACTIONS = {
gitWrite: {
url: 'http://example.com/',
completed: true,
},
userAdded: {
url: 'http://example.com/',
completed: true,
},
pipelineCreated: {
url: 'http://example.com/',
completed: true,
},
trialStarted: {
url: 'http://example.com/',
completed: false,
},
codeOwnersEnabled: {
url: 'http://example.com/',
completed: false,
},
requiredMrApprovalsEnabled: {
url: 'http://example.com/',
completed: false,
},
mergeRequestCreated: {
url: 'http://example.com/',
completed: false,
},
securityScanEnabled: {
url: 'http://example.com/',
completed: false,
},
};
describe('Learn GitLab Design B', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const createWrapper = () => {
wrapper = extendedWrapper(
shallowMount(LearnGitlabA, {
propsData: {
actions: TEST_ACTIONS,
},
}),
);
};
it('should render the loading state', () => {
createWrapper();
expect(wrapper.element).toMatchSnapshot();
});
});

View File

@ -14,7 +14,6 @@ describe('AccessDropdown', () => {
`);
const $dropdown = $('#dummy-dropdown');
$dropdown.data('defaultLabel', defaultLabel);
gon.features = { deployKeysOnProtectedBranches: true };
const options = {
$dropdown,
accessLevelsData: {

View File

@ -33,4 +33,14 @@ describe('Image Viewer', () => {
},
);
});
describe('file path', () => {
it('should output a valid URL path for the image', () => {
wrapper = mount(ImageViewer, {
propsData: { path: '/url/hello#1.jpg' },
});
expect(wrapper.find('img').attributes('src')).toBe('/url/hello%231.jpg');
});
});
});

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LearnGitlabHelper do
include AfterNextHelpers
include Devise::Test::ControllerHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: LearnGitlab::PROJECT_NAME, namespace: user.namespace) }
let_it_be(:namespace) { project.namespace }
before do
project.add_developer(user)
allow(helper).to receive(:user).and_return(user)
allow_next_instance_of(LearnGitlab) do |learn_gitlab|
allow(learn_gitlab).to receive(:project).and_return(project)
end
OnboardingProgress.onboard(namespace)
OnboardingProgress.register(namespace, :git_write)
end
describe '.onboarding_actions_data' do
subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) }
it 'has all actions' do
expect(onboarding_actions_data.keys).to contain_exactly(
:git_write,
:pipeline_created,
:merge_request_created,
:user_added,
:trial_started,
:required_mr_approvals_enabled,
:code_owners_enabled,
:security_scan_enabled
)
end
it 'sets correct path and completion status' do
expect(onboarding_actions_data[:git_write]).to eq({
url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:git_write]),
completed: true
})
expect(onboarding_actions_data[:pipeline_created]).to eq({
url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:pipeline_created]),
completed: false
})
end
end
describe '.learn_gitlab_experiment_enabled?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
subject { helper.learn_gitlab_experiment_enabled?(project) }
where(:experiment_a, :experiment_b, :onboarding, :learn_gitlab_available, :result) do
true | false | true | true | true
false | true | true | true | true
false | false | true | true | false
true | true | true | false | false
true | true | false | true | false
end
with_them do
before do
stub_experiment_for_subject(learn_gitlab_a: experiment_a, learn_gitlab_b: experiment_b)
allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
allow_next(LearnGitlab, user).to receive(:available?).and_return(learn_gitlab_available)
end
context 'when signed in' do
before do
sign_in(user)
end
it { is_expected.to eq(result) }
end
context 'when not signed in' do
it { is_expected.to eq(false) }
end
end
end
end

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe ProjectsHelper do
include ProjectForksHelper
include AfterNextHelpers
let_it_be_with_reload(:project) { create(:project) }
let_it_be_with_refind(:project_with_repo) { create(:project, :repository) }
@ -498,6 +499,20 @@ RSpec.describe ProjectsHelper do
it { is_expected.not_to include(:confluence) }
it { is_expected.to include(:wiki) }
end
context 'learn gitlab experiment' do
context 'when it is enabled' do
before do
expect(helper).to receive(:learn_gitlab_experiment_enabled?).with(project).and_return(true)
end
it { is_expected.to include(:learn_gitlab) }
end
context 'when it is not enabled' do
it { is_expected.not_to include(:learn_gitlab) }
end
end
end
describe '#can_view_operations_tab?' do

View File

@ -114,6 +114,22 @@ RSpec.describe OnboardingProgress do
end
end
describe '.onboarding?' do
subject(:onboarding?) { described_class.onboarding?(namespace) }
context 'when onboarded' do
before do
described_class.onboard(namespace)
end
it { is_expected.to eq true }
end
context 'when not onboarding' do
it { is_expected.to eq false }
end
end
describe '.register' do
subject(:register_action) { described_class.register(namespace, action) }

View File

@ -54,16 +54,6 @@ RSpec.describe ProtectedBranch::PushAccessLevel do
specify do
expect(push_access_level.check_access(user)).to be_truthy
end
context 'when the deploy_keys_on_protected_branches FF is false' do
before do
stub_feature_flags(deploy_keys_on_protected_branches: false)
end
it 'is false' do
expect(push_access_level.check_access(user)).to be_falsey
end
end
end
context 'when the deploy key is not among the active keys of this project' do

View File

@ -56,6 +56,8 @@ RSpec.shared_examples "protected branches > access control > CE" do
expect(first("li")).to have_content("Roles")
find(:link, access_type_name).click
end
find(".js-allowed-to-push").click
end
wait_for_requests

View File

@ -1,8 +1,7 @@
# frozen_string_literal: true
RSpec.shared_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
RSpec.shared_examples 'Deploy keys with protected branches' do
before do
stub_feature_flags(deploy_keys_on_protected_branches: true)
project.add_maintainer(user)
sign_in(user)
end