Commit d5549025 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-upstream' into 'master'

CE upstream

Closes gitlab-ce#26813 and gitlab-ce#25989

See merge request !1102
parents 6fb89ee2 6c404fa9
......@@ -21,7 +21,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries
gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth', '~> 1.3.2'
gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2'
......
......@@ -473,7 +473,7 @@ GEM
octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4)
omniauth (1.3.1)
omniauth (1.3.2)
hashie (>= 1.2, < 4)
rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1)
......@@ -958,7 +958,7 @@ DEPENDENCIES
oauth2 (~> 1.2.0)
octokit (~> 4.6.2)
oj (~> 2.17.4)
omniauth (~> 1.3.1)
omniauth (~> 1.3.2)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.2.0)
omniauth-azure-oauth2 (~> 0.0.6)
......
......@@ -143,4 +143,4 @@ Please see [Getting help for GitLab](https://about.gitlab.com/getting-help/) on
## Is it awesome?
Thanks for [asking this question](https://twitter.com/supersloth/status/489462789384056832) Joshua.
[These people](https://twitter.com/gitlab/favorites) seem to like it.
[These people](https://twitter.com/gitlab/likes) seem to like it.
......@@ -20,6 +20,9 @@
if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) {
this.dismissDropdown();
} else if (selected.getAttribute('data-action') === 'submit') {
this.dismissDropdown();
this.dispatchFormSubmitEvent();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
......
......@@ -39,6 +39,7 @@
}
this.dismissDropdown();
this.dispatchInputEvent();
}
}
......@@ -84,6 +85,12 @@
}));
}
dispatchFormSubmitEvent() {
// dispatchEvent() is necessary as form.submit() does not
// trigger event handlers
this.input.form.dispatchEvent(new Event('submit'));
}
hideDropdown() {
this.getCurrentHook().list.hide();
}
......
......@@ -66,11 +66,19 @@
const word = `${tokenName}:${tokenValue}`;
// Get the string to replace
const selectionStart = input.selectionStart;
let newCaretPosition = input.selectionStart;
const { left, right } = gl.DropdownUtils.getInputSelectionPosition(input);
input.value = `${inputValue.substr(0, left)}${word}${inputValue.substr(right)}`;
gl.FilteredSearchDropdownManager.updateInputCaretPosition(selectionStart, input);
// If we have added a tokenValue at the end of the input,
// add a space and set selection to the end
if (right >= inputValue.length && tokenValue !== '') {
input.value += ' ';
newCaretPosition = input.value.length;
}
gl.FilteredSearchDropdownManager.updateInputCaretPosition(newCaretPosition, input);
}
static updateInputCaretPosition(selectionStart, input) {
......
......@@ -25,6 +25,7 @@
}
bindEvents() {
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
......@@ -32,6 +33,7 @@
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.tokenChange = this.tokenChange.bind(this);
this.filteredSearchInput.form.addEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
......@@ -42,6 +44,7 @@
}
unbindEvents() {
this.filteredSearchInput.form.removeEventListener('submit', this.handleFormSubmit);
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
......@@ -88,6 +91,11 @@
this.dropdownManager.resetDropdowns();
}
handleFormSubmit(e) {
e.preventDefault();
this.search();
}
loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray();
const usernameParams = this.getUsernameParams();
......
......@@ -15,6 +15,7 @@
}
.ci-status-icon-pending,
.ci-status-icon-failed_with_warnings,
.ci-status-icon-success_with_warnings {
color: $gl-warning;
......
......@@ -46,10 +46,6 @@
font-weight: bold;
}
.fa-clipboard {
color: $dropdown-title-btn-color;
}
.commit-info {
&.branches {
margin-left: 8px;
......
......@@ -195,10 +195,10 @@ ul.notes {
}
.note-body {
overflow: auto;
overflow-x: auto;
overflow-y: hidden;
.note-text {
overflow: auto;
word-wrap: break-word;
@include md-typography;
// Reset ul style types since we're nested inside a ul already
......
......@@ -18,7 +18,6 @@
.file-finder-input:hover,
.issuable-search-form:hover,
.search-text-input:hover,
textarea:hover,
.form-control:hover {
border-color: lighten($dropdown-input-focus-border, 20%);
box-shadow: 0 0 4px lighten($search-input-focus-shadow-color, 20%);
......
......@@ -19,7 +19,8 @@
overflow: visible;
}
&.ci-failed {
&.ci-failed,
&.ci-failed_with_warnings {
color: $gl-danger;
border-color: $gl-danger;
......
module ServicesHelper
def service_event_description(event)
case event
when "push"
when "push", "push_events"
"Event will be triggered by a push to the repository"
when "tag_push"
when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository"
when "note"
when "note", "note_events"
"Event will be triggered when someone adds a comment"
when "issue"
when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed"
when "confidential_issue"
when "confidential_issue", "confidential_issue_events"
"Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request"
when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
when "build"
when "build", "build_events"
"Event will be triggered when a build status changes"
when "wiki_page"
when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
when "commit"
when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
end
end
......@@ -26,4 +26,6 @@ module ServicesHelper
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
end
extend self
end
......@@ -108,15 +108,11 @@ class Notify < BaseMailer
def mail_thread(model, headers = {})
add_project_headers
add_unsubscription_headers_and_links
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end
if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
......@@ -172,4 +168,16 @@ class Notify < BaseMailer
headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end
def add_unsubscription_headers_and_links
return unless !@labels_url && @sent_notification && @sent_notification.unsubscribable?
list_unsubscribe_methods = [unsubscribe_sent_notification_url(@sent_notification, force: true)]
if Gitlab::IncomingEmail.enabled? && Gitlab::IncomingEmail.supports_wildcard?
list_unsubscribe_methods << "mailto:#{Gitlab::IncomingEmail.unsubscribe_address(reply_key)}"
end
headers['List-Unsubscribe'] = list_unsubscribe_methods.map { |e| "<#{e}>" }.join(',')
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end
end
......@@ -128,16 +128,21 @@ module Ci
end
def stages
# TODO, this needs refactoring, see gitlab-ce#26481.
stages_query = statuses
.group('stage').select(:stage).order('max(stage_idx)')
status_sql = statuses.latest.where('stage=sg.stage').status_sql
stages_query = statuses.group('stage').select(:stage)
.order('max(stage_idx)')
warnings_sql = statuses.latest.select('COUNT(*) > 0')
.where('stage=sg.stage').failed_but_allowed.to_sql
stages_with_statuses = CommitStatus.from(stages_query, :sg).
pluck('sg.stage', status_sql)
stages_with_statuses = CommitStatus.from(stages_query, :sg)
.pluck('sg.stage', status_sql, "(#{warnings_sql})")
stages_with_statuses.map do |stage|
Ci::Stage.new(self, name: stage.first, status: stage.last)
Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
end
end
......
......@@ -8,10 +8,11 @@ module Ci
delegate :project, to: :pipeline
def initialize(pipeline, name:, status: nil)
def initialize(pipeline, name:, status: nil, warnings: nil)
@pipeline = pipeline
@name = name
@status = status
@warnings = warnings
end
def to_param
......@@ -39,5 +40,17 @@ module Ci
def builds
@builds ||= pipeline.builds.where(stage: name)
end
def success?
status.to_s == 'success'
end
def has_warnings?
if @warnings.nil?
statuses.latest.failed_but_allowed.any?
else
@warnings
end
end
end
end
module HasStatus
extend ActiveSupport::Concern
DEFAULT_STATUS = 'created'
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
STARTED_STATUSES = %w[running success failed skipped]
ACTIVE_STATUSES = %w[pending running]
......
......@@ -133,6 +133,8 @@ class Namespace < ActiveRecord::Base
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
Gitlab::PagesTransfer.new.rename_namespace(path_was, path)
remove_exports!
# If repositories moved successfully we need to
# send update instructions to users.
# However we cannot allow rollback since we moved namespace dir
......@@ -225,6 +227,8 @@ class Namespace < ActiveRecord::Base
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path)
end
end
remove_exports!
end
def refresh_access_of_projects_invited_groups
......@@ -237,4 +241,20 @@ class Namespace < ActiveRecord::Base
def full_path_changed?
path_changed? || parent_id_changed?
end
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
def export_path
File.join(Gitlab::ImportExport.storage_path, full_path_was)
end
def full_path_was
if parent
parent.full_path + '/' + path_was
else
path_was
end
end
end
......@@ -25,7 +25,7 @@ You can create a Personal Access Token here:
http://app.asana.com/-/account_api'
end
def to_param
def self.to_param
'asana'
end
......@@ -44,7 +44,7 @@ http://app.asana.com/-/account_api'
]
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -12,7 +12,7 @@ class AssemblaService < Service
'Project Management Software (Source Commits Endpoint)'
end
def to_param
def self.to_param
'assembla'
end
......@@ -23,7 +23,7 @@ class AssemblaService < Service
]
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -40,7 +40,7 @@ class BambooService < CiService
'You must set up automatic revision labeling and a repository trigger in Bamboo.'
end
def to_param
def self.to_param
'bamboo'
end
......@@ -56,10 +56,6 @@ class BambooService < CiService
]
end
def supported_events
%w(push)
end
def build_page(sha, ref)
with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end
......
......@@ -19,7 +19,7 @@ class BugzillaService < IssueTrackerService
end
end
def to_param
def self.to_param
'bugzilla'
end
end
......@@ -24,10 +24,6 @@ class BuildkiteService < CiService
hook.save
end
def supported_events
%w(push)
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
......@@ -54,7 +50,7 @@ class BuildkiteService < CiService
'Continuous integration and deployments'
end
def to_param
def self.to_param
'buildkite'
end
......
......@@ -19,11 +19,11 @@ class BuildsEmailService < Service
'Email the builds status to a list of recipients.'
end
def to_param
def self.to_param
'builds_email'
end
def supported_events
def self.supported_events
%w(build)
end
......
......@@ -12,7 +12,7 @@ class CampfireService < Service
'Simple web-based real-time group chat'
end
def to_param
def self.to_param
'campfire'
end
......@@ -24,7 +24,7 @@ class CampfireService < Service
]
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -25,7 +25,7 @@ class ChatNotificationService < Service
valid?
end
def supported_events
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page]
end
......@@ -82,19 +82,19 @@ class ChatNotificationService < Service
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
PushMessage.new(data)
ChatMessage::PushMessage.new(data)
when "issue"
IssueMessage.new(data) unless is_update?(data)
ChatMessage::IssueMessage.new(data) unless is_update?(data)
when "merge_request"
MergeMessage.new(data) unless is_update?(data)
ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note"
NoteMessage.new(data)
ChatMessage::NoteMessage.new(data)
when "build"
BuildMessage.new(data) if should_build_be_notified?(data)
ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline"
PipelineMessage.new(data) if should_pipeline_be_notified?(data)
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page"
WikiPageMessage.new(data)
ChatMessage::WikiPageMessage.new(data)
end
end
......
......@@ -13,8 +13,8 @@ class ChatSlashCommandsService < Service
ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def supported_events
[]
def self.supported_events
%w()
end
def can_test?
......
......@@ -8,7 +8,7 @@ class CiService < Service
self.respond_to?(:token) && self.token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -23,7 +23,7 @@ class CustomIssueTrackerService < IssueTrackerService
end
end
def to_param
def self.to_param
'custom_issue_tracker'
end
......
......@@ -5,8 +5,8 @@
class DeploymentService < Service
default_value_for :category, 'deployment'
def supported_events
[]
def self.supported_events
%w()
end
def predefined_variables
......
......@@ -32,7 +32,7 @@ class DroneCiService < CiService
true
end
def supported_events
def self.supported_events
%w(push merge_request tag_push)
end
......@@ -87,7 +87,7 @@ class DroneCiService < CiService
'Drone is a Continuous Integration platform built on Docker, written in Go'
end
def to_param
def self.to_param
'drone_ci'
end
......
......@@ -12,11 +12,11 @@ class EmailsOnPushService < Service
'Email the commits and diff of each push to a list of recipients.'
end
def to_param
def self.to_param
'emails_on_push'
end
def supported_events
def self.supported_events
%w(push tag_push)
end
......
......@@ -13,7 +13,7 @@ class ExternalWikiService < Service
'Replaces the link to the internal wiki with a link to an external wiki.'
end
def to_param
def self.to_param
'external_wiki'
end
......@@ -29,4 +29,8 @@ class ExternalWikiService < Service
nil
end
end
def self.supported_events
%w()
end
end
......@@ -12,7 +12,7 @@ class FlowdockService < Service
'Flowdock is a collaboration web app for technical teams.'
end
def to_param
def self.to_param
'flowdock'
end
......@@ -22,7 +22,7 @@ class FlowdockService < Service
]
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -12,7 +12,7 @@ class GemnasiumService < Service
'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.'
end
def to_param
def self.to_param
'gemnasium'
end
......@@ -23,7 +23,7 @@ class GemnasiumService < Service
]
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -7,7 +7,7 @@ class GitlabIssueTrackerService < IssueTrackerService
default_value_for :default, true
def to_param
def self.to_param
'gitlab'
end
......
......@@ -27,7 +27,7 @@ class HipchatService < Service
'Private group chat and IM'
end
def to_param
def self.to_param
'hipchat'
end
......@@ -45,7 +45,7 @@ class HipchatService < Service
]
end
def supported_events
def self.supported_events
%w(push issue confidential_issue merge_request note tag_push build)
end
......
......@@ -17,11 +17,11 @@ class IrkerService < Service
'gateway.'
end
def to_param
def self.to_param
'irker'
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -61,7 +61,7 @@ class IssueTrackerService < Service
end
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -33,7 +33,7 @@ class JenkinsDeprecatedService < CiService
'is deprecated. Use "Jenkins CI" service instead.'
end
def to_param
def self.to_param
'jenkins_deprecated'
end
......
......@@ -52,7 +52,7 @@ class JenkinsService < CiService
File.join(jenkins_url, "project/#{project_name}").to_s
end
def supported_events
def self.supported_events
%w(push merge_request tag_push)
end
......@@ -68,7 +68,7 @@ class JenkinsService < CiService
'You must have installed the Git Plugin and GitLab Plugin in Jenkins'
end
def to_param
def self.to_param
'jenkins'
end
......
......@@ -12,7 +12,7 @@ class JiraService < IssueTrackerService
# This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service
# configuration screen.
def supported_events
def self.supported_events
%w(commit merge_request)
end
......@@ -81,7 +81,7 @@ class JiraService < IssueTrackerService
end
end
def to_param
def self.to_param
'jira'
end
......
......@@ -52,7 +52,7 @@ class KubernetesService < DeploymentService
'deployments with `app=$CI_ENVIRONMENT_SLUG`'
end
def to_param
def self.to_param
'kubernetes'
end
......
......@@ -7,7 +7,7 @@ class MattermostService < ChatNotificationService
'Receive event notifications in Mattermost'
end
def to_param
def self.to_param
'mattermost'
end
......@@ -36,6 +36,6 @@ class MattermostService < ChatNotificationService
end
def default_channel_placeholder
"#town-square"
"town-square"
end
end
......@@ -15,7 +15,7 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
"Perform common operations on GitLab in Mattermost"
end
def to_param
def self.to_param
'mattermost_slash_commands'
end
......
......@@ -15,11 +15,11 @@ class PipelinesEmailService < Service
'Email the pipelines status to a list of recipients.'
end
def to_param
def self.to_param
'pipelines_email'
end
def supported_events
def self.supported_events
%w[pipeline]
end
......
......@@ -14,7 +14,7 @@ class PivotaltrackerService < Service
'Project Management Software (Source Commits Endpoint)'
end
def to_param
def self.to_param
'pivotaltracker'
end
......@@ -34,7 +34,7 @@ class PivotaltrackerService < Service
]
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -13,7 +13,7 @@ class PushoverService < Service
'Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.'
end
def to_param
def self.to_param
'pushover'
end
......@@ -61,7 +61,7 @@ class PushoverService < Service
]
end
def supported_events
def self.supported_events
%w(push)
end
......
......@@ -19,7 +19,7 @@ class RedmineService < IssueTrackerService
end
end
def to_param
def self.to_param
'redmine'
end
end
......@@ -7,7 +7,7 @@ class SlackService < ChatNotificationService
'Receive event notifications in Slack'
end
def to_param
def self.to_param
'slack'
end
......
......@@ -9,7 +9,7 @@ class SlackSlashCommandsService < ChatSlashCommandsService
"Perform common operations on GitLab in Slack"
end
def to_param
def self.to_param
'slack_slash_commands'
end
......
......@@ -43,14 +43,10 @@ class TeamcityService < CiService
'requests build, that setting is in the vsc root advanced settings.'
end
def to_param
def self.to_param
'teamcity'
end
def supported_events
%w(push)
end
def fields
[
{ type: 'text', name: 'teamcity_url',
......
......@@ -76,6 +76,11 @@ class Service < ActiveRecord::Base
def to_param
# implement inside child
self.class.to_param
end
def self.to_param
raise NotImplementedError
end
def fields
......@@ -92,7 +97,11 @@ class Service < ActiveRecord::Base
end
def event_names
supported_events.map { |event| "#{event}_events" }
self.class.event_names
end
def self.event_names
self.supported_events.map { |event| "#{event}_events" }
end
def event_field(event)
......@@ -104,6 +113,10 @@ class Service < ActiveRecord::Base
end
def supported_events
self.class.supported_events
end
def self.supported_events
%w(push tag_push issue confidential_issue merge_request wiki_page)
end
......
......@@ -17,7 +17,7 @@
= icon('times')
#js-dropdown-hint.dropdown-menu.hint-dropdown
%ul{ 'data-dropdown' => true }
%li.filter-dropdown-item{ 'data-value' => '' }
%li.filter-dropdown-item{ 'data-action' => 'submit' }
%button.btn.btn-link
= icon('search')
%span
......@@ -139,10 +139,6 @@
new MilestoneSelect();
new IssueStatusSelect();
new SubscriptionSelect();
$('form.filter-form').on('submit', function (event) {
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
});
$(document).off('page:restore').on('page:restore', function (event) {
if (gl.FilteredSearchManager) {
......
---
title: Handle unsubscribe from email notifications via replying to reply+%{key}+unsubscribe@ address
merge_request: 6597
author:
---
title: Allow creating protected branches when user can merge to such branch
merge_request: 8458
author:
---
title: Adds service trigger events to api
merge_request: 8324
author:
---
title: Remove rogue scrollbars for issue comments with inline elements
merge_request:
author:
---
title: Add hover style to copy icon on commit page header
merge_request:
author: Ryan Harris
---
title: Remove blue border from comment box hover
merge_request:
author:
---
title: Ensure export files are removed after a namespace is deleted
merge_request:
author:
---
title: Use warning icon in mini-graph if stage passed conditionally
merge_request: 8503
author:
---
title: Don't allow project guests to subscribe to merge requests through the API
merge_request:
author: Robert Schilling
---
title: Prevent users from creating notes on resources they can't access
merge_request:
author:
---
title: Prevent users from deleting system deploy keys via the project deploy key API
merge_request:
author:
---
title: allow issue filter bar to be operated with mouse only
merge_request: 8681
author:
---
title: Upgrade omniauth gem to 1.3.2
merge_request:
author:
......@@ -9,7 +9,7 @@ code is effective, understandable, and maintainable.
Any developer can, and is encouraged to, perform code review on merge requests
of colleagues and contributors. However, the final decision to accept a merge
request is up to one of our merge request "endbosses", denoted on the
request is up to one the project's maintainers, denoted on the
[team page](https://about.gitlab.com/team).
## Everyone
......@@ -81,15 +81,15 @@ balance in how deep the reviewer can interfere with the code created by a
reviewee.
- Learning how to find the right balance takes time; that is why we have
minibosses that become merge request endbosses after some time spent on
reviewing merge requests.
reviewers that become maintainers after some time spent on reviewing merge
requests.
- Finding bugs and improving code style is important, but thinking about good
design is important as well. Building abstractions and good design is what
makes it possible to hide complexity and makes future changes easier.
- Asking the reviewee to change the design sometimes means the complete rewrite
of the contributed code. It's usually a good idea to ask another merge
request endboss before doing it, but have the courage to do it when you
believe it is important.
of the contributed code. It's usually a good idea to ask another maintainer or
reviewer before doing it, but have the courage to do it when you believe it is
important.
- There is a difference in doing things right and doing things right now.
Ideally, we should do the former, but in the real world we need the latter as
well. A good example is a security fix which should be released as soon as
......
......@@ -3,7 +3,7 @@
To ensure a merge request does not negatively impact performance of GitLab
_every_ merge request **must** adhere to the guidelines outlined in this
document. There are no exceptions to this rule unless specifically discussed
with and agreed upon by merge request endbosses and performance specialists.
with and agreed upon by backend maintainers and performance specialists.
To measure the impact of a merge request you can use
[Sherlock](profiling.md#sherlock). It's also highly recommended that you read
......@@ -40,9 +40,9 @@ section below for more information.
about the impact.
Sometimes it's hard to assess the impact of a merge request. In this case you
should ask one of the merge request (mini) endbosses to review your changes. You
can find a list of these endbosses at <https://about.gitlab.com/team/>. An
endboss in turn can request a performance specialist to review the changes.
should ask one of the merge request reviewers to review your changes. You can
find a list of these reviewers at <https://about.gitlab.com/team/>. A reviewer
in turn can request a performance specialist to review the changes.
## Query Counts
......
......@@ -105,15 +105,19 @@ module API
present key.deploy_key, with: Entities::SSHKey
end
desc 'Delete existing deploy key of currently authenticated user' do
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.find(params[:key_id])
key.destroy
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
......
......@@ -90,6 +90,12 @@ module API
MergeRequestsFinder.new(current_user, project_id: user_project.id).find(id)
end
def find_merge_request_with_access(id, access_level = :read_merge_request)
merge_request = user_project.merge_requests.find(id)
authorize! access_level, merge_request
merge_request
end
def authenticate!
unauthorized! unless current_user
end
......
......@@ -15,10 +15,8 @@ module API
end
get ":id/merge_requests/:merge_request_id/versions" do
merge_request = user_project.merge_requests.
find(params[:merge_request_id])
merge_request = find_merge_request_with_access(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
end
......@@ -34,10 +32,8 @@ module API
end
get ":id/merge_requests/:merge_request_id/versions/:version_id" do
merge_request = user_project.merge_requests.
find(params[:merge_request_id])
merge_request = find_merge_request_with_access(params[:merge_request_id])
authorize! :read_merge_request, merge_request
present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
end
end
......
......@@ -119,8 +119,8 @@ module API
success Entities::MergeRequest
end
get path do
merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project
end
......@@ -128,8 +128,8 @@ module API
success Entities::RepoCommit
end
get "#{path}/commits" do
merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request.commits, with: Entities::RepoCommit
end
......@@ -137,8 +137,8 @@ module API
success Entities::MergeRequestChanges
end
get "#{path}/changes" do
merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
merge_request = find_merge_request_with_access(params[:merge_request_id])
present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
end
......@@ -156,8 +156,7 @@ module API
:remove_source_branch
end
put path do
merge_request = find_project_merge_request(params.delete(:merge_request_id))
authorize! :update_merge_request, merge_request
merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
......@@ -236,10 +235,7 @@ module API
use :pagination
end
get "#{path}/comments" do
merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :read_merge_request, merge_request
merge_request = find_merge_request_with_access(params[:merge_request_id])
present paginate(merge_request.notes.fresh), with: Entities::MRNote
end
......@@ -251,8 +247,7 @@ module API
requires :note, type: String, desc: 'The text of the comment'
end
post "#{path}/comments" do
merge_request = find_project_merge_request(params[:merge_request_id])
authorize! :create_note, merge_request
merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note)
opts = {
note: params[:note],
......@@ -276,7 +271,7 @@ module API
use :pagination
end
get "#{path}/closes_issues" do
merge_request = find_project_merge_request(params[:merge_request_id])
merge_request = find_merge_request_with_access(params[:merge_request_id])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: issue_entity(user_project), current_user: current_user
end
......
......@@ -70,21 +70,27 @@ module API
end
post ":id/#{noteables_str}/:noteable_id/notes" do
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
noteable_id: params[:noteable_id]
note: params[:body],
noteable_type: noteables_str.classify,
noteable_id: params[:noteable_id]
}
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
noteable = user_project.send(noteables_str.to_sym).find(params[:noteable_id])
if can?(current_user, noteable_read_ability_name(noteable), noteable)
if params[:created_at] && (current_user.is_admin? || user_project.owner == current_user)
opts[:created_at] = params[:created_at]
end
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
note = ::Notes::CreateService.new(user_project, current_user, opts).execute
if note.valid?
present note, with: Entities::const_get(note.class.name)
if note.valid?
present note, with: Entities::const_get(note.class.name)
else
not_found!("Note #{note.errors.messages}")
end
else
not_found!("Note #{note.errors.messages}")
not_found!("Note")
end
end
......
......@@ -145,7 +145,7 @@ module API
name: :room,
type: String,
desc: 'Campfire room'
},
}
],
'custom-issue-tracker' => [
{
......@@ -580,7 +580,38 @@ module API
desc: 'Should unstable builds be treated as passing?'
}
]
}.freeze
}
service_classes = [
AsanaService,
AssemblaService,
BambooService,
BugzillaService,
BuildkiteService,
BuildsEmailService,
CampfireService,
CustomIssueTrackerService,
DroneCiService,
EmailsOnPushService,
ExternalWikiService,
FlowdockService,
GemnasiumService,
HipchatService,
IrkerService,
JiraService,
KubernetesService,
MattermostSlashCommandsService,
SlackSlashCommandsService,
PipelinesEmailService,
PivotaltrackerService,
PushoverService,
RedmineService,
SlackService,
MattermostService,
TeamcityService,
JenkinsService,
JenkinsDeprecatedService
].freeze
trigger_services = {
'mattermost-slash-commands' => [
......@@ -614,6 +645,19 @@ module API
services.each do |service_slug, settings|
desc "Set #{service_slug} service for project"
params do
service_classes.each do |service|
event_names = service.try(:event_names) || []
event_names.each do |event_name|
services[service.to_param.tr("_", "-")] << {
required: false,
name: event_name.to_sym,
type: String,
desc: ServicesHelper.service_event_description(event_name)
}
end
end
services.freeze
settings.each do |setting|
if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc]
......@@ -627,7 +671,7 @@ module API
service_params = declared_params(include_missing: false).merge(active: true)
if service.update_attributes(service_params)
true
present service, with: Entities::ProjectService, include_passwords: current_user.is_admin?
else
render_api_error!('400 Bad Request', 400)
end
......
......@@ -3,8 +3,8 @@ module API
before { authenticate! }
subscribable_types = {
'merge_request' => proc { |id| user_project.merge_requests.find(id) },
'merge_requests' => proc { |id| user_project.merge_requests.find(id) },
'merge_request' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'merge_requests' => proc { |id| find_merge_request_with_access(id, :update_merge_request) },
'issues' => proc { |id| find_project_issue(id) },
'labels' => proc { |id| find_project_label(id) },
}
......
......@@ -5,7 +5,7 @@ module API
before { authenticate! }
ISSUABLE_TYPES = {
'merge_requests' => ->(id) { user_project.merge_requests.find(id) },
'merge_requests' => ->(id) { find_merge_request_with_access(id) },
'issues' => ->(id) { find_project_issue(id) }
}
......
......@@ -4,8 +4,11 @@ module Gitlab
module Build
class Factory < Status::Factory
def self.extended_statuses
[Status::Build::Stop, Status::Build::Play,
Status::Build::Cancelable, Status::Build::Retryable]
[[Status::Build::Cancelable,
Status::Build::Retryable],
[Status::Build::FailedAllowed,
Status::Build::Play,
Status::Build::Stop]]
end
def self.common_helpers
......
module Gitlab
module Ci
module Status
module Pipeline
class SuccessWithWarnings < SimpleDelegator
module Build
class FailedAllowed < SimpleDelegator
include Status::Extended
def text
'passed'
end
def label
'passed with warnings'
'failed (allowed to fail)'
end
def icon
......@@ -18,11 +14,11 @@ module Gitlab
end
def group
'success_with_warnings'
'failed_with_warnings'
end
def self.matches?(pipeline, user)
pipeline.success? && pipeline.has_warnings?
def self.matches?(build, user)
build.failed? && build.allow_failure?
end
end
end
......
......@@ -5,41 +5,46 @@ module Gitlab
def initialize(subject, user)
@subject = subject
@user = user
@status = subject.status || HasStatus::DEFAULT_STATUS
end
def fabricate!
if extended_status
extended_status.new(core_status)
else
if extended_statuses.none?
core_status
else
compound_extended_status
end
end
def self.extended_statuses
[]
def core_status
Gitlab::Ci::Status
.const_get(@status.capitalize)
.new(@subject, @user)
.extend(self.class.common_helpers)
end
def self.common_helpers
Module.new
def compound_extended_status
extended_statuses.inject(core_status) do |status, extended|
extended.new(status)
end
end
private
def extended_statuses
return @extended_statuses if defined?(@extended_statuses)
def simple_status
@simple_status ||= @subject.status || :created
groups = self.class.extended_statuses.map do |group|
Array(group).find { |status| status.matches?(@subject, @user) }
end
@extended_statuses = groups.flatten.compact
end
def core_status
Gitlab::Ci::Status
.const_get(simple_status.capitalize)
.new(@subject, @user)
.extend(self.class.common_helpers)
def self.extended_statuses
[]
end
def extended_status
@extended ||= self.class.extended_statuses.find do |status|
status.matches?(@subject, @user)
end
def self.common_helpers
Module.new
end
end
end
......
......@@ -4,7 +4,7 @@ module Gitlab
module Pipeline
class Factory < Status::Factory
def self.extended_statuses
[Pipeline::SuccessWithWarnings]
[Status::SuccessWarning]
end
def self.common_helpers
......
......@@ -3,6 +3,10 @@ module Gitlab
module Status
module Stage
class Factory < Status::Factory
def self.extended_statuses
[Status::SuccessWarning]
end
def self.common_helpers
Status::Stage::Common
end
......
module Gitlab
module Ci
module Status
##
# Extended status used when pipeline or stage passed conditionally.
# This means that failed jobs that are allowed to fail were present.
#
class SuccessWarning < SimpleDelegator
include Status::Extended
def text
'passed'
end
def label
'passed with warnings'
end
def icon
'icon_status_warning'
end
def group
'success_with_warnings'
end
def self.matches?(subject, user)
subject.success? && subject.has_warnings?
end
end
end
end
end
require 'gitlab/email/handler/create_note_handler'
require 'gitlab/email/handler/create_issue_handler'
require 'gitlab/email/handler/unsubscribe_handler'
module Gitlab
module Email
module Handler
HANDLERS = [CreateNoteHandler, CreateIssueHandler]
HANDLERS = [UnsubscribeHandler, CreateNoteHandler, CreateIssueHandler]
def self.for(mail, mail_key)
HANDLERS.find do |klass|
......
......@@ -9,52 +9,13 @@ module Gitlab
@mail_key = mail_key
end
def message
@message ||= process_message
end
def author
def can_execute?
raise NotImplementedError
end
def project
def execute
raise NotImplementedError
end
private
def validate_permission!(permission)
raise UserNotFoundError unless author
raise UserBlockedError if author.blocked?
raise ProjectNotFound unless author.can?(:read_project, project)
raise UserNotAuthorizedError unless author.can?(permission, project)
end
def process_message
message = ReplyParser.new(mail).execute.strip
add_attachments(message)
end
def add_attachments(reply)
attachments = Email::AttachmentUploader.new(mail).execute(project)
reply + attachments.map do |link|
"\n\n#{link[:markdown]}"
end.join
end
def verify_record!(record:, invalid_exception:, record_name:)
return if record.persisted?
return if record.errors.key?(:commands_only)
error_title = "The #{record_name} could not be created for the following reasons:"
msg = error_title + record.errors.full_messages.map do |error|
"\n\n- #{error}"
end.join
raise invalid_exception, msg
end
end
end
end
......
......@@ -5,6 +5,7 @@ module Gitlab
module Email
module Handler
class CreateIssueHandler < BaseHandler
include ReplyProcessing
attr_reader :project_path, :incoming_email_token
def initialize(mail, mail_key)
......
require 'gitlab/email/handler/base_handler'
require 'gitlab/email/handler/reply_processing'
module Gitlab
module Email
module Handler
class CreateNoteHandler < BaseHandler
include ReplyProcessing
def can_handle?
mail_key =~ /\A\w+\z/
end
......@@ -24,6 +27,8 @@ module Gitlab
record_name: 'comment')
end
private
def author
sent_notification.recipient
end
......@@ -36,8 +41,6 @@ module Gitlab
@sent_notification ||= SentNotification.for(mail_key)
end
private
def create_note
Notes::CreateService.new(
project,
......
module Gitlab
module Email
module Handler
module ReplyProcessing
private
def author
raise NotImplementedError
end
def project
raise NotImplementedError
end
def message
@message ||= process_message
end
def process_message
message = ReplyParser.new(mail).execute.strip
add_attachments(message)
end
def add_attachments(reply)
attachments = Email::AttachmentUploader.new(mail).execute(project)
reply + attachments.map do |link|
"\n\n#{link[:markdown]}"
end.join
end
def validate_permission!(permission)
raise UserNotFoundError unless author
raise UserBlockedError if author.blocked?
raise ProjectNotFound unless author.can?(:read_project, project)
raise UserNotAuthorizedError unless author.can?(permission, project)
end
def verify_record!(record:, invalid_exception:, record_name:)
return if record.persisted?
return if record.errors.key?(:commands_only)
error_title = "The #{record_name} could not be created for the following reasons:"
msg = error_title + record.errors.full_messages.map do |error|
"\n\n- #{error}"
end.join
raise invalid_exception, msg
end
end
end
end
end
require 'gitlab/email/handler/base_handler'
module Gitlab
module Email
module Handler
class UnsubscribeHandler < BaseHandler
def can_handle?
mail_key =~ /\A\w+#{Regexp.escape(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX)}\z/
end
def execute
raise SentNotificationNotFoundError unless sent_notification
return unless sent_notification.unsubscribable?
noteable = sent_notification.noteable
raise NoteableNotFoundError unless noteable
noteable.unsubscribe(sent_notification.recipient)
end
private
def sent_notification
@sent_notification ||= SentNotification.for(reply_key)
end
def reply_key
mail_key.sub(Gitlab::IncomingEmail::UNSUBSCRIBE_SUFFIX, '')
end
end
end
end
end
module Gitlab
module IncomingEmail
UNSUBSCRIBE_SUFFIX = '+unsubscribe'.freeze
WILDCARD_PLACEHOLDER = '%{key}'.freeze
class << self
......@@ -18,7 +19,11 @@ module Gitlab
end
def reply_address(key)
config.address.gsub(WILDCARD_PLACEHOLDER, key)
config.address.sub(WILDCARD_PLACEHOLDER, key)
end
def unsubscribe_address(key)
config.address.sub(WILDCARD_PLACEHOLDER, "#{key}#{UNSUBSCRIBE_SUFFIX}")
end
def key_from_address(address)
......@@ -49,7 +54,7 @@ module Gitlab
return nil unless wildcard_address
regex = Regexp.escape(wildcard_address)
regex = regex.gsub(Regexp.escape('%{key}'), "(.+)")
regex = regex.sub(Regexp.escape(WILDCARD_PLACEHOLDER), '(.+)')
Regexp.new(regex).freeze
end
end
......
......@@ -35,7 +35,9 @@ module Gitlab
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
access_levels.any? { |access_level| access_level.check_access(user) }
has_access = access_levels.any? { |access_level| access_level.check_access(user) }
has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
else
user.can?(:push_code, project)
end
......
......@@ -54,6 +54,7 @@ describe Projects::ServicesController do
context 'on successful update' do
it 'sets the flash' do
expect(service).to receive(:to_param).and_return('hipchat')
expect(service).to receive(:event_names).and_return(HipchatService.event_names)
put :update,
namespace_id: project.namespace.id,
......
......@@ -3,11 +3,12 @@ FactoryGirl.define do
transient do
name 'test'
status nil
warnings nil
pipeline factory: :ci_empty_pipeline
end
initialize_with do
Ci::Stage.new(pipeline, name: name, status: status)
Ci::Stage.new(pipeline, name: name, status: status, warnings: warnings)
end
end
end
......@@ -10,7 +10,7 @@ describe "Admin::Projects", feature: true do
end
describe "GET /admin/projects" do
let!(:archived_project) { create :project, :public, archived: true }
let!(:archived_project) { create :project, :public, :archived }
before do
visit admin_projects_path
......
......@@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do
include_examples 'project features apply to issuables', MergeRequest
context 'archived issuable' do
let(:project_archived) { create(:project, group: group, merge_requests_access_level: ProjectFeature::ENABLED, archived: true) }
let(:project_archived) { create(:project, :archived, group: group, merge_requests_access_level: ProjectFeature::ENABLED) }
let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') }
let(:access_level) { ProjectFeature::ENABLED }
let(:user) { user_in_group }
......
......@@ -134,14 +134,14 @@ describe 'Dropdown assignee', js: true, feature: true do
click_button 'Assigned to me'
end
expect(filtered_search.value).to eq("assignee:#{user.to_reference}")
expect(filtered_search.value).to eq("assignee:#{user.to_reference} ")
end
it 'fills in the assignee username when the assignee has not been filtered' do
click_assignee(user_jacob.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:@#{user_jacob.username}")
expect(filtered_search.value).to eq("assignee:@#{user_jacob.username} ")
end
it 'fills in the assignee username when the assignee has been filtered' do
......@@ -149,14 +149,14 @@ describe 'Dropdown assignee', js: true, feature: true do
click_assignee(user.name)
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:@#{user.username}")
expect(filtered_search.value).to eq("assignee:@#{user.username} ")
end
it 'selects `no assignee`' do
find('#js-dropdown-assignee .filter-dropdown-item', text: 'No Assignee').click
expect(page).to have_css(js_dropdown_assignee, visible: false)
expect(filtered_search.value).to eq("assignee:none")
expect(filtered_search.value).to eq("assignee:none ")
end
end
......
......@@ -121,14 +121,14 @@ describe 'Dropdown author', js: true, feature: true do
click_author(user_jacob.name)
expect(page).to have_css(js_dropdown_author, visible: false)
expect(filtered_search.value).to eq("author:@#{user_jacob.username}")
expect(filtered_search.value).to eq("author:@#{user_jacob.username} ")
end
it 'fills in the author username when the author has been filtered' do
click_author(user.name)
expect(page).to have_css(js_dropdown_author, visible: false)
expect(filtered_search.value).to eq("author:@#{user.username}")
expect(filtered_search.value).to eq("author:@#{user.username} ")
end
end
......
......@@ -159,7 +159,7 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{bug_label.title}")
expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
it 'fills in the label name when the label is partially filled' do
......@@ -167,49 +167,49 @@ describe 'Dropdown label', js: true, feature: true do
click_label(bug_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{bug_label.title}")
expect(filtered_search.value).to eq("label:~#{bug_label.title} ")
end
it 'fills in the label name that contains multiple words' do
click_label(two_words_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\"")
expect(filtered_search.value).to eq("label:~\"#{two_words_label.title}\" ")
end
it 'fills in the label name that contains multiple words and is very long' do
click_label(long_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~\"#{long_label.title}\"")
expect(filtered_search.value).to eq("label:~\"#{long_label.title}\" ")
end
it 'fills in the label name that contains double quotes' do
click_label(wont_fix_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}'")
expect(filtered_search.value).to eq("label:~'#{wont_fix_label.title}' ")
end
it 'fills in the label name with the correct capitalization' do
click_label(uppercase_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{uppercase_label.title}")
expect(filtered_search.value).to eq("label:~#{uppercase_label.title} ")
end
it 'fills in the label name with special characters' do
click_label(special_label.title)
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:~#{special_label.title}")
expect(filtered_search.value).to eq("label:~#{special_label.title} ")
end
it 'selects `no label`' do
find('#js-dropdown-label .filter-dropdown-item', text: 'No Label').click
expect(page).to have_css(js_dropdown_label, visible: false)
expect(filtered_search.value).to eq("label:none")
expect(filtered_search.value).to eq("label:none ")
end
end
......
......@@ -127,7 +127,7 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{milestone.title}")
expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
end
it 'fills in the milestone name when the milestone is partially filled' do
......@@ -135,56 +135,56 @@ describe 'Dropdown milestone', js: true, feature: true do
click_milestone(milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{milestone.title}")
expect(filtered_search.value).to eq("milestone:%#{milestone.title} ")
end
it 'fills in the milestone name that contains multiple words' do
click_milestone(two_words_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\"")
expect(filtered_search.value).to eq("milestone:%\"#{two_words_milestone.title}\" ")
end
it 'fills in the milestone name that contains multiple words and is very long' do
click_milestone(long_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\"")
expect(filtered_search.value).to eq("milestone:%\"#{long_milestone.title}\" ")
end
it 'fills in the milestone name that contains double quotes' do
click_milestone(wont_fix_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}'")
expect(filtered_search.value).to eq("milestone:%'#{wont_fix_milestone.title}' ")
end
it 'fills in the milestone name with the correct capitalization' do
click_milestone(uppercase_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title}")
expect(filtered_search.value).to eq("milestone:%#{uppercase_milestone.title} ")
end
it 'fills in the milestone name with special characters' do
click_milestone(special_milestone.title)
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:%#{special_milestone.title}")
expect(filtered_search.value).to eq("milestone:%#{special_milestone.title} ")
end
it 'selects `no milestone`' do
click_static_milestone('No Milestone')
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:none")
expect(filtered_search.value).to eq("milestone:none ")
end
it 'selects `upcoming milestone`' do
click_static_milestone('Upcoming')
expect(page).to have_css(js_dropdown_milestone, visible: false)
expect(filtered_search.value).to eq("milestone:upcoming")
expect(filtered_search.value).to eq("milestone:upcoming ")
end
end
......
......@@ -62,28 +62,28 @@ describe 'Dropdown weight', js: true, feature: true do
click_weight(1)
expect(page).to have_css(js_dropdown_weight, visible: false)
expect(filtered_search.value).to eq("weight:1")
expect(filtered_search.value).to eq("weight:1 ")
end
it 'fills in weight 2' do
click_weight(2)
expect(page).to have_css(js_dropdown_weight, visible: false)
expect(filtered_search.value).to eq("weight:2")
expect(filtered_search.value).to eq("weight:2 ")
end
it 'fills in weight 3' do
click_weight(3)
expect(page).to have_css(js_dropdown_weight, visible: false)
expect(filtered_search.value).to eq("weight:3")
expect(filtered_search.value).to eq("weight:3 ")
end
it 'fills in `no weight`' do
click_static_weight('No Weight')
expect(page).to have_css(js_dropdown_weight, visible: false)
expect(filtered_search.value).to eq("weight:none")
expect(filtered_search.value).to eq("weight:none ")
end
end
......
......@@ -539,7 +539,7 @@ describe 'Filter issues', js: true, feature: true do
click_button user2.username
end
expect(filtered_search.value).to eq("author:@#{user2.username}")
expect(filtered_search.value).to eq("author:@#{user2.username} ")
end
it 'changes label' do
......@@ -551,7 +551,7 @@ describe 'Filter issues', js: true, feature: true do
click_button label.name
end
expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name}")
expect(filtered_search.value).to eq("author:@#{user.username} label:~#{label.name} ")
end
it 'changes label correctly space is in previous label' do
......@@ -563,7 +563,7 @@ describe 'Filter issues', js: true, feature: true do
click_button label.name
end
expect(filtered_search.value).to eq("label:~#{label.name}")
expect(filtered_search.value).to eq("label:~#{label.name} ")
end
end
......
require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', feature: true, js: true do
let(:export_path) { "#{Dir::tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
let(:project) { create(:empty_project) }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
end
after do
FileUtils.rm_rf(export_path, secure: true)
end
context 'admin user' do
before do
login_as(:admin)
end
context 'moving the namespace' do
scenario 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
expect(File).to exist(old_export_path)
project.namespace.update(path: 'new_path')
expect(File).not_to exist(old_export_path)
end
end
context 'deleting the namespace' do
scenario 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
expect(File).to exist(old_export_path)
project.namespace.destroy
expect(File).not_to exist(old_export_path)
end
end
def setup_export_project
visit edit_namespace_project_path(project.namespace, project)
expect(page).to have_content('Export project')
click_link 'Export project'
visit edit_namespace_project_path(project.namespace, project)
expect(page).to have_content('Download export')
end
end
end
......@@ -6,7 +6,7 @@ describe MergeRequestsFinder do
let(:project1) { create(:project) }
let(:project2) { create(:project, forked_from_project: project1) }
let(:project3) { create(:project, forked_from_project: project1, archived: true) }
let(:project3) { create(:project, :archived, forked_from_project: project1) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
......
......@@ -36,7 +36,7 @@ describe MoveToProjectFinder do
it 'does not return archived projects' do
reporter_project.team << [user, :reporter]
reporter_project.update_attributes(archived: true)
reporter_project.archive!
other_reporter_project = create(:empty_project)
other_reporter_project.team << [user, :reporter]
......
......@@ -31,7 +31,7 @@
it('should add tokenName and tokenValue', () => {
gl.FilteredSearchDropdownManager.addWordToInput('label', 'none');
expect(getInputValue()).toBe('label:none');
expect(getInputValue()).toBe('label:none ');
});
});
......@@ -45,13 +45,13 @@
it('should replace tokenValue', () => {
setInputValue('author:roo');
gl.FilteredSearchDropdownManager.addWordToInput('author', '@root');
expect(getInputValue()).toBe('author:@root');
expect(getInputValue()).toBe('author:@root ');
});
it('should add tokenValues containing spaces', () => {
setInputValue('label:~"test');
gl.FilteredSearchDropdownManager.addWordToInput('label', '~\'"test me"\'');
expect(getInputValue()).toBe('label:~\'"test me"\'');
expect(getInputValue()).toBe('label:~\'"test me"\' ');
});
});
});
......
......@@ -3,15 +3,23 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Build::Factory do
let(:user) { create(:user) }
let(:project) { build.project }
subject { described_class.new(build, user) }
let(:status) { subject.fabricate! }
let(:status) { factory.fabricate! }
let(:factory) { described_class.new(build, user) }
before { project.team << [user, :developer] }
context 'when build is successful' do
let(:build) { create(:ci_build, :success) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable]
end
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
......@@ -26,24 +34,72 @@ describe Gitlab::Ci::Status::Build::Factory do
end
context 'when build is failed' do
let(:build) { create(:ci_build, :failed) }
context 'when build is not allowed to fail' do
let(:build) { create(:ci_build, :failed) }
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable]
end
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_failed'
expect(status.label).to eq 'failed'
expect(status).to have_details
expect(status).to have_action
end
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_failed'
expect(status.label).to eq 'failed'
expect(status).to have_details
expect(status).to have_action
context 'when build is allowed to fail' do
let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable,
Gitlab::Ci::Status::Build::FailedAllowed]
end
it 'fabricates a failed but allowed build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::FailedAllowed
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'failed'
expect(status.icon).to eq 'icon_status_warning'
expect(status.label).to eq 'failed (allowed to fail)'
expect(status).to have_details
expect(status).to have_action
expect(status.action_title).to include 'Retry'
expect(status.action_path).to include 'retry'
end
end
end
context 'when build is a canceled' do
let(:build) { create(:ci_build, :canceled) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Canceled
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Retryable]
end
it 'fabricates a retryable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Retryable
end
......@@ -60,6 +116,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is running' do
let(:build) { create(:ci_build, :running) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Running
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Cancelable]
end
it 'fabricates a canceable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
end
......@@ -76,6 +141,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is pending' do
let(:build) { create(:ci_build, :pending) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Pending
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Cancelable]
end
it 'fabricates a cancelable build status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Cancelable
end
......@@ -92,6 +166,14 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is skipped' do
let(:build) { create(:ci_build, :skipped) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
end
it 'does not match extended statuses' do
expect(factory.extended_statuses).to be_empty
end
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Skipped
end
......@@ -109,6 +191,15 @@ describe Gitlab::Ci::Status::Build::Factory do
context 'when build is a play action' do
let(:build) { create(:ci_build, :playable) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Play]
end
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Play
end
......@@ -119,12 +210,22 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.label).to eq 'manual play action'
expect(status).to have_details
expect(status).to have_action
expect(status.action_path).to include 'play'
end
end
context 'when build is an environment stop action' do
let(:build) { create(:ci_build, :playable, :teardown_environment) }
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::Build::Stop]
end
it 'fabricates a core skipped status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Stop
end
......
require 'spec_helper'
describe Gitlab::Ci::Status::Build::FailedAllowed do
let(:status) { double('core status') }
let(:user) { double('user') }
subject do
described_class.new(status)
end
describe '#text' do
it 'does not override status text' do
expect(status).to receive(:text)
subject.text
end
end
describe '#icon' do
it 'returns a warning icon' do
expect(subject.icon).to eq 'icon_status_warning'
end
end
describe '#label' do
it 'returns information about failed but allowed to fail status' do
expect(subject.label).to eq 'failed (allowed to fail)'
end
end
describe '#group' do
it 'returns status failed with warnings status group' do
expect(subject.group).to eq 'failed_with_warnings'
end
end
describe 'action details' do
describe '#has_action?' do
it 'does not decorate action details' do
expect(status).to receive(:has_action?)
subject.has_action?
end
end
describe '#action_path' do
it 'does not decorate action path' do
expect(status).to receive(:action_path)
subject.action_path
end
end
describe '#action_icon' do
it 'does not decorate action icon' do
expect(status).to receive(:action_icon)
subject.action_icon
end
end
describe '#action_title' do
it 'does not decorate action title' do
expect(status).to receive(:action_title)
subject.action_title
end
end
end
describe '.matches?' do
subject { described_class.matches?(build, user) }
context 'when build is failed' do
context 'when build is allowed to fail' do
let(:build) { create(:ci_build, :failed, :allowed_to_fail) }
it 'is a correct match' do
expect(subject).to be true
end
end
context 'when build is not allowed to fail' do
let(:build) { create(:ci_build, :failed) }
it 'is not a correct match' do
expect(subject).not_to be true
end
end
end
context 'when build did not fail' do
context 'when build is allowed to fail' do
let(:build) { create(:ci_build, :success, :allowed_to_fail) }
it 'is not a correct match' do
expect(subject).not_to be true
end
end
context 'when build is not allowed to fail' do
let(:build) { create(:ci_build, :success) }
it 'is not a correct match' do
expect(subject).not_to be true
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Status::Factory do
subject do
described_class.new(resource, user)
let(:user) { create(:user) }
let(:fabricated_status) { factory.fabricate! }
let(:factory) { described_class.new(resource, user) }
context 'when object has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |simple_status|
context "when simple core status is #{simple_status}" do
let(:resource) { double('resource', status: simple_status) }
let(:expected_status) do
Gitlab::Ci::Status.const_get(simple_status.capitalize)
end
it "fabricates a core status #{simple_status}" do
expect(fabricated_status).to be_a expected_status
end
it "matches a valid core status for #{simple_status}" do
expect(factory.core_status).to be_a expected_status
end
it "does not match any extended statuses for #{simple_status}" do
expect(factory.extended_statuses).to be_empty
end
end
end
end
let(:user) { create(:user) }
context 'when resource supports multiple extended statuses' do
let(:resource) { double('resource', status: :success) }
let(:status) { subject.fabricate! }
let(:first_extended_status) do
Class.new(SimpleDelegator) do
def first_method
'first return value'
end
context 'when object has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status|
context "when core status is #{core_status}" do
let(:resource) { double(status: core_status) }
def second_method
'second return value'
end
def self.matches?(*)
true
end
end
end
it "fabricates a core status #{core_status}" do
expect(status).to be_a(
Gitlab::Ci::Status.const_get(core_status.capitalize))
let(:second_extended_status) do
Class.new(SimpleDelegator) do
def first_method
'decorated return value'
end
def third_method
'third return value'
end
def self.matches?(*)
true
end
end
end
shared_examples 'compound decorator factory' do
it 'fabricates compound decorator' do
expect(fabricated_status.first_method).to eq 'decorated return value'
expect(fabricated_status.second_method).to eq 'second return value'
expect(fabricated_status.third_method).to eq 'third return value'
end
it 'delegates to core status' do
expect(fabricated_status.text).to eq 'passed'
end
it 'latest matches status becomes a status name' do
expect(fabricated_status.class).to eq second_extended_status
end
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [first_extended_status, second_extended_status]
end
end
context 'when exclusive statuses are matches' do
before do
allow(described_class).to receive(:extended_statuses)
.and_return([[first_extended_status, second_extended_status]])
end
it 'does not fabricate compound decorator' do
expect(fabricated_status.first_method).to eq 'first return value'
expect(fabricated_status.second_method).to eq 'second return value'
expect(fabricated_status).not_to respond_to(:third_method)
end
it 'delegates to core status' do
expect(fabricated_status.text).to eq 'passed'
end
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses).to eq [first_extended_status]
end
end
context 'when exclusive statuses are not matched' do
before do
allow(described_class).to receive(:extended_statuses)
.and_return([[first_extended_status], [second_extended_status]])
end
it_behaves_like 'compound decorator factory'
end
context 'when using simplified status grouping' do
before do
allow(described_class).to receive(:extended_statuses)
.and_return([first_extended_status, second_extended_status])
end
it_behaves_like 'compound decorator factory'
end
end
end
......@@ -3,29 +3,32 @@ require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::Factory do
let(:user) { create(:user) }
let(:project) { pipeline.project }
subject do
described_class.new(pipeline, user)
end
let(:status) do
subject.fabricate!
end
let(:status) { factory.fabricate! }
let(:factory) { described_class.new(pipeline, user) }
before do
project.team << [user, :developer]
end
context 'when pipeline has a core status' do
HasStatus::AVAILABLE_STATUSES.each do |core_status|
context "when core status is #{core_status}" do
let(:pipeline) do
create(:ci_pipeline, status: core_status)
HasStatus::AVAILABLE_STATUSES.each do |simple_status|
context "when core status is #{simple_status}" do
let(:pipeline) { create(:ci_pipeline, status: simple_status) }
let(:expected_status) do
Gitlab::Ci::Status.const_get(simple_status.capitalize)
end
it "matches correct core status for #{simple_status}" do
expect(factory.core_status).to be_a expected_status
end
it "fabricates a core status #{core_status}" do
expect(status).to be_a(
Gitlab::Ci::Status.const_get(core_status.capitalize))
it 'does not matche extended statuses' do
expect(factory.extended_statuses).to be_empty
end
it "fabricates a core status #{simple_status}" do
expect(status).to be_a expected_status
end
it 'extends core status with common pipeline methods' do
......@@ -47,13 +50,22 @@ describe Gitlab::Ci::Status::Pipeline::Factory do
create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline)
end
it 'matches correct core status' do
expect(factory.core_status).to be_a Gitlab::Ci::Status::Success
end
it 'matches correct extended statuses' do
expect(factory.extended_statuses)
.to eq [Gitlab::Ci::Status::SuccessWarning]
end
it 'fabricates extended "success with warnings" status' do
expect(status)
.to be_a Gitlab::Ci::Status::Pipeline::SuccessWithWarnings
expect(status).to be_a Gitlab::Ci::Status::SuccessWarning
end
it 'extends core status with common pipeline methods' do
it 'extends core status with common pipeline method' do
expect(status).to have_details
expect(status.details_path).to include "pipelines/#{pipeline.id}"
end
end
end
......@@ -43,4 +43,25 @@ describe Gitlab::Ci::Status::Stage::Factory do
end
end
end
context 'when stage has warnings' do
let(:stage) do
build(:ci_stage, name: 'test', status: :success, pipeline: pipeline)
end
before do
create(:ci_build, :allowed_to_fail, :failed,
stage: 'test', pipeline: stage.pipeline)
end
it 'fabricates extended "success with warnings" status' do
expect(status)
.to be_a Gitlab::Ci::Status::SuccessWarning
end
it 'extends core status with common stage method' do
expect(status).to have_details
expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}"
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
describe Gitlab::Ci::Status::SuccessWarning do
subject do
described_class.new(double('status'))
end
......@@ -22,46 +22,52 @@ describe Gitlab::Ci::Status::Pipeline::SuccessWithWarnings do
end
describe '.matches?' do
context 'when pipeline is successful' do
let(:pipeline) do
create(:ci_pipeline, status: :success)
let(:matchable) { double('matchable') }
context 'when matchable subject is successful' do
before do
allow(matchable).to receive(:success?).and_return(true)
end
context 'when pipeline has warnings' do
context 'when matchable subject has warnings' do
before do
allow(pipeline).to receive(:has_warnings?).and_return(true)
allow(matchable).to receive(:has_warnings?).and_return(true)
end
it 'is a correct match' do
expect(described_class.matches?(pipeline, double)).to eq true
expect(described_class.matches?(matchable, double)).to eq true
end
end
context 'when pipeline does not have warnings' do
context 'when matchable subject does not have warnings' do
before do
allow(matchable).to receive(:has_warnings?).and_return(false)
end
it 'does not match' do
expect(described_class.matches?(pipeline, double)).to eq false
expect(described_class.matches?(matchable, double)).to eq false
end
end
end
context 'when pipeline is not successful' do
let(:pipeline) do
create(:ci_pipeline, status: :skipped)
context 'when matchable subject is not successful' do
before do
allow(matchable).to receive(:success?).and_return(false)
end
context 'when pipeline has warnings' do
context 'when matchable subject has warnings' do
before do
allow(pipeline).to receive(:has_warnings?).and_return(true)
allow(matchable).to receive(:has_warnings?).and_return(true)
end
it 'does not match' do
expect(described_class.matches?(pipeline, double)).to eq false
expect(described_class.matches?(matchable, double)).to eq false
end
end
context 'when pipeline does not have warnings' do
context 'when matchable subject does not have warnings' do
it 'does not match' do
expect(described_class.matches?(pipeline, double)).to eq false
expect(described_class.matches?(matchable, double)).to eq false
end
end
end
......
......@@ -18,7 +18,7 @@ shared_context :email_shared_context do
end
end
shared_examples :email_shared_examples do
shared_examples :reply_processing_shared_examples do
context "when the user could not be found" do
before do
user.destroy
......
......@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
include_context :email_shared_context
it_behaves_like :email_shared_examples
it_behaves_like :reply_processing_shared_examples
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
......
......@@ -3,7 +3,7 @@ require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do
include_context :email_shared_context
it_behaves_like :email_shared_examples
it_behaves_like :reply_processing_shared_examples
before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
......
require 'spec_helper'
require_relative '../email_shared_blocks'
describe Gitlab::Email::Handler::UnsubscribeHandler, lib: true do
include_context :email_shared_context
before do
stub_incoming_email_setting(enabled: true, address: 'reply+%{key}@appmail.adventuretime.ooo')
stub_config_setting(host: 'localhost')
end
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "#{mail_key}+unsubscribe") }
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:noteable) { create(:issue, project: project) }
let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) }
context 'when notification concerns a commit' do
let(:commit) { create(:commit, project: project) }
let!(:sent_notification) { SentNotification.record(commit, user.id, mail_key) }
it 'handler does not raise an error' do
expect { receiver.execute }.not_to raise_error
end
end
context 'user is unsubscribed' do
it 'leaves user unsubscribed' do
expect { receiver.execute }.not_to change { noteable.subscribed?(user) }.from(false)
end
end
context 'user is subscribed' do
before do
noteable.subscribe(user)
end
it 'unsubscribes user from notable' do
expect { receiver.execute }.to change { noteable.subscribed?(user) }.from(true).to(false)
end
end
context 'when the noteable could not be found' do
before do
noteable.destroy
end
it 'raises a NoteableNotFoundError' do
expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError)
end
end
context 'when no sent notification for the mail key could be found' do
let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') }
it 'raises a SentNotificationNotFoundError' do
expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError)
end
end
end
......@@ -23,6 +23,48 @@ describe Gitlab::IncomingEmail, lib: true do
end
end
describe 'self.supports_wildcard?' do
context 'address contains the wildard placeholder' do
before do
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
end
it 'confirms that wildcard is supported' do
expect(described_class.supports_wildcard?).to be_truthy
end
end
context "address doesn't contain the wildcard placeholder" do
before do
stub_incoming_email_setting(address: 'replies@example.com')
end
it 'returns that wildcard is not supported' do
expect(described_class.supports_wildcard?).to be_falsey
end
end
context 'address is not set' do
before do
stub_incoming_email_setting(address: nil)
end
it 'returns that wildard is not supported' do
expect(described_class.supports_wildcard?).to be_falsey
end
end
end
context 'self.unsubscribe_address' do
before do
stub_incoming_email_setting(address: 'replies+%{key}@example.com')
end
it 'returns the address with interpolated reply key and unsubscribe suffix' do
expect(described_class.unsubscribe_address('key')).to eq('replies+key+unsubscribe@example.com')
end
end
context "self.reply_address" do
before do
stub_incoming_email_setting(address: "replies+%{key}@example.com")
......
......@@ -66,7 +66,8 @@ describe Gitlab::UserAccess, lib: true do
end
describe 'push to protected branch' do
let(:branch) { create :protected_branch, project: project }
let(:branch) { create :protected_branch, project: project, name: "test" }
let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project }
it 'returns true if user is a master' do
project.team << [user, :master]
......@@ -85,6 +86,12 @@ describe Gitlab::UserAccess, lib: true do
expect(access.can_push_to_branch?(branch.name)).to be_falsey
end
it 'returns true if branch does not exist and user has permission to merge' do
project.team << [user, :developer]
expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy
end
end
describe 'push to protected branch if allowed for developers' do
......
......@@ -122,55 +122,80 @@ describe Ci::Pipeline, models: true do
end
end
describe '#stages' do
describe 'pipeline stages' do
before do
create(:commit_status, pipeline: pipeline, stage: 'build', name: 'linux', stage_idx: 0, status: 'success')
create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'failed')
create(:commit_status, pipeline: pipeline, stage: 'deploy', name: 'staging', stage_idx: 2, status: 'running')
create(:commit_status, pipeline: pipeline, stage: 'test', name: 'rspec', stage_idx: 1, status: 'success')
end
subject { pipeline.stages }
context 'stages list' do
it 'returns ordered list of stages' do
expect(subject.map(&:name)).to eq(%w[build test deploy])
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'linux',
stage_idx: 0,
status: 'success')
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'failed')
create(:commit_status, pipeline: pipeline,
stage: 'deploy',
name: 'staging',
stage_idx: 2,
status: 'running')
create(:commit_status, pipeline: pipeline,
stage: 'test',
name: 'rspec',
stage_idx: 1,
status: 'success')
end
describe '#stages' do
subject { pipeline.stages }
context 'stages list' do
it 'returns ordered list of stages' do
expect(subject.map(&:name)).to eq(%w[build test deploy])
end
end
end
it 'returns a valid number of stages' do
expect(pipeline.stages_count).to eq(3)
end
context 'stages with statuses' do
let(:statuses) do
subject.map { |stage| [stage.name, stage.status] }
end
it 'returns a valid names of stages' do
expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
end
it 'returns list of stages with correct statuses' do
expect(statuses).to eq([['build', 'failed'],
['test', 'success'],
['deploy', 'running']])
end
context 'stages with statuses' do
let(:statuses) do
subject.map do |stage|
[stage.name, stage.status]
context 'when commit status is retried' do
before do
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'success')
end
it 'ignores the previous state' do
expect(statuses).to eq([['build', 'success'],
['test', 'success'],
['deploy', 'running']])
end
end
end
end
it 'returns list of stages with statuses' do
expect(statuses).to eq([['build', 'failed'],
['test', 'success'],
['deploy', 'running']
])
describe '#stages_count' do
it 'returns a valid number of stages' do
expect(pipeline.stages_count).to eq(3)
end
end
context 'when build is retried' do
before do
create(:commit_status, pipeline: pipeline, stage: 'build', name: 'mac', stage_idx: 0, status: 'success')
end
it 'ignores the previous state' do
expect(statuses).to eq([['build', 'success'],
['test', 'success'],
['deploy', 'running']
])
end
describe '#stages_name' do
it 'returns a valid names of stages' do
expect(pipeline.stages_name).to eq(['build', 'test', 'deploy'])
end
end
end
......
......@@ -142,6 +142,78 @@ describe Ci::Stage, models: true do
end
end
describe '#success?' do
context 'when stage is successful' do
before do
create_job(:ci_build, status: :success)
create_job(:generic_commit_status, status: :success)
end
it 'is successful' do
expect(stage).to be_success
end
end
context 'when stage is not successful' do
before do
create_job(:ci_build, status: :failed)
create_job(:generic_commit_status, status: :success)
end
it 'is not successful' do
expect(stage).not_to be_success
end
end
end
describe '#has_warnings?' do
context 'when stage has warnings' do
context 'when using memoized warnings flag' do
context 'when there are warnings' do
let(:stage) { build(:ci_stage, warnings: true) }
it 'has memoized warnings' do
expect(stage).not_to receive(:statuses)
expect(stage).to have_warnings
end
end
context 'when there are no warnings' do
let(:stage) { build(:ci_stage, warnings: false) }
it 'has memoized warnings' do
expect(stage).not_to receive(:statuses)
expect(stage).not_to have_warnings
end
end
end
context 'when calculating warnings from statuses' do
before do
create(:ci_build, :failed, :allowed_to_fail,
stage: stage_name, pipeline: pipeline)
end
it 'has warnings calculated from statuses' do
expect(stage).to receive(:statuses).and_call_original
expect(stage).to have_warnings
end
end
end
context 'when stage does not have warnings' do
before do
create(:ci_build, :success, stage: stage_name,
pipeline: pipeline)
end
it 'does not have warnings calculated from statuses' do
expect(stage).to receive(:statuses).and_call_original
expect(stage).not_to have_warnings
end
end
end
def create_job(type, status: 'success', stage: stage_name)
create(type, pipeline: pipeline, stage: stage, status: status)
end
......
......@@ -219,4 +219,10 @@ describe HasStatus do
end
end
end
describe '::DEFAULT_STATUS' do
it 'is a status created' do
expect(described_class::DEFAULT_STATUS).to eq 'created'
end
end
end
......@@ -117,6 +117,7 @@ describe Namespace, models: true do
new_path = @namespace.path + "_new"
allow(@namespace).to receive(:path_was).and_return(@namespace.path)
allow(@namespace).to receive(:path).and_return(new_path)
expect(@namespace).to receive(:remove_exports!)
expect(@namespace.move_dir).to be_truthy
end
......@@ -151,11 +152,17 @@ describe Namespace, models: true do
let!(:project) { create(:project, namespace: namespace) }
let!(:path) { File.join(Gitlab.config.repositories.storages.default, namespace.path) }
before { namespace.destroy }
it "removes its dirs when deleted" do
namespace.destroy
expect(File.exist?(path)).to be(false)
end
it 'removes the exports folder' do
expect(namespace).to receive(:remove_exports!)
namespace.destroy
end
end
describe '.find_by_path_or_name' do
......
......@@ -687,6 +687,17 @@ describe API::MergeRequests, api: true do
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.first['id']).to eq(issue.id)
end
it 'returns 403 if the user has no access to the merge request' do
project = create(:empty_project, :private)
merge_request = create(:merge_request, :simple, source_project: project)
guest = create(:user)
project.team << [guest, :guest]
get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest)
expect(response).to have_http_status(403)
end
end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
......@@ -708,6 +719,15 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
it 'returns 403 if user has no access to read code' do
guest = create(:user)
project.team << [guest, :guest]
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
expect(response).to have_http_status(403)
end
end
describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
......@@ -729,6 +749,15 @@ describe API::MergeRequests, api: true do
expect(response).to have_http_status(404)
end
it 'returns 403 if user has no access to read code' do
guest = create(:user)
project.team << [guest, :guest]
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest)
expect(response).to have_http_status(403)
end
end
describe 'GET :id/merge_requests/:merge_request_id/approvals' do
......
......@@ -264,6 +264,18 @@ describe API::Notes, api: true do
end
end
context 'when user does not have access to read the noteable' do
it 'responds with 404' do
project = create(:empty_project, :private) { |p| p.add_guest(user) }
issue = create(:issue, :confidential, project: project)
post api("/projects/#{project.id}/issues/#{issue.id}/notes", user),
body: 'Foo'
expect(response).to have_http_status(404)
end
end
context 'when user does not have access to create noteable' do
let(:private_issue) { create(:issue, project: create(:empty_project, :private)) }
......
......@@ -6,7 +6,7 @@ describe API::Services, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:user2) { create(:user) }
let(:project) {create(:empty_project, creator_id: user.id, namespace: user.namespace) }
let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) }
Service.available_services_names.each do |service|
describe "PUT /projects/:id/services/#{service.dasherize}" do
......@@ -16,6 +16,15 @@ describe API::Services, api: true do
put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
expect(response).to have_http_status(200)
current_service = project.services.first
event = current_service.event_names.empty? ? "foo" : current_service.event_names.first
state = current_service[event] || false
put api("/projects/#{project.id}/services/#{dashed_service}?#{event}=#{!state}", user), service_attrs
expect(response).to have_http_status(200)
expect(project.services.first[event]).not_to eq(state) unless event == "foo"
end
it "returns if required fields missing" do
......
......@@ -183,12 +183,25 @@ describe API::Todos, api: true do
expect(response.status).to eq(404)
end
it 'returns an error if the issuable is not accessible' do
guest = create(:user)
project_1.team << [guest, :guest]
post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", guest)
if issuable_type == 'merge_requests'
expect(response).to have_http_status(403)
else
expect(response).to have_http_status(404)
end
end
end
describe 'POST :id/issuable_type/:issueable_id/todo' do
context 'for an issue' do
it_behaves_like 'an issuable', 'issues' do
let(:issuable) { create(:issue, author: author_1, project: project_1) }
let(:issuable) { create(:issue, :confidential, author: author_1, project: project_1) }
end
end
......
......@@ -179,9 +179,24 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end
shared_examples 'an unsubscribeable thread' do
it_behaves_like 'an unsubscribeable thread with incoming address without %{key}'
it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
is_expected.to have_header 'List-Unsubscribe', /mailto/
is_expected.to have_header 'List-Unsubscribe', /^<.+,.+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }
end
shared_examples 'an unsubscribeable thread with incoming address without %{key}' do
include_context 'reply-by-email is enabled with incoming address without %{key}'
it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
is_expected.not_to have_header 'List-Unsubscribe', /mailto/
is_expected.to have_header 'List-Unsubscribe', /^<[^,]+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }
......
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