Commit 1eb50566 authored by Felipe Artur's avatar Felipe Artur

Merge CE/master into EE/master

parents 7e5cf83f d3aaa1a2
......@@ -432,9 +432,9 @@ pages:
script:
- mv public/ .public/
- mkdir public/
- mv coverage public/coverage-ruby
- mv coverage-javascript/default/ public/coverage-javascript/
- mv eslint-report.html public/
- mv coverage/ public/coverage-ruby/ || true
- mv coverage-javascript/default/ public/coverage-javascript/ || true
- mv eslint-report.html public/ || true
artifacts:
paths:
- public
......
......@@ -252,5 +252,7 @@ window.ES6Promise.polyfill();
new Aside();
// bind sidebar events
new gl.Sidebar();
gl.utils.initTimeagoTimeout();
});
}).call(this);
......@@ -19,7 +19,6 @@
/* global UsersSelect */
/* global GroupAvatar */
/* global LineHighlighter */
/* global ShortcutsBlob */
/* global ProjectFork */
/* global BuildArtifacts */
/* global GroupsSelect */
......@@ -38,6 +37,8 @@
/* global WeightSelect */
/* global AdminEmailSelect */
const ShortcutsBlob = require('./shortcuts_blob');
(function() {
var Dispatcher;
......@@ -223,7 +224,12 @@
case 'projects:blame:show':
new LineHighlighter();
shortcut_handler = new ShortcutsNavigation();
new ShortcutsBlob(true);
const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url');
const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
new ShortcutsBlob({
skipResetBindings: true,
fileBlobPermalinkUrl,
});
break;
case 'groups:labels:new':
case 'groups:labels:edit':
......
......@@ -147,12 +147,12 @@ require('./environment_terminal_button');
},
/**
* Returns the value of the `stoppable?` key provided in the response.
* Returns the value of the `stop_action?` key provided in the response.
*
* @returns {Boolean}
*/
isStoppable() {
return this.model['stoppable?'];
hasStopAction() {
return this.model['stop_action?'];
},
/**
......@@ -508,7 +508,7 @@ require('./environment_terminal_button');
</external-url-component>
</div>
<div v-if="isStoppable && canCreateDeployment"
<div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
:stop-url="model.stop_path">
......
......@@ -69,6 +69,9 @@
var hash = w.gl.utils.getLocationHash();
if (!hash) return;
// This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash);
var navbar = document.querySelector('.navbar-gitlab');
var subnav = document.querySelector('.layout-nav');
var fixedTabs = document.querySelector('.js-tabs-affix');
......@@ -134,6 +137,14 @@
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
};
gl.utils.isMetaClick = function(e) {
// Identify following special clicks
// 1) Cmd + Click on Mac (e.metaKey)
// 2) Ctrl + Click on PC (e.ctrlKey)
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
return e.metaKey || e.ctrlKey || e.which === 2;
};
gl.utils.scrollToElement = function($el) {
var top = $el.offset().top;
gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height();
......
......@@ -8,6 +8,8 @@ window.dateFormat = require('vendor/date.format');
(function() {
(function(w) {
var base;
var timeagoInstance;
if (w.gl == null) {
w.gl = {};
}
......@@ -24,49 +26,51 @@ window.dateFormat = require('vendor/date.format');
return this.days[date.getDay()];
};
w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago) {
if (setTimeago == null) {
setTimeago = true;
}
$timeagoEls.filter(':not([data-timeago-rendered])').each(function() {
var $el = $(this);
$el.attr('title', gl.utils.formatDate($el.attr('datetime')));
w.gl.utils.localTimeAgo = function($timeagoEls, setTimeago = true) {
$timeagoEls.each((i, el) => {
el.setAttribute('title', gl.utils.formatDate(el.getAttribute('datetime')));
if (setTimeago) {
// Recreate with custom template
$el.tooltip({
$(el).tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
}
$el.attr('data-timeago-rendered', true);
gl.utils.renderTimeago($el);
el.classList.add('js-timeago-render');
});
gl.utils.renderTimeago($timeagoEls);
};
w.gl.utils.getTimeago = function() {
var locale = function(number, index) {
return [
['less than a minute ago', 'a while'],
['less than a minute ago', 'in %s seconds'],
['about a minute ago', 'in 1 minute'],
['%s minutes ago', 'in %s minutes'],
['about an hour ago', 'in 1 hour'],
['about %s hours ago', 'in %s hours'],
['a day ago', 'in 1 day'],
['%s days ago', 'in %s days'],
['a week ago', 'in 1 week'],
['%s weeks ago', 'in %s weeks'],
['a month ago', 'in 1 month'],
['%s months ago', 'in %s months'],
['a year ago', 'in 1 year'],
['%s years ago', 'in %s years']
][index];
};
timeago.register('gl_en', locale);
return timeago();
var locale;
if (!timeagoInstance) {
locale = function(number, index) {
return [
['less than a minute ago', 'a while'],
['less than a minute ago', 'in %s seconds'],
['about a minute ago', 'in 1 minute'],
['%s minutes ago', 'in %s minutes'],
['about an hour ago', 'in 1 hour'],
['about %s hours ago', 'in %s hours'],
['a day ago', 'in 1 day'],
['%s days ago', 'in %s days'],
['a week ago', 'in 1 week'],
['%s weeks ago', 'in %s weeks'],
['a month ago', 'in 1 month'],
['%s months ago', 'in %s months'],
['a year ago', 'in 1 year'],
['%s years ago', 'in %s years']
][index];
};
timeago.register('gl_en', locale);
timeagoInstance = timeago();
}
return timeagoInstance;
};
w.gl.utils.timeFor = function(time, suffix, expiredLabel) {
......@@ -85,9 +89,30 @@ window.dateFormat = require('vendor/date.format');
return timefor;
};
w.gl.utils.renderTimeago = function($element) {
var timeagoInstance = gl.utils.getTimeago();
timeagoInstance.render($element, 'gl_en');
w.gl.utils.cachedTimeagoElements = [];
w.gl.utils.renderTimeago = function($els) {
if (!$els && !w.gl.utils.cachedTimeagoElements.length) {
w.gl.utils.cachedTimeagoElements = [].slice.call(document.querySelectorAll('.js-timeago-render'));
} else if ($els) {
w.gl.utils.cachedTimeagoElements = w.gl.utils.cachedTimeagoElements.concat($els.toArray());
}
w.gl.utils.cachedTimeagoElements.forEach(gl.utils.updateTimeagoText);
};
w.gl.utils.updateTimeagoText = function(el) {
const timeago = gl.utils.getTimeago();
const formattedDate = timeago.format(el.getAttribute('datetime'), 'gl_en');
if (el.textContent !== formattedDate) {
el.textContent = formattedDate;
}
};
w.gl.utils.initTimeagoTimeout = function() {
gl.utils.renderTimeago();
gl.utils.timeagoTimeout = setTimeout(gl.utils.initTimeagoTimeout, 1000);
};
w.gl.utils.getDayDifference = function(a, b) {
......
......@@ -82,12 +82,18 @@ require('./flash');
$(document)
.on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.on('click', '.js-show-tab', this.showTab);
$('.merge-request-tabs a[data-toggle="tab"]')
.on('click', this.clickTab);
}
unbindEvents() {
$(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
.off('click', '.js-show-tab', this.showTab);
$('.merge-request-tabs a[data-toggle="tab"]')
.off('click', this.clickTab);
}
showTab(e) {
......@@ -95,6 +101,14 @@ require('./flash');
this.activateTab($(e.target).data('action'));
}
clickTab(e) {
if (e.target && gl.utils.isMetaClick(e)) {
const targetLink = e.target.getAttribute('href');
e.stopImmediatePropagation();
window.open(targetLink, '_blank');
}
}
tabShown(e) {
const $target = $(e.target);
const action = $target.data('action');
......
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return */
/* global Shortcuts */
/* global Mousetrap */
require('./shortcuts');
(function() {
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
this.ShortcutsBlob = (function(superClass) {
extend(ShortcutsBlob, superClass);
function ShortcutsBlob(skipResetBindings) {
ShortcutsBlob.__super__.constructor.call(this, skipResetBindings);
Mousetrap.bind('y', ShortcutsBlob.copyToClipboard);
}
ShortcutsBlob.copyToClipboard = function() {
var clipboardButton;
clipboardButton = $('.btn-clipboard');
if (clipboardButton) {
return clipboardButton.click();
}
};
return ShortcutsBlob;
})(Shortcuts);
}).call(this);
/* global Mousetrap */
/* global Shortcuts */
require('./shortcuts');
const defaults = {
skipResetBindings: false,
fileBlobPermalinkUrl: null,
};
class ShortcutsBlob extends Shortcuts {
constructor(opts) {
const options = Object.assign({}, defaults, opts);
super(options.skipResetBindings);
this.options = options;
Mousetrap.bind('y', this.moveToFilePermalink.bind(this));
}
moveToFilePermalink() {
if (this.options.fileBlobPermalinkUrl) {
const hash = gl.utils.getLocationHash();
const hashUrlString = hash ? `#${hash}` : '';
gl.utils.visitUrl(`${this.options.fileBlobPermalinkUrl}${hashUrlString}`);
}
}
}
module.exports = ShortcutsBlob;
......@@ -146,14 +146,26 @@
}
goToTodoUrl(e) {
const todoLink = $(this).data('url');
const todoLink = this.dataset.url;
let targetLink = e.target.getAttribute('href');
if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
}
if (!todoLink) {
return;
}
// Allow Meta-Click or Mouse3-click to open in a new tab
if (e.metaKey || e.which === 2) {
if (gl.utils.isMetaClick(e)) {
e.preventDefault();
return window.open(todoLink, '_blank');
// Meta-Click on username leads to different URL than todoLink.
// Turbolinks can resolve that URL, but window.open requires URL manually.
if (targetLink !== todoLink) {
return window.open(targetLink, '_blank');
} else {
return window.open(todoLink, '_blank');
}
} else {
return gl.utils.visitUrl(todoLink);
}
......
......@@ -313,3 +313,7 @@ ul.controls {
}
}
}
ul.indent-list {
padding: 10px 0 0 30px;
}
......@@ -193,7 +193,7 @@
top: $header-height;
bottom: 0;
right: 0;
z-index: 10;
z-index: 8;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
......
......@@ -148,3 +148,7 @@ ul.related-merge-requests > li {
border: 1px solid $border-gray-normal;
}
}
.recaptcha {
margin-bottom: 30px;
}
......@@ -259,3 +259,8 @@
}
}
}
.label-link {
display: inline-block;
vertical-align: text-top;
}
......@@ -229,6 +229,7 @@
.finished-at {
color: $gl-text-color-secondary;
margin: 4px 0;
white-space: nowrap;
.fa {
font-size: 12px;
......@@ -666,7 +667,7 @@
vertical-align: bottom;
display: inline-block;
position: relative;
font-weight: 200;
font-weight: normal;
}
// Dropdown button in mini pipeline graph
......
module SpammableActions
extend ActiveSupport::Concern
include Recaptcha::Verify
included do
before_action :authorize_submit_spammable!, only: :mark_as_spam
end
......@@ -15,6 +17,15 @@ module SpammableActions
private
def recaptcha_params
return {} unless params[:recaptcha_verification] && Gitlab::Recaptcha.load_configurations! && verify_recaptcha
{
recaptcha_verified: true,
spam_log_id: params[:spam_log_id]
}
end
def spammable
raise NotImplementedError, "#{self.class} does not implement #{__method__}"
end
......@@ -22,4 +33,11 @@ module SpammableActions
def authorize_submit_spammable!
access_denied! unless current_user.admin?
end
def render_recaptcha?
return false if spammable.errors.count > 1 # re-render "new" template in case there are other errors
return false unless Gitlab::Recaptcha.enabled?
spammable.spam
end
end
......@@ -52,10 +52,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
def stop
return render_404 unless @environment.stoppable?
return render_404 unless @environment.available?
new_action = @environment.stop!(current_user)
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, new_action])
stop_action = @environment.stop_with_action!(current_user)
if stop_action
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
else
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
end
end
def terminal
......
......@@ -98,15 +98,13 @@ class Projects::IssuesController < Projects::ApplicationController
def create
extra_params = { request: request,
merge_request_for_resolving_discussions: merge_request_for_resolving_discussions }
extra_params.merge!(recaptcha_params)
@issue = Issues::CreateService.new(project, current_user, issue_params.merge(extra_params)).execute
respond_to do |format|
format.html do
if @issue.valid?
redirect_to issue_path(@issue)
else
render :new
end
html_response_create
end
format.js do
@link = @issue.attachment.url.to_js
......@@ -183,6 +181,20 @@ class Projects::IssuesController < Projects::ApplicationController
protected
def html_response_create
if @issue.valid?
redirect_to issue_path(@issue)
elsif render_recaptcha?
if params[:recaptcha_verification]
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
render :verify
else
render :new
end
end
def issue
# The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
......
......@@ -480,7 +480,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
stop_url =
if environment.stoppable? && can?(current_user, :create_deployment, environment)
if environment.stop_action? && can?(current_user, :create_deployment, environment)
stop_namespace_project_environment_path(project.namespace, project, environment)
end
......
......@@ -17,7 +17,7 @@ class RegistrationsController < Devise::RegistrationsController
if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
super
else
flash[:alert] = 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.'
flash[:alert] = 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
flash.delete :recaptcha_error
render action: 'new'
end
......@@ -30,7 +30,7 @@ class RegistrationsController < Devise::RegistrationsController
format.html do
session.try(:destroy)
redirect_to new_user_session_path, notice: "Account successfully removed."
end
end
end
end
......
......@@ -20,8 +20,8 @@ module MergeRequestsHelper
end
def mr_widget_refresh_url(mr)
if mr && mr.source_project
merge_widget_refresh_namespace_project_merge_request_url(mr.source_project.namespace, mr.source_project, mr)
if mr && mr.target_project
merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr)
else
''
end
......
......@@ -11,6 +11,7 @@ module Spammable
has_one :user_agent_detail, as: :subject, dependent: :destroy
attr_accessor :spam
attr_accessor :spam_log
after_validation :check_for_spam, on: :create
......@@ -34,9 +35,14 @@ module Spammable
end
def check_for_spam
if spam?
self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.")
end
error_msg = if Gitlab::Recaptcha.enabled?
"Your #{spammable_entity_type} has been recognized as spam. "\
"You can still submit it by solving Captcha."
else
"Your #{spammable_entity_type} has been recognized as spam and has been discarded."
end
self.errors.add(:base, error_msg) if spam?
end
def spammable_entity_type
......
......@@ -18,7 +18,7 @@ module TimeTrackable
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
has_many :timelogs, as: :trackable, dependent: :destroy
has_many :timelogs, dependent: :destroy
end
def spend_time(options)
......
......@@ -91,7 +91,7 @@ class Deployment < ActiveRecord::Base
@stop_action ||= manual_actions.find_by(name: on_stop)
end
def stoppable?
def stop_action?
stop_action.present?
end
......
......@@ -110,15 +110,15 @@ class Environment < ActiveRecord::Base
external_url.gsub(/\A.*?:\/\//, '')
end
def stoppable?
def stop_action?
available? && stop_action.present?
end
def stop!(current_user)
return unless stoppable?
def stop_with_action!(current_user)
return unless available?
stop
stop_action.play(current_user)
stop!
stop_action.play(current_user) if stop_action
end
def actions_for(environment)
......
......@@ -241,7 +241,12 @@ class Group < Namespace
end
def refresh_members_authorized_projects
UserProjectAccessChangedService.new(users_with_parents.pluck(:id)).execute
UserProjectAccessChangedService.new(user_ids_for_project_authorizations).
execute
end
def user_ids_for_project_authorizations
users_with_parents.pluck(:id)
end
def members_with_parents
......
......@@ -218,6 +218,10 @@ class Namespace < ActiveRecord::Base
self.class.joins(:route).where('routes.path LIKE ?', "#{route.path}/%").reorder('routes.path ASC')
end
def user_ids_for_project_authorizations
[owner_id]
end
private
def repository_storage_paths
......
......@@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service
def fields
[
{ type: 'text', name: 'token', placeholder: '' }
{ type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' }
]
end
......
......@@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
end
def title
'Mattermost Command'
'Mattermost slash commands'
end
def description
"Perform common operations on GitLab in Mattermost"
"Perform common operations in Mattermost"
end
def self.to_param
......
......@@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService
include TriggersHelper
def title
'Slack Command'
'Slack slash commands'
end
def description
"Perform common operations on GitLab in Slack"
"Perform common operations in Slack"
end
def self.to_param
......
class Timelog < ActiveRecord::Base
validates :time_spent, :user, presence: true
validate :issuable_id_is_present
belongs_to :trackable, polymorphic: true
belongs_to :issue
belongs_to :merge_request
belongs_to :user
def issuable
issue || merge_request
end
private
def issuable_id_is_present
if issue_id && merge_request_id
errors.add(:base, 'Only Issue ID or Merge Request ID is required')
elsif issuable.nil?
errors.add(:base, 'Issue or Merge Request ID is required')
end
end
end
......@@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity
expose :external_url
expose :environment_type
expose :last_deployment, using: DeploymentEntity
expose :stoppable?
expose :stop_action?
expose :environment_path do |environment|
namespace_project_environment_path(
......
......@@ -8,10 +8,9 @@ module Ci
return unless has_ref?
environments.each do |environment|
next unless environment.stoppable?
next unless can?(current_user, :create_deployment, project)
environment.stop!(current_user)
environment.stop_with_action!(current_user)
end
end
......
......@@ -3,6 +3,8 @@ module Issues
def execute
@request = params.delete(:request)
@api = params.delete(:api)
@recaptcha_verified = params.delete(:recaptcha_verified)
@spam_log_id = params.delete(:spam_log_id)
issue_attributes = params.merge(merge_request_for_resolving_discussions: merge_request_for_resolving_discussions)
@issue = BuildService.new(project, current_user, issue_attributes).execute
......@@ -11,7 +13,13 @@ module Issues
end
def before_create(issuable)
issuable.spam = spam_service.check(@api)
if @recaptcha_verified
spam_log = current_user.spam_logs.find_by(id: @spam_log_id, title: issuable.title)
spam_log.update!(recaptcha_verified: true) if spam_log
else
issuable.spam = spam_service.check(@api)
issuable.spam_log = spam_service.spam_log
end
end
def after_create(issuable)
......@@ -35,7 +43,7 @@ module Issues
private
def spam_service
SpamService.new(@issue, @request)
@spam_service ||= SpamService.new(@issue, @request)
end
def user_agent_detail_service
......
......@@ -25,9 +25,10 @@ module Projects
end
def transfer(project, new_namespace)
old_namespace = project.namespace
Project.transaction do
old_path = project.path_with_namespace
old_namespace = project.namespace
old_group = project.group
new_path = File.join(new_namespace.try(:path) || '', project.path)
......@@ -70,8 +71,11 @@ module Projects
project.old_path_with_namespace = old_path
SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end
refresh_permissions(old_namespace, new_namespace)
true
end
def allowed_transfer?(current_user, project, namespace)
......@@ -80,5 +84,14 @@ module Projects
namespace.id != project.namespace_id &&
current_user.can?(:create_projects, namespace)
end
def refresh_permissions(old_namespace, new_namespace)
# This ensures we only schedule 1 job for every user that has access to
# the namespaces.
user_ids = old_namespace.user_ids_for_project_authorizations |
new_namespace.user_ids_for_project_authorizations
UserProjectAccessChangedService.new(user_ids).execute
end
end
end
class SpamService
attr_accessor :spammable, :request, :options
attr_reader :spam_log
def initialize(spammable, request = nil)
@spammable = spammable
......@@ -63,7 +64,7 @@ class SpamService
end
def create_spam_log(api)
SpamLog.create(
@spam_log = SpamLog.create!(
{
user_id: spammable_owner_id,
title: spammable.spam_title,
......
......@@ -13,6 +13,8 @@
= spam_log.source_ip
%td
= spam_log.via_api? ? 'Y' : 'N'
%td
= spam_log.recaptcha_verified ? 'Y' : 'N'
%td
= spam_log.noteable_type
%td
......
......@@ -10,6 +10,7 @@
%th User
%th Source IP
%th API?
%th Recaptcha verified?
%th Type
%th Title
%th Description
......
......@@ -23,7 +23,7 @@
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div
- if current_application_settings.recaptcha_enabled
- if Gitlab::Recaptcha.enabled?
= recaptcha_tags
%div
= f.submit "Register", class: "btn-register btn"
......
......@@ -79,6 +79,14 @@
%td.shortcut
.key esc
%td Go back
%tbody
%tr
%th
%th Project File
%tr
%td.shortcut
.key y
%td Go to file permalink
.col-lg-4
%table.shortcut-mappings
......
......@@ -12,7 +12,7 @@
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
tree_join(@commit.sha, @path)), class: 'btn btn-sm'
tree_join(@commit.sha, @path)), class: 'btn btn-sm js-data-file-blob-permalink-url'
- if current_user
.btn-group{ role: "group" }
......
- if can?(current_user, :create_deployment, environment) && environment.stoppable?
- if can?(current_user, :create_deployment, environment) && environment.stop_action?
.inline
= link_to stop_namespace_project_environment_path(@project.namespace, @project, environment), method: :post,
class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do
......
......@@ -12,7 +12,7 @@
= render 'projects/environments/external_url', environment: @environment
- if can?(current_user, :update_environment, @environment)
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
- if can?(current_user, :create_deployment, @environment) && @environment.stoppable?
- if can?(current_user, :create_deployment, @environment) && @environment.can_stop?
= link_to 'Stop', stop_namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post
.deployments-container
......
......@@ -50,7 +50,7 @@
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
= link_to_label(label, subject: issue.project)
= link_to_label(label, subject: issue.project, css_class: 'label-link')
- if issue.tasks?
&nbsp;
%span.task-status
......
- page_title "Anti-spam verification"
%h3.page-title
Anti-spam verification
%hr
%p
We detected potential spam in the issue description. Please verify that you are not a robot to submit the issue.
= form_for [@project.namespace.becomes(Namespace), @project, @issue] do |f|
.recaptcha
- params[:issue].each do |field, value|
= hidden_field(:issue, field, value: value)
= hidden_field_tag(:merge_request_for_resolving_discussions, params[:merge_request_for_resolving_discussions])
= hidden_field_tag(:spam_log_id, @issue.spam_log.id)
= hidden_field_tag(:recaptcha_verification, true)
= recaptcha_tags
.row-content-block.footer-block
= f.submit "Submit #{@issue.class.model_name.human.downcase}", class: 'btn btn-create'
......@@ -64,7 +64,7 @@
- if merge_request.labels.any?
&nbsp;
- merge_request.labels.each do |label|
= link_to_label(label, subject: merge_request.project, type: :merge_request)
= link_to_label(label, subject: merge_request.project, type: :merge_request, css_class: 'label-link')
- if merge_request.tasks?
&nbsp;
......
- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
To setup this service:
%ul.list-unstyled
%p To setup this service:
%ul.list-unstyled.indent-list
%li
1.
= link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
= link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
Enable custom slash commands
= icon('external-link')
on your Mattermost installation
%li
2.
= link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
in Mattermost with these options:
= link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
Add a slash command
= icon('external-link')
in your Mattermost team with these options:
%hr
.help-form
......@@ -83,9 +86,14 @@ To setup this service:
%hr
%ul.list-unstyled
%ul.list-unstyled.indent-list
%li
3. After adding the slash command, paste the
%strong token
3. Paste the
%strong Token
into the field below
%li
4. Select the
%strong Active
checkbox, press
%strong Save changes
and start using GitLab inside Mattermost!
- enabled = Gitlab.config.mattermost.enabled
.well
This service allows GitLab users to perform common operations on this
project by entering slash commands in Mattermost.
%br
See list of available commands in Mattermost after setting up this service,
by entering
%code /&lt;command_trigger_word&gt; help
%p
This service allows users to perform common operations on this
project by entering slash commands in Mattermost.
= link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
View documentation
= icon('external-link')
%p.inline
See list of available commands in Mattermost after setting up this service,
by entering
%kbd.inline /&lt;trigger&gt; help
- unless enabled || @service.template?
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
......
- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path"
- run_actions_text = "Perform common operations on this project: #{pretty_name}"
- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
This service allows GitLab users to perform common operations on this
project by entering slash commands in Slack.
%br
See list of available commands in Slack after setting up this service,
by entering
%code /&lt;command&gt; help
%br
%br
%p
This service allows users to perform common operations on this
project by entering slash commands in Slack.
= link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
View documentation
= icon('external-link')
%p.inline
See list of available commands in Slack after setting up this service,
by entering
%kbd.inline /&lt;command&gt; help
- unless @service.template?
To setup this service:
%ul.list-unstyled
%p To setup this service:
%ul.list-unstyled.indent-list
%li
1.
= link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands'
= link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
Add a slash command
= icon('external-link')
in your Slack team with these options:
%hr
......@@ -82,7 +86,7 @@
%hr
%ul.list-unstyled
%ul.list-unstyled.indent-list
%li
2. Paste the
%strong Token
......
......@@ -66,7 +66,7 @@
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
......
......@@ -115,7 +115,7 @@
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do
%ul
......
---
title: Use reCaptcha when an issue is identified as a spam
merge_request: 8846
author:
---
title: Adds back ability to stop all environments
merge_request: 7379
author:
---
title: Fix Ctrl+Click support for Todos and Merge Request page tabs
merge_request: 8898
author:
---
title: Align Segoe UI label text
merge_request:
author:
---
title: Refactor Timelogs structure to use foreign keys.
merge_request: 8769
author:
---
title: 27352-search-label-filter-header
merge_request:
author:
---
title: Fix MR widget url
merge_request: 8989
author:
---
title: Layer award emoji dropdown over the right sidebar
merge_request: 9004
author:
---
title: Give ci status text on pipeline graph a better font-weight
merge_request:
author:
---
title: Add `y` keyboard shortcut to move to file permalink
merge_request:
author:
---
title: Fix broken anchor links when special characters are used
merge_request: 8961
author: Andrey Krivko
---
title: Refresh authorizations when transferring projects
merge_request:
author:
---
title: 'API: Remove /projects/:id/keys/.. endpoints'
merge_request: 8716
author: Robert Schilling
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddForeignKeysToTimelogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
def up
change_table :timelogs do |t|
t.column :issue_id, :integer
t.column :merge_request_id, :integer
end
add_concurrent_index :timelogs, :issue_id
add_concurrent_index :timelogs, :merge_request_id
if Gitlab::Database.postgresql?
execute <<-EOF
ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_issues_issue_id" FOREIGN KEY (issue_id) REFERENCES "issues" (id) ON DELETE CASCADE NOT VALID;
ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_merge_requests_merge_request_id" FOREIGN KEY (merge_request_id) REFERENCES "merge_requests" (id) ON DELETE CASCADE NOT VALID;
EOF
else
execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;"
execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_merge_requests_merge_request_id FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;"
end
Timelog.where(trackable_type: 'Issue').update_all("issue_id = trackable_id")
Timelog.where(trackable_type: 'MergeRequest').update_all("merge_request_id = trackable_id")
end
def down
Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'")
Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'")
remove_columns :timelogs, :issue_id, :merge_request_id
end
end
class AddRecaptchaVerifiedToSpamLogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
DOWNTIME = false
def up
add_column_with_default(:spam_logs, :recaptcha_verified, :boolean, default: false)
end
def down
remove_column(:spam_logs, :recaptcha_verified)
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
remove_columns :timelogs, :trackable_id, :trackable_type
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ValidateForeignKeysOnTimelogs < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
disable_ddl_transaction!
def up
if Gitlab::Database.postgresql?
execute <<-EOF
ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_issues_issue_id";
ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_merge_requests_merge_request_id";
EOF
end
end
def down
# noop
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170204181513) do
ActiveRecord::Schema.define(version: 20170206101030) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -1296,6 +1296,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "submitted_as_ham", default: false, null: false
t.boolean "recaptcha_verified", default: false, null: false
end
create_table "subscriptions", force: :cascade do |t|
......@@ -1332,14 +1333,15 @@ ActiveRecord::Schema.define(version: 20170204181513) do
create_table "timelogs", force: :cascade do |t|
t.integer "time_spent", null: false
t.integer "trackable_id"
t.string "trackable_type"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "issue_id"
t.integer "merge_request_id"
end
add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree
add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree
add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree
create_table "todos", force: :cascade do |t|
......@@ -1534,6 +1536,8 @@ ActiveRecord::Schema.define(version: 20170204181513) do
add_foreign_key "protected_branch_push_access_levels", "users"
add_foreign_key "remote_mirrors", "projects"
add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
......@@ -11,3 +11,4 @@ changes are in V4:
- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids`
- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`)
- Project snippets do not return deprecated field `expires_at`
- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`)
......@@ -5,6 +5,7 @@ module API
version %w(v3 v4), using: :path
version 'v3', using: :path do
mount ::API::V3::DeployKeys
mount ::API::V3::Issues
mount ::API::V3::MergeRequests
mount ::API::V3::Projects
......
module API
# Projects API
class DeployKeys < Grape::API
before { authenticate! }
......@@ -16,107 +15,102 @@ module API
resource :projects do
before { authorize_admin_project }
# Routing "projects/:id/keys/..." is DEPRECATED and WILL BE REMOVED in version 9.0
# Use "projects/:id/deploy_keys/..." instead.
#
%w(keys deploy_keys).each do |path|
desc "Get a specific project's deploy keys" do
success Entities::SSHKey
end
get ":id/#{path}" do
present user_project.deploy_keys, with: Entities::SSHKey
end
desc "Get a specific project's deploy keys" do
success Entities::SSHKey
end
get ":id/deploy_keys" do
present user_project.deploy_keys, with: Entities::SSHKey
end
desc 'Get single deploy key' do
success Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
get ":id/#{path}/:key_id" do
key = user_project.deploy_keys.find params[:key_id]
desc 'Get single deploy key' do
success Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
get ":id/deploy_keys/:key_id" do
key = user_project.deploy_keys.find params[:key_id]
present key, with: Entities::SSHKey
end
desc 'Add new deploy key to currently authenticated user' do
success Entities::SSHKey
end
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
end
post ":id/deploy_keys" do
params[:key].strip!
# Check for an existing key joined to this project
key = user_project.deploy_keys.find_by(key: params[:key])
if key
present key, with: Entities::SSHKey
break
end
desc 'Add new deploy key to currently authenticated user' do
success Entities::SSHKey
end
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: params[:key])
if key
user_project.deploy_keys << key
present key, with: Entities::SSHKey
break
end
post ":id/#{path}" do
params[:key].strip!
# Check for an existing key joined to this project
key = user_project.deploy_keys.find_by(key: params[:key])
if key
present key, with: Entities::SSHKey
break
end
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: params[:key])
if key
user_project.deploy_keys << key
present key, with: Entities::SSHKey
break
end
# Create a new deploy key
key = DeployKey.new(declared_params(include_missing: false))
if key.valid? && user_project.deploy_keys << key
present key, with: Entities::SSHKey
else
render_validation_error!(key)
end
# Create a new deploy key
key = DeployKey.new(declared_params(include_missing: false))
if key.valid? && user_project.deploy_keys << key
present key, with: Entities::SSHKey
else
render_validation_error!(key)
end
end
desc 'Enable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
post ":id/#{path}/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project,
current_user, declared_params).execute
desc 'Enable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
post ":id/deploy_keys/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project,
current_user, declared_params).execute
if key
present key, with: Entities::SSHKey
else
not_found!('Deploy Key')
end
if key
present key, with: Entities::SSHKey
else
not_found!('Deploy Key')
end
end
desc 'Disable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id/disable" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
key.destroy
desc 'Disable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/deploy_keys/:key_id/disable" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
key.destroy
present key.deploy_key, with: Entities::SSHKey
end
present key.deploy_key, with: Entities::SSHKey
end
desc 'Delete deploy key for a project' do
success Key
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
if key
key.destroy
else
not_found!('Deploy Key')
end
desc 'Delete deploy key for a project' do
success Key
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/deploy_keys/:key_id" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
if key
key.destroy
else
not_found!('Deploy Key')
end
end
end
......
module API
module V3
class DeployKeys < Grape::API
before { authenticate! }
get "deploy_keys" do
authenticated_as_admin!
keys = DeployKey.all
present keys, with: ::API::Entities::SSHKey
end
params do
requires :id, type: String, desc: 'The ID of the project'
end
resource :projects do
before { authorize_admin_project }
%w(keys deploy_keys).each do |path|
desc "Get a specific project's deploy keys" do
success ::API::Entities::SSHKey
end
get ":id/#{path}" do
present user_project.deploy_keys, with: ::API::Entities::SSHKey
end
desc 'Get single deploy key' do
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
get ":id/#{path}/:key_id" do
key = user_project.deploy_keys.find params[:key_id]
present key, with: ::API::Entities::SSHKey
end
desc 'Add new deploy key to currently authenticated user' do
success ::API::Entities::SSHKey
end
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
end
post ":id/#{path}" do
params[:key].strip!
# Check for an existing key joined to this project
key = user_project.deploy_keys.find_by(key: params[:key])
if key
present key, with: ::API::Entities::SSHKey
break
end
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: params[:key])
if key
user_project.deploy_keys << key
present key, with: ::API::Entities::SSHKey
break
end
# Create a new deploy key
key = DeployKey.new(declared_params(include_missing: false))
if key.valid? && user_project.deploy_keys << key
present key, with: ::API::Entities::SSHKey
else
render_validation_error!(key)
end
end
desc 'Enable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
post ":id/#{path}/:key_id/enable" do
key = ::Projects::EnableDeployKeyService.new(user_project,
current_user, declared_params).execute
if key
present key, with: ::API::Entities::SSHKey
else
not_found!('Deploy Key')
end
end
desc 'Disable a deploy key for a project' do
detail 'This feature was added in GitLab 8.11'
success ::API::Entities::SSHKey
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id/disable" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
key.destroy
present key.deploy_key, with: ::API::Entities::SSHKey
end
desc 'Delete deploy key for a project' do
success Key
end
params do
requires :key_id, type: Integer, desc: 'The ID of the deploy key'
end
delete ":id/#{path}/:key_id" do
key = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
if key
key.destroy
else
not_found!('Deploy Key')
end
end
end
end
end
end
end
......@@ -10,5 +10,9 @@ module Gitlab
true
end
end
def self.enabled?
current_application_settings.recaptcha_enabled
end
end
end
......@@ -163,20 +163,20 @@ namespace :gitlab do
namespace :pages do
task create: :environment do
$progress.puts "Dumping pages ... ".blue
$progress.puts "Dumping pages ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("pages")
$progress.puts "[SKIPPED]".cyan
$progress.puts "[SKIPPED]".color(:cyan)
else
Backup::Pages.new.dump
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
task restore: :environment do
$progress.puts "Restoring pages ... ".blue
$progress.puts "Restoring pages ... ".color(:blue)
Backup::Pages.new.restore
$progress.puts "done".green
$progress.puts "done".color(:green)
end
end
......
......@@ -326,7 +326,7 @@ describe Projects::IssuesController do
end
describe 'POST #create' do
def post_new_issue(attrs = {})
def post_new_issue(issue_attrs = {}, additional_params = {})
sign_in(user)
project = create(:empty_project, :public)
project.team << [user, :developer]
......@@ -334,8 +334,8 @@ describe Projects::IssuesController do
post :create, {
namespace_id: project.namespace.to_param,
project_id: project.to_param,
issue: { title: 'Title', description: 'Description' }.merge(attrs)
}
issue: { title: 'Title', description: 'Description' }.merge(issue_attrs)
}.merge(additional_params)
project.issues.first
end
......@@ -378,24 +378,81 @@ describe Projects::IssuesController do
context 'Akismet is enabled' do
before do
stub_application_setting(recaptcha_enabled: true)
allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true)
end
def post_spam_issue
post_new_issue(title: 'Spam Title', description: 'Spam lives here')
end
context 'when an issue is not identified as a spam' do
before do
allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false)
allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(false)
end
it 'rejects an issue recognized as spam' do
expect{ post_spam_issue }.not_to change(Issue, :count)
expect(response).to render_template(:new)
it 'does not create an issue' do
expect { post_new_issue(title: '') }.not_to change(Issue, :count)
end
end
it 'creates a spam log' do
post_spam_issue
spam_logs = SpamLog.all
expect(spam_logs.count).to eq(1)
expect(spam_logs[0].title).to eq('Spam Title')
context 'when an issue is identified as a spam' do
before { allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) }
context 'when captcha is not verified' do
def post_spam_issue
post_new_issue(title: 'Spam Title', description: 'Spam lives here')
end
before { allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(false) }
it 'rejects an issue recognized as a spam' do
expect { post_spam_issue }.not_to change(Issue, :count)
end
it 'creates a spam log' do
post_spam_issue
spam_logs = SpamLog.all
expect(spam_logs.count).to eq(1)
expect(spam_logs.first.title).to eq('Spam Title')
expect(spam_logs.first.recaptcha_verified).to be_falsey
end
it 'does not create an issue when it is not valid' do
expect { post_new_issue(title: '') }.not_to change(Issue, :count)
end
it 'does not create an issue when recaptcha is not enabled' do
stub_application_setting(recaptcha_enabled: false)
expect { post_spam_issue }.not_to change(Issue, :count)
end
end
context 'when captcha is verified' do
let!(:spam_logs) { create_list(:spam_log, 2, user: user, title: 'Title') }
def post_verified_issue
post_new_issue({}, { spam_log_id: spam_logs.last.id, recaptcha_verification: true } )
end
before do
allow_any_instance_of(described_class).to receive(:verify_recaptcha).and_return(true)
end
it 'accepts an issue after recaptcha is verified' do
expect { post_verified_issue }.to change(Issue, :count)
end
it 'marks spam log as recaptcha_verified' do
expect { post_verified_issue }.to change { SpamLog.last.recaptcha_verified }.from(false).to(true)
end
it 'does not mark spam log as recaptcha_verified when it does not belong to current_user' do
spam_log = create(:spam_log)
expect { post_new_issue({}, { spam_log_id: spam_log.id, recaptcha_verification: true } ) }.
not_to change { SpamLog.last.recaptcha_verified }
end
end
end
end
......@@ -405,7 +462,7 @@ describe Projects::IssuesController do
end
it 'creates a user agent detail' do
expect{ post_new_issue }.to change(UserAgentDetail, :count).by(1)
expect { post_new_issue }.to change(UserAgentDetail, :count).by(1)
end
end
......
......@@ -44,7 +44,7 @@ describe RegistrationsController do
post(:create, user_params)
expect(response).to render_template(:new)
expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please re-solve the reCAPTCHA.'
expect(flash[:alert]).to include 'There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.'
end
it 'redirects to the dashboard when the recaptcha is solved' do
......
......@@ -4,6 +4,6 @@ FactoryGirl.define do
factory :timelog do
time_spent 3600
user
association :trackable, factory: :issue
issue
end
end
......@@ -64,10 +64,6 @@ feature 'Environment', :feature do
expect(page).to have_link('Re-deploy')
end
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop')
end
scenario 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
......@@ -116,27 +112,43 @@ feature 'Environment', :feature do
end
end
context 'with stop action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
context 'when environment is available' do
context 'with stop action' do
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
scenario 'does show stop button' do
expect(page).to have_link('Stop')
end
scenario 'does show stop button' do
expect(page).to have_link('Stop')
end
scenario 'does allow to stop environment' do
click_link('Stop')
scenario 'does allow to stop environment' do
click_link('Stop')
expect(page).to have_content('close_app')
end
expect(page).to have_content('close_app')
end
context 'for reporter' do
let(:role) { :reporter }
context 'for reporter' do
let(:role) { :reporter }
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop')
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop')
end
end
end
context 'without stop action' do
scenario 'does allow to stop environment' do
click_link('Stop')
end
end
end
context 'when environment is stopped' do
given(:environment) { create(:environment, project: project, state: :stopped) }
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop')
end
end
end
end
......
......@@ -52,6 +52,22 @@ feature 'Environments page', :feature, :js do
scenario 'does show no deployments' do
expect(page).to have_content('No deployments yet')
end
context 'for available environment' do
given(:environment) { create(:environment, project: project, state: :available) }
scenario 'does not shows stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
end
context 'for stopped environment' do
given(:environment) { create(:environment, project: project, state: :stopped) }
scenario 'does not shows stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
end
end
context 'with deployments' do
......
require 'rails_helper'
describe 'New issue', feature: true do
include StubENV
let(:project) { create(:project, :public) }
let(:user) { create(:user)}
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
current_application_settings.update!(
akismet_enabled: true,
akismet_api_key: 'testkey',
recaptcha_enabled: true,
recaptcha_site_key: 'test site key',
recaptcha_private_key: 'test private key'
)
project.team << [user, :master]
login_as(user)
end
context 'when identified as a spam' do
before do
WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: "true", status: 200)
visit new_namespace_project_issue_path(project.namespace, project)
end
it 'creates an issue after solving reCaptcha' do
fill_in 'issue_title', with: 'issue title'
fill_in 'issue_description', with: 'issue description'
click_button 'Submit issue'
# it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
# recaptcha verification is skipped in test environment and it always returns true
expect(page).not_to have_content('issue title')
expect(page).to have_css('.recaptcha')
click_button 'Submit issue'
expect(page.find('.issue-details h2.title')).to have_content('issue title')
expect(page.find('.issue-details .description')).to have_content('issue description')
end
end
context 'when not identified as a spam' do
before do
WebMock.stub_request(:any, /.*akismet.com.*/).to_return(body: 'false', status: 200)
visit new_namespace_project_issue_path(project.namespace, project)
end
it 'creates an issue' do
fill_in 'issue_title', with: 'issue title'
fill_in 'issue_description', with: 'issue description'
click_button 'Submit issue'
expect(page.find('.issue-details h2.title')).to have_content('issue title')
expect(page.find('.issue-details .description')).to have_content('issue description')
end
end
end
require 'spec_helper'
feature 'Blob shortcuts', feature: true do
include TreeHelper
let(:project) { create(:project, :public, :repository) }
let(:path) { project.repository.ls_files(project.repository.root_ref)[0] }
let(:sha) { project.repository.commit.sha }
describe 'On a file(blob)', js: true do
def get_absolute_url(path = "")
"http://#{page.server.host}:#{page.server.port}#{path}"
end
def visit_blob(fragment = nil)
visit namespace_project_blob_path(project.namespace, project, tree_join('master', path), anchor: fragment)
end
describe 'pressing "y"' do
it 'redirects to permalink with commit sha' do
visit_blob
find('body').native.send_key('y')
expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path))), url: true)
end
it 'maintains fragment hash when redirecting' do
fragment = "L1"
visit_blob(fragment)
find('body').native.send_key('y')
expect(page).to have_current_path(get_absolute_url(namespace_project_blob_path(project.namespace, project, tree_join(sha, path), anchor: fragment)), url: true)
end
end
end
end
require 'spec_helper'
feature 'Setup Mattermost slash commands', feature: true do
include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service }
......@@ -15,11 +13,15 @@ feature 'Setup Mattermost slash commands', feature: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
end
describe 'user visits the mattermost slash command config page', js: true do
describe 'user visits the mattermost slash command config page' do
it 'shows a help message' do
wait_for_ajax
expect(page).to have_content("This service allows users to perform common")
end
it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
expect(page).to have_content("This service allows GitLab users to perform common")
expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
it 'shows the token after saving' do
......@@ -64,7 +66,7 @@ feature 'Setup Mattermost slash commands', feature: true do
select_element = find('select#mattermost_team_id')
selected_option = select_element.find('option[selected]')
expect(select_element['disabled']).to be(true)
expect(select_element['disabled']).to eq('disabled')
expect(selected_option).to have_content(team_name.to_s)
end
......@@ -93,7 +95,7 @@ feature 'Setup Mattermost slash commands', feature: true do
select_element = find('select#mattermost_team_id')
selected_option = select_element.find('option[selected]')
expect(select_element['disabled']).to be(false)
expect(select_element['disabled']).to be(nil)
expect(selected_option).to have_content('Select team...')
# The 'Select team...' placeholder is item `0`.
expect(select_element.all('option').count).to eq(3)
......@@ -135,6 +137,12 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
end
it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
end
end
......
require 'spec_helper'
feature 'Slack slash commands', feature: true do
include WaitForAjax
given(:user) { create(:user) }
given(:project) { create(:project) }
given(:service) { project.create_slack_slash_commands_service }
......@@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do
background do
project.team << [user, :master]
login_as(user)
end
scenario 'user visits the slack slash command config page and shows a help message', js: true do
visit edit_namespace_project_service_path(project.namespace, project, service)
end
wait_for_ajax
it 'shows a token placeholder' do
token_placeholder = find_field('service_token')['placeholder']
expect(page).to have_content('This service allows GitLab users to perform common')
expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
scenario 'shows the token after saving' do
visit edit_namespace_project_service_path(project.namespace, project, service)
it 'shows a help message' do
expect(page).to have_content('This service allows users to perform common')
end
it 'shows the token after saving' do
fill_in 'service_token', with: 'token'
click_on 'Save'
......@@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do
expect(value).to eq('token')
end
scenario 'shows the correct trigger url' do
visit edit_namespace_project_service_path(project.namespace, project, service)
it 'shows the correct trigger url' do
value = find_field('url').value
expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger")
end
......
......@@ -63,9 +63,11 @@ describe MergeRequestsHelper do
end
end
describe 'mr_widget_refresh_url' do
let(:project) { create(:empty_project) }
let(:merge_request) { create(:merge_request, source_project: project) }
describe '#mr_widget_refresh_url' do
let(:guest) { create(:user) }
let(:project) { create(:project, :public) }
let(:project_fork) { Projects::ForkService.new(project, guest).execute }
let(:merge_request) { create(:merge_request, source_project: project_fork, target_project: project) }
it 'returns correct url for MR' do
expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh"
......@@ -74,7 +76,7 @@ describe MergeRequestsHelper do
end
it 'returns empty string for nil' do
expect(mr_widget_refresh_url(nil)).to end_with('')
expect(mr_widget_refresh_url(nil)).to eq('')
end
end
end
......@@ -119,7 +119,7 @@ describe('Environment item', () => {
},
],
},
'stoppable?': true,
'stop_action?': true,
environment_path: 'root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
......
......@@ -50,7 +50,7 @@ const environmentsList = [
},
manual_actions: [],
},
'stoppable?': true,
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
......@@ -105,7 +105,7 @@ const environmentsList = [
},
manual_actions: [],
},
'stoppable?': false,
'stop_action?': false,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
......@@ -116,7 +116,7 @@ const environmentsList = [
state: 'available',
environment_type: 'review',
last_deployment: null,
'stoppable?': true,
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
......@@ -127,7 +127,7 @@ const environmentsList = [
state: 'available',
environment_type: 'review',
last_deployment: null,
'stoppable?': true,
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
......@@ -143,7 +143,7 @@ const environment = {
external_url: 'http://production.',
environment_type: null,
last_deployment: {},
'stoppable?': false,
'stop_action?': false,
environment_path: '/root/review-app/environments/4',
stop_path: '/root/review-app/environments/4/stop',
created_at: '2016-12-16T11:51:04.690Z',
......
......@@ -41,6 +41,19 @@ require('~/lib/utils/common_utils');
});
});
describe('gl.utils.handleLocationHash', () => {
beforeEach(() => {
window.history.pushState({}, null, '#definição');
});
it('decodes hash parameter', () => {
spyOn(window.document, 'getElementById').and.callThrough();
gl.utils.handleLocationHash();
expect(window.document.getElementById).toHaveBeenCalledWith('definição');
expect(window.document.getElementById).toHaveBeenCalledWith('user-content-definição');
});
});
describe('gl.utils.getParameterByName', () => {
beforeEach(() => {
window.history.pushState({}, null, '?scope=all&p=2');
......@@ -73,5 +86,37 @@ require('~/lib/utils/common_utils');
expect(normalized[NGINX].nginx).toBe('ok');
});
});
describe('gl.utils.isMetaClick', () => {
it('should identify meta click on Windows/Linux', () => {
const e = {
metaKey: false,
ctrlKey: true,
which: 1,
};
expect(gl.utils.isMetaClick(e)).toBe(true);
});
it('should identify meta click on macOS', () => {
const e = {
metaKey: true,
ctrlKey: false,
which: 1,
};
expect(gl.utils.isMetaClick(e)).toBe(true);
});
it('should identify as meta click on middle-click or Mouse-wheel click', () => {
const e = {
metaKey: false,
ctrlKey: false,
which: 2,
};
expect(gl.utils.isMetaClick(e)).toBe(true);
});
});
});
})();
......@@ -61,6 +61,56 @@ require('vendor/jquery.scrollTo');
expect($('#diffs')).toHaveClass('active');
});
});
describe('#opensInNewTab', function () {
var commitsLink;
var tabUrl;
beforeEach(function () {
commitsLink = '.commits-tab li a';
tabUrl = $(commitsLink).attr('href');
spyOn($.fn, 'attr').and.returnValue(tabUrl);
});
it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () {
spyOn(window, 'open').and.callFake(function (url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual('_blank');
});
this.class.clickTab({
metaKey: false,
ctrlKey: true,
which: 1,
stopImmediatePropagation: function () {}
});
});
it('opens page tab in a new browser tab with Cmd+Click - Mac', function () {
spyOn(window, 'open').and.callFake(function (url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual('_blank');
});
this.class.clickTab({
metaKey: true,
ctrlKey: false,
which: 1,
stopImmediatePropagation: function () {}
});
});
it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () {
spyOn(window, 'open').and.callFake(function (url, name) {
expect(url).toEqual(tabUrl);
expect(name).toEqual('_blank');
});
this.class.clickTab({
metaKey: false,
ctrlKey: false,
which: 2,
stopImmediatePropagation: function () {}
});
});
});
describe('#setCurrentAction', function () {
beforeEach(function () {
......
......@@ -221,5 +221,6 @@ award_emoji:
priorities:
- label
timelogs:
- trackable
- issue
- merge_request
- user
......@@ -357,8 +357,8 @@ LabelPriority:
Timelog:
- id
- time_spent
- trackable_id
- trackable_type
- merge_request_id
- issue_id
- user_id
- created_at
- updated_at
......@@ -77,8 +77,8 @@ describe Deployment, models: true do
end
end
describe '#stoppable?' do
subject { deployment.stoppable? }
describe '#stop_action?' do
subject { deployment.stop_action? }
context 'when no other actions' do
let(:deployment) { build(:deployment) }
......
......@@ -112,8 +112,8 @@ describe Environment, models: true do
end
end
describe '#stoppable?' do
subject { environment.stoppable? }
describe '#stop_action?' do
subject { environment.stop_action? }
context 'when no other actions' do
it { is_expected.to be_falsey }
......@@ -142,17 +142,39 @@ describe Environment, models: true do
end
end
describe '#stop!' do
describe '#stop_with_action!' do
let(:user) { create(:user) }
subject { environment.stop!(user) }
subject { environment.stop_with_action!(user) }
before do
expect(environment).to receive(:stoppable?).and_call_original
expect(environment).to receive(:available?).and_call_original
end
context 'when no other actions' do
it { is_expected.to be_nil }
context 'environment is available' do
before do
environment.update(state: :available)
end
it do
subject
expect(environment).to be_stopped
end
end
context 'environment is already stopped' do
before do
environment.update(state: :stopped)
end
it do
subject
expect(environment).to be_stopped
end
end
end
context 'when matching action is defined' do
......
......@@ -330,4 +330,17 @@ describe Group, models: true do
expect(group.members_with_parents).to include(master)
end
end
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
master = create(:user)
developer = create(:user)
group.add_user(master, GroupMember::MASTER)
group.add_user(developer, GroupMember::DEVELOPER)
expect(group.user_ids_for_project_authorizations).
to include(master.id, developer.id)
end
end
end
......@@ -230,4 +230,11 @@ describe Namespace, models: true do
expect(group.descendants.to_a).to eq([nested_group, deep_nested_group, very_deep_nested_group])
end
end
describe '#user_ids_for_project_authorizations' do
it 'returns the user IDs for which to refresh authorizations' do
expect(namespace.user_ids_for_project_authorizations).
to eq([namespace.owner_id])
end
end
end
......@@ -2,9 +2,37 @@ require 'rails_helper'
RSpec.describe Timelog, type: :model do
subject { build(:timelog) }
let(:issue) { create(:issue) }
let(:merge_request) { create(:merge_request) }
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:time_spent) }
it { is_expected.to validate_presence_of(:user) }
describe 'Issuable validation' do
it 'is invalid if issue_id and merge_request_id are missing' do
subject.attributes = { issue: nil, merge_request: nil }
expect(subject).to be_invalid
end
it 'is invalid if issue_id and merge_request_id are set' do
subject.attributes = { issue: issue, merge_request: merge_request }
expect(subject).to be_invalid
end
it 'is valid if only issue_id is set' do
subject.attributes = { issue: issue, merge_request: nil }
expect(subject).to be_valid
end
it 'is valid if only merge_request_id is set' do
subject.attributes = { merge_request: merge_request, issue: nil }
expect(subject).to be_valid
end
end
end
require 'spec_helper'
describe API::V3::DeployKeys, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:empty_project, creator_id: user.id) }
let(:project2) { create(:empty_project, creator_id: user.id) }
let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
create(:deploy_keys_project, project: project, deploy_key: deploy_key)
end
describe 'GET /deploy_keys' do
context 'when unauthenticated' do
it 'should return authentication error' do
get v3_api('/deploy_keys')
expect(response.status).to eq(401)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 403 error' do
get v3_api('/deploy_keys', user)
expect(response.status).to eq(403)
end
end
context 'when authenticated as admin' do
it 'should return all deploy keys' do
get v3_api('/deploy_keys', admin)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id)
end
end
end
%w(deploy_keys keys).each do |path|
describe "GET /projects/:id/#{path}" do
before { deploy_key }
it 'should return array of ssh keys' do
get v3_api("/projects/#{project.id}/#{path}", admin)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(deploy_key.title)
end
end
describe "GET /projects/:id/#{path}/:key_id" do
it 'should return a single key' do
get v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(deploy_key.title)
end
it 'should return 404 Not Found with invalid ID' do
get v3_api("/projects/#{project.id}/#{path}/404", admin)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/deploy_keys" do
it 'should not create an invalid ssh key' do
post v3_api("/projects/#{project.id}/#{path}", admin), { title: 'invalid key' }
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('key is missing')
end
it 'should not create a key without title' do
post v3_api("/projects/#{project.id}/#{path}", admin), key: 'some key'
expect(response).to have_http_status(400)
expect(json_response['error']).to eq('title is missing')
end
it 'should create new ssh key' do
key_attrs = attributes_for :another_key
expect do
post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
end.to change{ project.deploy_keys.count }.by(1)
end
it 'returns an existing ssh key when attempting to add a duplicate' do
expect do
post v3_api("/projects/#{project.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
end.not_to change { project.deploy_keys.count }
expect(response).to have_http_status(201)
end
it 'joins an existing ssh key to a new project' do
expect do
post v3_api("/projects/#{project2.id}/#{path}", admin), { key: deploy_key.key, title: deploy_key.title }
end.to change { project2.deploy_keys.count }.by(1)
expect(response).to have_http_status(201)
end
end
describe "DELETE /projects/:id/#{path}/:key_id" do
before { deploy_key }
it 'should delete existing key' do
expect do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}", admin)
end.to change{ project.deploy_keys.count }.by(-1)
end
it 'should return 404 Not Found with invalid ID' do
delete v3_api("/projects/#{project.id}/#{path}/404", admin)
expect(response).to have_http_status(404)
end
end
describe "POST /projects/:id/#{path}/:key_id/enable" do
let(:project2) { create(:empty_project) }
context 'when the user can admin the project' do
it 'enables the key' do
expect do
post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", admin)
end.to change { project2.deploy_keys.count }.from(0).to(1)
expect(response).to have_http_status(201)
expect(json_response['id']).to eq(deploy_key.id)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 404 error' do
post v3_api("/projects/#{project2.id}/#{path}/#{deploy_key.id}/enable", user)
expect(response).to have_http_status(404)
end
end
end
describe "DELETE /projects/:id/deploy_keys/:key_id/disable" do
context 'when the user can admin the project' do
it 'disables the key' do
expect do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", admin)
end.to change { project.deploy_keys.count }.from(1).to(0)
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(deploy_key.id)
end
end
context 'when authenticated as non-admin user' do
it 'should return a 404 error' do
delete v3_api("/projects/#{project.id}/#{path}/#{deploy_key.id}/disable", user)
expect(response).to have_http_status(404)
end
end
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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