New upstream version 11.8.2

This commit is contained in:
Sruthi Chandran 2019-03-13 22:55:13 +05:30
parent 0329642ba5
commit 5595a2eca7
129 changed files with 2040 additions and 687 deletions

View file

@ -2,6 +2,48 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 11.8.2 (2019-03-13)
### Security (1 change)
- Fixed ability to see private groups by users not belonging to given group.
### Fixed (5 changes)
- Fix import_jid error on project import. !25239
- Properly handle multiple X-Forwarded-For addresses in runner IP. !25511
- Fix error when viewing group issue boards when user doesn't have explicit group permissions. !25524
- Fix method to mark a project repository as writable. !25546
- Allow project members to see private group if the project is in the group namespace.
## 11.8.1 (2019-02-28)
### Security (21 changes)
- Stop linking to unrecognized package sources. !55518
- Don't allow non-members to see private related MRs.
- Do not display impersonated sessions under active sessions and remove ability to revoke session.
- Display only information visible to current user on the Milestone page.
- Show only merge requests visible to user on milestone detail page.
- Disable issue boards API when issues are disabled.
- Don't show new issue link after move when a user does not have permissions.
- Fix git clone revealing private repo's presence.
- Fix blind SSRF in Prometheus integration by checking URL before querying.
- Check snippet attached file to be moved is within designated directory.
- Check if desired milestone for an issue is available.
- Fix arbitrary file read via diffs during import.
- Display the correct number of MRs a user has access to.
- Forbid creating discussions for users with restricted access.
- Do not disclose milestone titles for unauthorized users.
- Validate session key when authorizing with GCP to create a cluster.
- Block local URLs for Kubernetes integration.
- Limit mermaid rendering to 5K characters.
- Remove the possibility to share a project with a group that a user is not a member of.
- Fix leaking private repository information in API.
- Prevent releases links API to leak tag existance.
## 11.8.0 (2019-02-22) ## 11.8.0 (2019-02-22)
### Security (7 changes, 1 of them is from the community) ### Security (7 changes, 1 of them is from the community)

View file

@ -1 +1 @@
11.8.0 11.8.2

View file

@ -1,4 +1,5 @@
import flash from '~/flash'; import flash from '~/flash';
import { sprintf, __ } from '../../locale';
// Renders diagrams and flowcharts from text using Mermaid in any element with the // Renders diagrams and flowcharts from text using Mermaid in any element with the
// `js-render-mermaid` class. // `js-render-mermaid` class.
@ -14,6 +15,9 @@ import flash from '~/flash';
// </pre> // </pre>
// //
// This is an arbitary number; Can be iterated upon when suitable.
const MAX_CHAR_LIMIT = 5000;
export default function renderMermaid($els) { export default function renderMermaid($els) {
if (!$els.length) return; if (!$els.length) return;
@ -34,6 +38,21 @@ export default function renderMermaid($els) {
$els.each((i, el) => { $els.each((i, el) => {
const source = el.textContent; const source = el.textContent;
/**
* Restrict the rendering to a certain amount of character to
* prevent mermaidjs from hanging up the entire thread and
* causing a DoS.
*/
if (source && source.length > MAX_CHAR_LIMIT) {
el.textContent = sprintf(
__(
'Cannot render the image. Maximum character count (%{charLimit}) has been exceeded.',
),
{ charLimit: MAX_CHAR_LIMIT },
);
return;
}
// Remove any extra spans added by the backend syntax highlighting. // Remove any extra spans added by the backend syntax highlighting.
Object.assign(el, { textContent: source }); Object.assign(el, { textContent: source });

View file

@ -315,7 +315,7 @@ export default {
:endpoint="mr.testResultsPath" :endpoint="mr.testResultsPath"
/> />
<div class="mr-widget-section p-0"> <div class="mr-widget-section">
<component :is="componentName" :mr="mr" :service="service" /> <component :is="componentName" :mr="mr" :service="service" />
<section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links"> <section v-if="shouldRenderCollaborationStatus" class="mr-info-list mr-links">

View file

@ -82,7 +82,6 @@
} }
.mr-widget-body, .mr-widget-body,
.mr-widget-section,
.mr-widget-content, .mr-widget-content,
.mr-widget-footer { .mr-widget-footer {
padding: $gl-padding; padding: $gl-padding;

View file

@ -8,7 +8,7 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path } format.html { redirect_to milestone_redirect_path }
format.json do format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", { render json: tabs_json("shared/milestones/_merge_requests_tab", {
merge_requests: @milestone.sorted_merge_requests, # rubocop:disable Gitlab/ModuleWithInstanceVariables merge_requests: @milestone.sorted_merge_requests(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables
show_project_name: true show_project_name: true
}) })
end end

View file

@ -2,6 +2,10 @@
module GoogleApi module GoogleApi
class AuthorizationsController < ApplicationController class AuthorizationsController < ApplicationController
include Gitlab::Utils::StrongMemoize
before_action :validate_session_key!
def callback def callback
token, expires_at = GoogleApi::CloudPlatform::Client token, expires_at = GoogleApi::CloudPlatform::Client
.new(nil, callback_google_api_auth_url) .new(nil, callback_google_api_auth_url)
@ -11,21 +15,27 @@ module GoogleApi
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] = session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at] =
expires_at.to_s expires_at.to_s
state_redirect_uri = redirect_uri_from_session_key(params[:state]) redirect_to redirect_uri_from_session
if state_redirect_uri
redirect_to state_redirect_uri
else
redirect_to root_path
end
end end
private private
def redirect_uri_from_session_key(state) def validate_session_key!
key = GoogleApi::CloudPlatform::Client access_denied! unless redirect_uri_from_session.present?
.session_key_for_redirect_uri(params[:state]) end
session[key] if key
def redirect_uri_from_session
strong_memoize(:redirect_uri_from_session) do
if params[:state].present?
session[session_key_for_redirect_uri(params[:state])]
else
nil
end
end
end
def session_key_for_redirect_uri(state)
GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(state)
end end
end end
end end

View file

@ -2,15 +2,6 @@
class Profiles::ActiveSessionsController < Profiles::ApplicationController class Profiles::ActiveSessionsController < Profiles::ApplicationController
def index def index
@sessions = ActiveSession.list(current_user) @sessions = ActiveSession.list(current_user).reject(&:is_impersonated)
end
def destroy
ActiveSession.destroy(current_user, params[:id])
respond_to do |format|
format.html { redirect_to profile_active_sessions_url, status: :found }
format.js { head :ok }
end
end end
end end

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
class Projects::AutocompleteSourcesController < Projects::ApplicationController class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :authorize_read_milestone!, only: :milestones
def members def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target) render json: ::Projects::ParticipantsService.new(@project, current_user).execute(target)
end end

View file

@ -65,7 +65,11 @@ class Projects::CommitController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def merge_requests def merge_requests
@merge_requests = @commit.merge_requests.map do |mr| @merge_requests = MergeRequestsFinder.new(
current_user,
project_id: @project.id,
commit_sha: @commit.sha
).execute.map do |mr|
{ iid: mr.iid, path: merge_request_path(mr), title: mr.title } { iid: mr.iid, path: merge_request_path(mr), title: mr.title }
end end

View file

@ -13,9 +13,10 @@ class Projects::GroupLinksController < Projects::ApplicationController
group = Group.find(params[:link_group_id]) if params[:link_group_id].present? group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
if group if group
return render_404 unless can?(current_user, :read_group, group) result = Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group)
return render_404 if result[:http_status] == 404
Projects::GroupLinks::CreateService.new(project, current_user, group_link_create_params).execute(group) flash[:alert] = result[:message] if result[:http_status] == 409
else else
flash[:alert] = 'Please select a group.' flash[:alert] = 'Please select a group.'
end end

View file

@ -37,13 +37,20 @@ class MergeRequestsFinder < IssuableFinder
end end
def filter_items(_items) def filter_items(_items)
items = by_source_branch(super) items = by_commit(super)
items = by_source_branch(items)
items = by_wip(items) items = by_wip(items)
by_target_branch(items) by_target_branch(items)
end end
private private
def by_commit(items)
return items unless params[:commit_sha].presence
items.by_commit_sha(params[:commit_sha])
end
def source_branch def source_branch
@source_branch ||= params[:source_branch].presence @source_branch ||= params[:source_branch].presence
end end

View file

@ -16,7 +16,6 @@ module Types
field :description, GraphQL::STRING_TYPE, null: true field :description, GraphQL::STRING_TYPE, null: true
field :default_branch, GraphQL::STRING_TYPE, null: true
field :tag_list, GraphQL::STRING_TYPE, null: true field :tag_list, GraphQL::STRING_TYPE, null: true
field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true field :ssh_url_to_repo, GraphQL::STRING_TYPE, null: true
@ -59,7 +58,6 @@ module Types
end end
field :import_status, GraphQL::STRING_TYPE, null: true field :import_status, GraphQL::STRING_TYPE, null: true
field :ci_config_path, GraphQL::STRING_TYPE, null: true
field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true field :only_allow_merge_if_pipeline_succeeds, GraphQL::BOOLEAN_TYPE, null: true
field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true field :request_access_enabled, GraphQL::BOOLEAN_TYPE, null: true

View file

@ -74,6 +74,7 @@ module Emails
@new_issue = new_issue @new_issue = new_issue
@new_project = new_issue.project @new_project = new_issue.project
@can_access_project = recipient.can?(:read_project, @new_project)
mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason)) mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason))
end end

View file

@ -5,7 +5,8 @@ class ActiveSession
attr_accessor :created_at, :updated_at, attr_accessor :created_at, :updated_at,
:session_id, :ip_address, :session_id, :ip_address,
:browser, :os, :device_name, :device_type :browser, :os, :device_name, :device_type,
:is_impersonated
def current?(session) def current?(session)
return false if session_id.nil? || session.id.nil? return false if session_id.nil? || session.id.nil?
@ -31,7 +32,8 @@ class ActiveSession
device_type: client.device_type, device_type: client.device_type,
created_at: user.current_sign_in_at || timestamp, created_at: user.current_sign_in_at || timestamp,
updated_at: timestamp, updated_at: timestamp,
session_id: session_id session_id: session_id,
is_impersonated: request.session[:impersonator_id].present?
) )
redis.pipelined do redis.pipelined do

View file

@ -41,7 +41,7 @@ module Clusters
validate :no_namespace, unless: :allow_user_defined_namespace? validate :no_namespace, unless: :allow_user_defined_namespace?
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned) # We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
validates :api_url, url: true, presence: true validates :api_url, public_url: true, presence: true
validates :token, presence: true validates :token, presence: true
validate :prevent_modification, on: :update validate :prevent_modification, on: :update

View file

@ -74,6 +74,7 @@ module Issuable
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 } validates :title, presence: true, length: { maximum: 255 }
validate :milestone_is_valid
scope :authored, ->(user) { where(author_id: user) } scope :authored, ->(user) { where(author_id: user) }
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
@ -117,6 +118,16 @@ module Issuable
def has_multiple_assignees? def has_multiple_assignees?
assignees.count > 1 assignees.count > 1
end end
def milestone_available?
project_id == milestone&.project_id || project.ancestors_upto.compact.include?(milestone&.group)
end
private
def milestone_is_valid
errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available?
end
end end
class_methods do class_methods do

View file

@ -46,12 +46,31 @@ module Milestoneish
end end
end end
def issue_participants_visible_by_user(user)
User.joins(:issue_assignees)
.where('issue_assignees.issue_id' => issues_visible_to_user(user).select(:id))
.distinct
end
def issue_labels_visible_by_user(user)
Label.joins(:label_links)
.where('label_links.target_id' => issues_visible_to_user(user).select(:id), 'label_links.target_type' => 'Issue')
.distinct
end
def sorted_issues(user) def sorted_issues(user)
issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority') issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority')
end end
def sorted_merge_requests def sorted_merge_requests(user)
merge_requests.sort_by_attribute('label_priority') merge_requests_visible_to_user(user).sort_by_attribute('label_priority')
end
def merge_requests_visible_to_user(user)
memoize_per_user(user, :merge_requests_visible_to_user) do
MergeRequestsFinder.new(user, issues_finder_params)
.execute.where(milestone_id: milestoneish_id)
end
end end
def upcoming? def upcoming?

View file

@ -71,7 +71,7 @@ class MergeRequest < ActiveRecord::Base
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
after_create :ensure_merge_request_diff, unless: :importing? after_create :ensure_merge_request_diff
after_update :clear_memoized_shas after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed after_update :reload_diff_if_branch_changed
after_save :ensure_metrics after_save :ensure_metrics
@ -189,6 +189,9 @@ class MergeRequest < ActiveRecord::Base
after_save :keep_around_commit after_save :keep_around_commit
alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id
def self.reference_prefix def self.reference_prefix
'!' '!'
end end
@ -837,10 +840,6 @@ class MergeRequest < ActiveRecord::Base
target_project != source_project target_project != source_project
end end
def project
target_project
end
# If the merge request closes any issues, save this information in the # If the merge request closes any issues, save this information in the
# `MergeRequestsClosingIssues` model. This is a performance optimization. # `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires # Calculating this information for a number of merge requests requires

View file

@ -25,6 +25,8 @@ class MergeRequestDiff < ActiveRecord::Base
has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) } has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
validates :base_commit_sha, :head_commit_sha, :start_commit_sha, sha: true
state_machine :state, initial: :empty do state_machine :state, initial: :empty do
event :clean do event :clean do
transition any => :without_files transition any => :without_files

View file

@ -1830,7 +1830,7 @@ class Project < ActiveRecord::Base
# Set repository as writable again # Set repository as writable again
def set_repository_writable! def set_repository_writable!
with_lock do with_lock do
update_column(repository_read_only, false) update_column(:repository_read_only, false)
end end
end end

View file

@ -76,7 +76,7 @@ class ProjectFeature < ActiveRecord::Base
# This feature might not be behind a feature flag at all, so default to true # This feature might not be behind a feature flag at all, so default to true
return false unless ::Feature.enabled?(feature, user, default_enabled: true) return false unless ::Feature.enabled?(feature, user, default_enabled: true)
get_permission(user, access_level(feature)) get_permission(user, feature)
end end
def access_level(feature) def access_level(feature)
@ -134,12 +134,12 @@ class ProjectFeature < ActiveRecord::Base
(FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")} (FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")}
end end
def get_permission(user, level) def get_permission(user, feature)
case level case access_level(feature)
when DISABLED when DISABLED
false false
when PRIVATE when PRIVATE
user && (project.team.member?(user) || user.full_private_access?) team_access?(user, feature)
when ENABLED when ENABLED
true true
when PUBLIC when PUBLIC
@ -148,4 +148,11 @@ class ProjectFeature < ActiveRecord::Base
true true
end end
end end
def team_access?(user, feature)
return unless user
return true if user.full_private_access?
project.team.member?(user, ProjectFeature.required_minimum_access_level(feature))
end
end end

View file

@ -71,7 +71,7 @@ class PrometheusService < MonitoringService
end end
def prometheus_client def prometheus_client
RestClient::Resource.new(api_url, max_redirects: 0) if api_url && manual_configuration? && active? RestClient::Resource.new(api_url, max_redirects: 0) if should_return_client?
end end
def prometheus_available? def prometheus_available?
@ -83,6 +83,10 @@ class PrometheusService < MonitoringService
private private
def should_return_client?
api_url && manual_configuration? && active? && valid?
end
def synchronize_service_state def synchronize_service_state
self.active = prometheus_available? || manual_configuration? self.active = prometheus_available? || manual_configuration?

View file

@ -26,7 +26,7 @@ class GroupPolicy < BasePolicy
condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) } condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
condition(:has_projects) do condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true }).execute.any? GroupProjectsFinder.new(group: @subject, current_user: @user, options: { include_subgroups: true, only_owned: true }).execute.any?
end end
condition(:has_clusters, scope: :subject) { clusterable_has_clusters? } condition(:has_clusters, scope: :subject) { clusterable_has_clusters? }
@ -53,8 +53,9 @@ class GroupPolicy < BasePolicy
rule { admin }.enable :read_group rule { admin }.enable :read_group
rule { has_projects }.policy do rule { has_projects }.policy do
enable :read_group enable :read_list
enable :read_label enable :read_label
enable :read_group
end end
rule { has_access }.enable :read_namespace rule { has_access }.enable :read_namespace

View file

@ -299,6 +299,8 @@ class ProjectPolicy < BasePolicy
rule { issues_disabled }.policy do rule { issues_disabled }.policy do
prevent(*create_read_update_admin_destroy(:issue)) prevent(*create_read_update_admin_destroy(:issue))
prevent(*create_read_update_admin_destroy(:board))
prevent(*create_read_update_admin_destroy(:list))
end end
rule { merge_requests_disabled | repository_disabled }.policy do rule { merge_requests_disabled | repository_disabled }.policy do
@ -462,7 +464,7 @@ class ProjectPolicy < BasePolicy
when ProjectFeature::DISABLED when ProjectFeature::DISABLED
false false
when ProjectFeature::PRIVATE when ProjectFeature::PRIVATE
guest? || admin? admin? || team_access_level >= ProjectFeature.required_minimum_access_level(feature)
else else
true true
end end

View file

@ -387,4 +387,10 @@ class IssuableBaseService < BaseService
def parent def parent
project project
end end
# we need to check this because milestone from milestone_id param is displayed on "new" page
# where private project milestone could leak without this check
def ensure_milestone_available(issuable)
issuable.milestone_id = nil unless issuable.milestone_available?
end
end end

View file

@ -6,7 +6,9 @@ module Issues
def execute def execute
filter_resolve_discussion_params filter_resolve_discussion_params
@issue = project.issues.new(issue_params) @issue = project.issues.new(issue_params).tap do |issue|
ensure_milestone_available(issue)
end
end end
def issue_params_with_info_from_discussions def issue_params_with_info_from_discussions

View file

@ -19,6 +19,7 @@ module MergeRequests
merge_request.target_project = find_target_project merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch merge_request.target_branch = find_target_branch
merge_request.can_be_created = projects_and_branches_valid? merge_request.can_be_created = projects_and_branches_valid?
ensure_milestone_available(merge_request)
# compare branches only if branches are valid, otherwise # compare branches only if branches are valid, otherwise
# compare_branches may raise an error # compare_branches may raise an error

View file

@ -4,13 +4,19 @@ module Projects
module GroupLinks module GroupLinks
class CreateService < BaseService class CreateService < BaseService
def execute(group) def execute(group)
return false unless group return error('Not Found', 404) unless group && can?(current_user, :read_namespace, group)
project.project_group_links.create( link = project.project_group_links.new(
group: group, group: group,
group_access: params[:link_group_access], group_access: params[:link_group_access],
expires_at: params[:expires_at] expires_at: params[:expires_at]
) )
if link.save
success(link: link)
else
error(link.errors.full_messages.to_sentence, 409)
end
end end
end end
end end

View file

@ -11,6 +11,8 @@ class FileMover
end end
def execute def execute
return unless valid?
move move
if update_markdown if update_markdown
@ -21,6 +23,12 @@ class FileMover
private private
def valid?
Pathname.new(temp_file_path).realpath.to_path.start_with?(
(Pathname(temp_file_uploader.root) + temp_file_uploader.base_dir).to_path
)
end
def move def move
FileUtils.mkdir_p(File.dirname(file_path)) FileUtils.mkdir_p(File.dirname(file_path))
FileUtils.move(temp_file_path, file_path) FileUtils.move(temp_file_path, file_path)

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class ShaValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank? || value.match(/\A\h{40}\z/)
record.errors.add(attribute, 'is not a valid SHA')
end
end

View file

@ -1,6 +1,9 @@
%p %p
Issue was moved to another project. Issue was moved to another project.
- if @can_access_project
%p %p
New issue: New issue:
= link_to project_issue_url(@new_project, @new_issue) do = link_to project_issue_url(@new_project, @new_issue) do
= @new_issue.title = @new_issue.title
- else
You don't have access to the project.

View file

@ -1,4 +1,8 @@
Issue was moved to another project. Issue was moved to another project.
<% if @can_access_project %>
New issue location: New issue location:
<%= project_issue_url(@new_project, @new_issue) %> <%= project_issue_url(@new_project, @new_issue) %>
<% else %>
You don't have access to the project.
<% end %>

View file

@ -23,9 +23,3 @@
%strong Signed in %strong Signed in
on on
= l(active_session.created_at, format: :short) = l(active_session.created_at, format: :short)
- unless is_current_session
.float-right
= link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
%span.sr-only Revoke
Revoke

View file

@ -3,9 +3,4 @@
This project manages its dependencies using This project manages its dependencies using
%strong= viewer.manager_name %strong= viewer.manager_name
- if viewer.package_name
and defines a #{viewer.package_type} named
%strong<
= link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer' = link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'

View file

@ -32,7 +32,7 @@
= milestone_progress_bar(milestone) = milestone_progress_bar(milestone)
= link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path = link_to pluralize(milestone.total_issues_count(current_user), 'Issue'), issues_path
&middot; &middot;
= link_to pluralize(milestone.merge_requests.size, 'Merge Request'), merge_requests_path = link_to pluralize(milestone.merge_requests_visible_to_user(current_user).size, 'Merge Request'), merge_requests_path
.float-lg-right.light #{milestone.percent_complete(current_user)}% complete .float-lg-right.light #{milestone.percent_complete(current_user)}% complete
.col-sm-2 .col-sm-2
.milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end .milestone-actions.d-flex.justify-content-sm-start.justify-content-md-end

View file

@ -12,7 +12,7 @@
%li.nav-item %li.nav-item
= link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do = link_to '#tab-merge-requests', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests Merge Requests
%span.badge.badge-pill= milestone.merge_requests.size %span.badge.badge-pill= milestone.merge_requests_visible_to_user(current_user).size
- else - else
%li.nav-item %li.nav-item
= link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do = link_to '#tab-merge-requests', class: 'nav-link active', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
@ -21,11 +21,11 @@
%li.nav-item %li.nav-item
= link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do = link_to '#tab-participants', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants Participants
%span.badge.badge-pill= milestone.participants.count %span.badge.badge-pill= milestone.issue_participants_visible_by_user(current_user).count
%li.nav-item %li.nav-item
= link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do = link_to '#tab-labels', class: 'nav-link', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels Labels
%span.badge.badge-pill= milestone.labels.count %span.badge.badge-pill= milestone.issue_labels_visible_by_user(current_user).count
- issues = milestone.sorted_issues(current_user) - issues = milestone.sorted_issues(current_user)
- show_project_name = local_assigns.fetch(:show_project_name, false) - show_project_name = local_assigns.fetch(:show_project_name, false)

View file

@ -40,7 +40,7 @@ scope(path: '*namespace_id/:project_id',
# /info/refs?service=git-receive-pack, but nothing else. # /info/refs?service=git-receive-pack, but nothing else.
# #
git_http_handshake = lambda do |request| git_http_handshake = lambda do |request|
::Constraints::ProjectUrlConstrainer.new.matches?(request) && ::Constraints::ProjectUrlConstrainer.new.matches?(request, existence_check: false) &&
(request.query_string.blank? || (request.query_string.blank? ||
request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)) request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/))
end end

View file

@ -48,7 +48,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Third party offers](../user/admin_area/settings/third_party_offers.md) - [Third party offers](../user/admin_area/settings/third_party_offers.md)
- [Compliance](compliance.md): A collection of features from across the application that you may configure to help ensure that your GitLab instance and DevOps workflow meet compliance standards. - [Compliance](compliance.md): A collection of features from across the application that you may configure to help ensure that your GitLab instance and DevOps workflow meet compliance standards.
- [Diff limits](../user/admin_area/diff_limits.md): Configure the diff rendering size limits of branch comparison pages. - [Diff limits](../user/admin_area/diff_limits.md): Configure the diff rendering size limits of branch comparison pages.
- [Merge request diffs](merge_request_diffs.md): Configure the diffs shown on merge requests - [Merge request diffs storage](merge_request_diffs.md): Configure merge requests diffs external storage.
- [Broadcast Messages](../user/admin_area/broadcast_messages.md): Send messages to GitLab users through the UI. - [Broadcast Messages](../user/admin_area/broadcast_messages.md): Send messages to GitLab users through the UI.
#### Customizing GitLab's appearance #### Customizing GitLab's appearance

View file

@ -1,7 +1,6 @@
# Merge request diffs administration # Merge request diffs storage **[CORE ONLY]**
> **Notes:** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/52568) in GitLab 11.8.
> - External merge request diffs introduced in GitLab 11.8
Merge request diffs are size-limited copies of diffs associated with merge Merge request diffs are size-limited copies of diffs associated with merge
requests. When viewing a merge request, diffs are sourced from these copies requests. When viewing a merge request, diffs are sourced from these copies
@ -16,9 +15,7 @@ large, in which case, switching to external storage is recommended.
Merge request diffs can be stored on disk, or in object storage. In general, it Merge request diffs can be stored on disk, or in object storage. In general, it
is better to store the diffs in the database than on disk. is better to store the diffs in the database than on disk.
To enable external storage of merge request diffs: To enable external storage of merge request diffs, follow the instructions below.
---
**In Omnibus installations:** **In Omnibus installations:**
@ -30,16 +27,14 @@ To enable external storage of merge request diffs:
1. _The external diffs will be stored in in 1. _The external diffs will be stored in in
`/var/opt/gitlab/gitlab-rails/shared/external-diffs`._ To change the path, `/var/opt/gitlab/gitlab-rails/shared/external-diffs`._ To change the path,
for example to `/mnt/storage/external-diffs`, edit `/etc/gitlab/gitlab.rb` for example, to `/mnt/storage/external-diffs`, edit `/etc/gitlab/gitlab.rb`
and add the following line: and add the following line:
```ruby ```ruby
gitlab_rails['external_diffs_storage_path'] = "/mnt/storage/external-diffs" gitlab_rails['external_diffs_storage_path'] = "/mnt/storage/external-diffs"
``` ```
1. Save the file and [reconfigure GitLab][] for the changes to take effect. 1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
---
**In installations from source:** **In installations from source:**
@ -52,7 +47,7 @@ To enable external storage of merge request diffs:
``` ```
1. _The external diffs will be stored in 1. _The external diffs will be stored in
`/home/git/gitlab/shared/external-diffs`._ To change the path, for example `/home/git/gitlab/shared/external-diffs`._ To change the path, for example,
to `/mnt/storage/external-diffs`, edit `/home/git/gitlab/config/gitlab.yml` to `/mnt/storage/external-diffs`, edit `/home/git/gitlab/config/gitlab.yml`
and add or amend the following lines: and add or amend the following lines:
@ -62,18 +57,18 @@ To enable external storage of merge request diffs:
storage_path: /mnt/storage/external-diffs storage_path: /mnt/storage/external-diffs
``` ```
1. Save the file and [restart GitLab][] for the changes to take effect. 1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.
### Using object storage ### Using object storage
Instead of storing the external diffs on disk, we recommended you use an object Instead of storing the external diffs on disk, we recommended the use of an object
store like AWS S3 instead. This configuration relies on valid AWS credentials to store like AWS S3 instead. This configuration relies on valid AWS credentials to
be configured already. be configured already.
### Object Storage Settings ### Object Storage Settings
For source installations, these settings are nested under `external_diffs:` and For source installations, these settings are nested under `external_diffs:` and
then `object_store:`. On omnibus installs, they are prefixed by then `object_store:`. On Omnibus installations, they are prefixed by
`external_diffs_object_store_`. `external_diffs_object_store_`.
| Setting | Description | Default | | Setting | Description | Default |
@ -118,7 +113,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
} }
``` ```
NOTE: if you are using AWS IAM profiles, be sure to omit the Note that, if you are using AWS IAM profiles, be sure to omit the
AWS access key and secret access key/value pairs. For example: AWS access key and secret access key/value pairs. For example:
```ruby ```ruby
@ -129,9 +124,7 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
} }
``` ```
1. Save the file and [reconfigure GitLab][] for the changes to take effect. 1. Save the file and [reconfigure GitLab](restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
---
**In installations from source:** **In installations from source:**
@ -151,4 +144,4 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
region: eu-central-1 region: eu-central-1
``` ```
1. Save the file and [restart GitLab][] for the changes to take effect. 1. Save the file and [restart GitLab](restart_gitlab.md#installations-from-source) for the changes to take effect.

View file

@ -33,7 +33,7 @@ Please consider using a virtual machine to run GitLab.
## Ruby versions ## Ruby versions
GitLab requires Ruby (MRI) 2.3. Support for Ruby versions below 2.3 (2.1, 2.2) will stop with GitLab 8.13. GitLab requires Ruby (MRI) 2.5. Support for Ruby versions below 2.5 (2.3, 2.4) will stop with GitLab 11.6.
You will have to use the standard MRI implementation of Ruby. You will have to use the standard MRI implementation of Ruby.
We love [JRuby](https://www.jruby.org/) and [Rubinius](https://rubinius.com) but GitLab We love [JRuby](https://www.jruby.org/) and [Rubinius](https://rubinius.com) but GitLab

View file

@ -4,7 +4,7 @@
> in GitLab 10.8. > in GitLab 10.8.
GitLab lists all devices that have logged into your account. This allows you to GitLab lists all devices that have logged into your account. This allows you to
review the sessions and revoke any of it that you don't recognize. review the sessions.
## Listing all active sessions ## Listing all active sessions
@ -12,9 +12,3 @@ review the sessions and revoke any of it that you don't recognize.
1. Navigate to the **Active Sessions** tab. 1. Navigate to the **Active Sessions** tab.
![Active sessions list](img/active_sessions_list.png) ![Active sessions list](img/active_sessions_list.png)
## Revoking a session
1. Navigate to your [profile's](#profile-settings) **Settings > Active Sessions**.
1. Click on **Revoke** besides a session. The current session cannot be
revoked, as this would sign you out of GitLab.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -318,10 +318,18 @@ module API
use :pagination use :pagination
end end
get ':id/repository/commits/:sha/merge_requests', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do get ':id/repository/commits/:sha/merge_requests', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
authorize! :read_merge_request, user_project
commit = user_project.commit(params[:sha]) commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit not_found! 'Commit' unless commit
present paginate(commit.merge_requests), with: Entities::MergeRequestBasic commit_merge_requests = MergeRequestsFinder.new(
current_user,
project_id: user_project.id,
commit_sha: commit.sha
).execute
present paginate(commit_merge_requests), with: Entities::MergeRequestBasic
end end
end end
end end

View file

@ -156,7 +156,7 @@ module API
class BasicProjectDetails < ProjectIdentity class BasicProjectDetails < ProjectIdentity
include ::API::ProjectsRelationBuilder include ::API::ProjectsRelationBuilder
expose :default_branch expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
# Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770
expose :tag_list do |project| expose :tag_list do |project|
# project.tags.order(:name).pluck(:name) is the most suitable option # project.tags.order(:name).pluck(:name) is the most suitable option
@ -261,7 +261,7 @@ module API
expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds, as: :public_jobs expose :public_builds, as: :public_jobs
expose :ci_config_path expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
expose :shared_with_groups do |project, options| expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links, options) SharedGroup.represent(project.project_group_links, options)
end end
@ -270,8 +270,9 @@ module API
expose :only_allow_merge_if_all_discussions_are_resolved expose :only_allow_merge_if_all_discussions_are_resolved
expose :printing_merge_request_link_enabled expose :printing_merge_request_link_enabled
expose :merge_method expose :merge_method
expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics options[:statistics] && Ability.allowed?(options[:current_user], :download_code, project)
}
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def self.preload_relation(projects_relation, options = {}) def self.preload_relation(projects_relation, options = {})

View file

@ -22,7 +22,7 @@ module API
get ':id/environments' do get ':id/environments' do
authorize! :read_environment, user_project authorize! :read_environment, user_project
present paginate(user_project.environments), with: Entities::Environment present paginate(user_project.environments), with: Entities::Environment, current_user: current_user
end end
desc 'Creates a new environment' do desc 'Creates a new environment' do
@ -40,7 +40,7 @@ module API
environment = user_project.environments.create(declared_params) environment = user_project.environments.create(declared_params)
if environment.persisted? if environment.persisted?
present environment, with: Entities::Environment present environment, with: Entities::Environment, current_user: current_user
else else
render_validation_error!(environment) render_validation_error!(environment)
end end
@ -63,7 +63,7 @@ module API
update_params = declared_params(include_missing: false).extract!(:name, :external_url) update_params = declared_params(include_missing: false).extract!(:name, :external_url)
if environment.update(update_params) if environment.update(update_params)
present environment, with: Entities::Environment present environment, with: Entities::Environment, current_user: current_user
else else
render_validation_error!(environment) render_validation_error!(environment)
end end
@ -99,7 +99,7 @@ module API
environment.stop_with_action!(current_user) environment.stop_with_action!(current_user)
status 200 status 200
present environment, with: Entities::Environment present environment, with: Entities::Environment, current_user: current_user
end end
end end
end end

View file

@ -70,14 +70,7 @@ module API
def find_noteable(parent, noteables_str, noteable_id) def find_noteable(parent, noteables_str, noteable_id)
noteable = public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend noteable = public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
readable = readable = can?(current_user, noteable_read_ability_name(noteable), noteable)
if noteable.is_a?(Commit)
# for commits there is not :read_commit policy, check if user
# has :read_note permission on the commit's project
can?(current_user, :read_note, user_project)
else
can?(current_user, noteable_read_ability_name(noteable), noteable)
end
return not_found!(noteables_str) unless readable return not_found!(noteables_str) unless readable
@ -89,12 +82,11 @@ module API
end end
def create_note(noteable, opts) def create_note(noteable, opts)
policy_object = noteable.is_a?(Commit) ? user_project : noteable authorize!(:create_note, noteable)
authorize!(:create_note, policy_object)
parent = noteable_parent(noteable) parent = noteable_parent(noteable)
opts.delete(:created_at) unless current_user.can?(:set_note_created_at, policy_object) opts.delete(:created_at) unless current_user.can?(:set_note_created_at, noteable)
opts[:updated_at] = opts[:created_at] if opts[:created_at] opts[:updated_at] = opts[:created_at] if opts[:created_at]

View file

@ -26,7 +26,7 @@ module API
end end
def get_runner_ip def get_runner_ip
{ ip_address: request.env["HTTP_X_FORWARDED_FOR"] || request.ip } { ip_address: env["action_dispatch.remote_ip"].to_s || request.ip }
end end
def current_runner def current_runner

View file

@ -184,7 +184,8 @@ module API
if project.saved? if project.saved?
present project, with: Entities::Project, present project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, project) user_can_admin_project: can?(current_user, :admin_project, project),
current_user: current_user
else else
if project.errors[:limit_reached].present? if project.errors[:limit_reached].present?
error!(project.errors[:limit_reached], 403) error!(project.errors[:limit_reached], 403)
@ -217,7 +218,8 @@ module API
if project.saved? if project.saved?
present project, with: Entities::Project, present project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, project) user_can_admin_project: can?(current_user, :admin_project, project),
current_user: current_user
else else
render_validation_error!(project) render_validation_error!(project)
end end
@ -279,7 +281,8 @@ module API
conflict!(forked_project.errors.messages) conflict!(forked_project.errors.messages)
else else
present forked_project, with: Entities::Project, present forked_project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, forked_project) user_can_admin_project: can?(current_user, :admin_project, forked_project),
current_user: current_user
end end
end end
@ -328,7 +331,8 @@ module API
if result[:status] == :success if result[:status] == :success
present user_project, with: Entities::Project, present user_project, with: Entities::Project,
user_can_admin_project: can?(current_user, :admin_project, user_project) user_can_admin_project: can?(current_user, :admin_project, user_project),
current_user: current_user
else else
render_validation_error!(user_project) render_validation_error!(user_project)
end end
@ -342,7 +346,7 @@ module API
::Projects::UpdateService.new(user_project, current_user, archived: true).execute ::Projects::UpdateService.new(user_project, current_user, archived: true).execute
present user_project, with: Entities::Project present user_project, with: Entities::Project, current_user: current_user
end end
desc 'Unarchive a project' do desc 'Unarchive a project' do
@ -353,7 +357,7 @@ module API
::Projects::UpdateService.new(@project, current_user, archived: false).execute ::Projects::UpdateService.new(@project, current_user, archived: false).execute
present user_project, with: Entities::Project present user_project, with: Entities::Project, current_user: current_user
end end
desc 'Star a project' do desc 'Star a project' do
@ -366,7 +370,7 @@ module API
current_user.toggle_star(user_project) current_user.toggle_star(user_project)
user_project.reload user_project.reload
present user_project, with: Entities::Project present user_project, with: Entities::Project, current_user: current_user
end end
end end
@ -378,7 +382,7 @@ module API
current_user.toggle_star(user_project) current_user.toggle_star(user_project)
user_project.reload user_project.reload
present user_project, with: Entities::Project present user_project, with: Entities::Project, current_user: current_user
else else
not_modified! not_modified!
end end
@ -414,7 +418,7 @@ module API
result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project) result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project)
if result if result
present user_project.reload, with: Entities::Project present user_project.reload, with: Entities::Project, current_user: current_user
else else
render_api_error!("Project already forked", 409) if user_project.forked? render_api_error!("Project already forked", 409) if user_project.forked?
end end
@ -436,27 +440,24 @@ module API
end end
params do params do
requires :group_id, type: Integer, desc: 'The ID of a group' requires :group_id, type: Integer, desc: 'The ID of a group'
requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level' requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level'
optional :expires_at, type: Date, desc: 'Share expiration date' optional :expires_at, type: Date, desc: 'Share expiration date'
end end
post ":id/share" do post ":id/share" do
authorize! :admin_project, user_project authorize! :admin_project, user_project
group = Group.find_by_id(params[:group_id]) group = Group.find_by_id(params[:group_id])
unless group && can?(current_user, :read_group, group)
not_found!('Group')
end
unless user_project.allowed_to_share_with_group? unless user_project.allowed_to_share_with_group?
break render_api_error!("The project sharing with group is disabled", 400) break render_api_error!("The project sharing with group is disabled", 400)
end end
link = user_project.project_group_links.new(declared_params(include_missing: false)) result = ::Projects::GroupLinks::CreateService.new(user_project, current_user, declared_params(include_missing: false))
.execute(group)
if link.save if result[:status] == :success
present link, with: Entities::ProjectGroupLink present result[:link], with: Entities::ProjectGroupLink
else else
render_api_error!(link.errors.full_messages.first, 409) render_api_error!(result[:message], result[:http_status])
end end
end end
@ -520,7 +521,7 @@ module API
result = ::Projects::TransferService.new(user_project, current_user).execute(namespace) result = ::Projects::TransferService.new(user_project, current_user).execute(namespace)
if result if result
present user_project, with: Entities::Project present user_project, with: Entities::Project, current_user: current_user
else else
render_api_error!("Failed to transfer project #{user_project.errors.messages}", 400) render_api_error!("Failed to transfer project #{user_project.errors.messages}", 400)
end end

View file

@ -8,6 +8,8 @@ module API
RELEASE_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS RELEASE_ENDPOINT_REQUIREMETS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS
.merge(tag_name: API::NO_SLASH_URL_PART_REGEX) .merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
before { authorize! :read_release, user_project }
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end

View file

@ -2,12 +2,13 @@
module Constraints module Constraints
class ProjectUrlConstrainer class ProjectUrlConstrainer
def matches?(request) def matches?(request, existence_check: true)
namespace_path = request.params[:namespace_id] namespace_path = request.params[:namespace_id]
project_path = request.params[:project_id] || request.params[:id] project_path = request.params[:project_id] || request.params[:id]
full_path = [namespace_path, project_path].join('/') full_path = [namespace_path, project_path].join('/')
return false unless ProjectPathValidator.valid_path?(full_path) return false unless ProjectPathValidator.valid_path?(full_path)
return true unless existence_check
# We intentionally allow SELECT(*) here so result of this query can be used # We intentionally allow SELECT(*) here so result of this query can be used
# as cache for further Project.find_by_full_path calls within request # as cache for further Project.find_by_full_path calls within request

View file

@ -4,6 +4,7 @@ module Gitlab
module DependencyLinker module DependencyLinker
class BaseLinker class BaseLinker
URL_REGEX = %r{https?://[^'" ]+}.freeze URL_REGEX = %r{https?://[^'" ]+}.freeze
GIT_INVALID_URL_REGEX = /^git\+#{URL_REGEX}/.freeze
REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze REPO_REGEX = %r{[^/'" ]+/[^/'" ]+}.freeze
class_attribute :file_type class_attribute :file_type
@ -29,8 +30,25 @@ module Gitlab
highlighted_lines.join.html_safe highlighted_lines.join.html_safe
end end
def external_url(name, external_ref)
return if external_ref =~ GIT_INVALID_URL_REGEX
case external_ref
when /\A#{URL_REGEX}\z/
external_ref
when /\A#{REPO_REGEX}\z/
github_url(external_ref)
else
package_url(name)
end
end
private private
def package_url(_name)
raise NotImplementedError
end
def link_dependencies def link_dependencies
raise NotImplementedError raise NotImplementedError
end end

View file

@ -8,8 +8,8 @@ module Gitlab
private private
def link_packages def link_packages
link_packages_at_key("require", &method(:package_url)) link_packages_at_key("require")
link_packages_at_key("require-dev", &method(:package_url)) link_packages_at_key("require-dev")
end end
def package_url(name) def package_url(name)

View file

@ -3,8 +3,14 @@
module Gitlab module Gitlab
module DependencyLinker module DependencyLinker
class GemfileLinker < MethodLinker class GemfileLinker < MethodLinker
class_attribute :package_keyword
self.package_keyword = :gem
self.file_type = :gemfile self.file_type = :gemfile
GITHUB_REGEX = /(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/.freeze
GIT_REGEX = /(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]/.freeze
private private
def link_dependencies def link_dependencies
@ -14,20 +20,34 @@ module Gitlab
def link_urls def link_urls
# Link `github: "user/repo"` to https://github.com/user/repo # Link `github: "user/repo"` to https://github.com/user/repo
link_regex(/(github:|:github\s*=>)\s*['"](?<name>[^'"]+)['"]/, &method(:github_url)) link_regex(GITHUB_REGEX, &method(:github_url))
# Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo # Link `git: "https://gitlab.example.com/user/repo"` to https://gitlab.example.com/user/repo
link_regex(/(git:|:git\s*=>)\s*['"](?<name>#{URL_REGEX})['"]/, &:itself) link_regex(GIT_REGEX, &:itself)
# Link `source "https://rubygems.org"` to https://rubygems.org # Link `source "https://rubygems.org"` to https://rubygems.org
link_method_call('source', URL_REGEX, &:itself) link_method_call('source', URL_REGEX, &:itself)
end end
def link_packages def link_packages
# Link `gem "package_name"` to https://rubygems.org/gems/package_name packages = parse_packages
link_method_call('gem') do |name|
return if packages.blank?
packages.each do |package|
link_method_call('gem', package.name) do
external_url(package.name, package.external_ref)
end
end
end
def package_url(name)
"https://rubygems.org/gems/#{name}" "https://rubygems.org/gems/#{name}"
end end
def parse_packages
parser = Gitlab::DependencyLinker::Parser::Gemfile.new(plain_text)
parser.parse(keyword: self.class.package_keyword)
end end
end end
end end

View file

@ -11,7 +11,7 @@ module Gitlab
link_method_call('homepage', URL_REGEX, &:itself) link_method_call('homepage', URL_REGEX, &:itself)
link_method_call('license', &method(:license_url)) link_method_call('license', &method(:license_url))
link_method_call(%w[name add_dependency add_runtime_dependency add_development_dependency]) do |name| link_method_call(%w[add_dependency add_runtime_dependency add_development_dependency]) do |name|
"https://rubygems.org/gems/#{name}" "https://rubygems.org/gems/#{name}"
end end
end end

View file

@ -23,18 +23,22 @@ module Gitlab
# link_method_call('name') # link_method_call('name')
# # Will link `package` in `self.name = "package"` # # Will link `package` in `self.name = "package"`
def link_method_call(method_name, value = nil, &url_proc) def link_method_call(method_name, value = nil, &url_proc)
regex = method_call_regex(method_name, value)
link_regex(regex, &url_proc)
end
def method_call_regex(method_name, value = nil)
method_name = regexp_for_value(method_name) method_name = regexp_for_value(method_name)
value = regexp_for_value(value) value = regexp_for_value(value)
regex = %r{ %r{
#{method_name} # Method name #{method_name} # Method name
\s* # Whitespace \s* # Whitespace
[(=]? # Opening brace or equals sign [(=]? # Opening brace or equals sign
\s* # Whitespace \s* # Whitespace
['"](?<name>#{value})['"] # Package name in quotes ['"](?<name>#{value})['"] # Package name in quotes
}x }x
link_regex(regex, &url_proc)
end end
end end
end end

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module DependencyLinker
class Package
attr_reader :name, :git_ref, :github_ref
def initialize(name, git_ref, github_ref)
@name = name
@git_ref = git_ref
@github_ref = github_ref
end
def external_ref
@git_ref || @github_ref
end
end
end
end

View file

@ -8,7 +8,6 @@ module Gitlab
private private
def link_dependencies def link_dependencies
link_json('name', json["name"], &method(:package_url))
link_json('license', &method(:license_url)) link_json('license', &method(:license_url))
link_json(%w[homepage url], URL_REGEX, &:itself) link_json(%w[homepage url], URL_REGEX, &:itself)
@ -16,25 +15,19 @@ module Gitlab
end end
def link_packages def link_packages
link_packages_at_key("dependencies", &method(:package_url)) link_packages_at_key("dependencies")
link_packages_at_key("devDependencies", &method(:package_url)) link_packages_at_key("devDependencies")
end end
def link_packages_at_key(key, &url_proc) def link_packages_at_key(key)
dependencies = json[key] dependencies = json[key]
return unless dependencies return unless dependencies
dependencies.each do |name, version| dependencies.each do |name, version|
link_json(name, version, link: :key, &url_proc) external_url = external_url(name, version)
link_json(name) do |value| link_json(name, version, link: :key) { external_url }
case value link_json(name) { external_url }
when /\A#{URL_REGEX}\z/
value
when /\A#{REPO_REGEX}\z/
github_url(value)
end
end
end end
end end

View file

@ -0,0 +1,40 @@
# frozen_string_literal: true
module Gitlab
module DependencyLinker
module Parser
class Gemfile < MethodLinker
GIT_REGEX = Gitlab::DependencyLinker::GemfileLinker::GIT_REGEX
GITHUB_REGEX = Gitlab::DependencyLinker::GemfileLinker::GITHUB_REGEX
def initialize(plain_text)
@plain_text = plain_text
end
# Returns a list of Gitlab::DependencyLinker::Package
#
# keyword - The package definition keyword, e.g. `:gem` for
# Gemfile parsing, `:pod` for Podfile.
def parse(keyword:)
plain_lines.each_with_object([]) do |line, packages|
name = fetch(line, method_call_regex(keyword))
next unless name
git_ref = fetch(line, GIT_REGEX)
github_ref = fetch(line, GITHUB_REGEX)
packages << Gitlab::DependencyLinker::Package.new(name, git_ref, github_ref)
end
end
private
def fetch(line, regex, group: :name)
match = line.match(regex)
match[group] if match
end
end
end
end
end

View file

@ -5,12 +5,21 @@ module Gitlab
class PodfileLinker < GemfileLinker class PodfileLinker < GemfileLinker
include Cocoapods include Cocoapods
self.package_keyword = :pod
self.file_type = :podfile self.file_type = :podfile
private private
def link_packages def link_packages
link_method_call('pod', &method(:package_url)) packages = parse_packages
return unless packages
packages.each do |package|
link_method_call('pod', package.name) do
external_url(package.name, package.external_ref)
end
end
end end
end end
end end

View file

@ -19,7 +19,7 @@ module Gitlab
link_method_call('license', &method(:license_url)) link_method_call('license', &method(:license_url))
link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url)) link_regex(/license\s*=\s*\{\s*(type:|:type\s*=>)\s*#{STRING_REGEX}/, &method(:license_url))
link_method_call(%w[name dependency], &method(:package_url)) link_method_call('dependency', &method(:package_url))
end end
end end
end end

View file

@ -20,6 +20,17 @@ module Gitlab
create_target_branch unless branch_exists?(@merge_request.target_branch) create_target_branch unless branch_exists?(@merge_request.target_branch)
end end
# The merge_request_diff associated with the current @merge_request might
# be invalid. Than means, when the @merge_request object is saved, the
# @merge_request.merge_request_diff won't. This can leave the merge request
# in an invalid state, because a merge request must have an associated
# merge request diff.
# In this change, if the associated merge request diff is invalid, we set
# it to nil. This change, in association with the after callback
# :ensure_merge_request_diff in the MergeRequest class, makes that
# when the merge request is going to be created and it doesn't have
# one, a default one will be generated.
@merge_request.merge_request_diff = nil unless @merge_request.merge_request_diff&.valid?
@merge_request @merge_request
end end

View file

@ -61,7 +61,7 @@ module Gitlab
def log_base_data def log_base_data
{ {
importer: 'Import/Export', importer: 'Import/Export',
import_jid: @project&.import_state&.import_jid, import_jid: @project&.import_state&.jid,
project_id: @project&.id, project_id: @project&.id,
project_path: @project&.full_path project_path: @project&.full_path
} }

View file

@ -82,6 +82,8 @@ module Gitlab
def initialize(api_prefix, **kubeclient_options) def initialize(api_prefix, **kubeclient_options)
@api_prefix = api_prefix @api_prefix = api_prefix
@kubeclient_options = kubeclient_options.merge(http_max_redirects: 0) @kubeclient_options = kubeclient_options.merge(http_max_redirects: 0)
validate_url!
end end
def create_or_update_cluster_role_binding(resource) def create_or_update_cluster_role_binding(resource)
@ -118,6 +120,12 @@ module Gitlab
private private
def validate_url!
return if Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services?
Gitlab::UrlBlocker.validate!(api_prefix, allow_local_network: false)
end
def cluster_role_binding_exists?(resource) def cluster_role_binding_exists?(resource)
get_cluster_role_binding(resource.metadata.name) get_cluster_role_binding(resource.metadata.name)
rescue ::Kubeclient::ResourceNotFoundError rescue ::Kubeclient::ResourceNotFoundError

View file

@ -1293,6 +1293,9 @@ msgstr ""
msgid "Cannot modify managed Kubernetes cluster" msgid "Cannot modify managed Kubernetes cluster"
msgstr "" msgstr ""
msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded."
msgstr ""
msgid "Certificate" msgid "Certificate"
msgstr "" msgstr ""

View file

@ -8,3 +8,4 @@ gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.12' gem 'selenium-webdriver', '~> 3.12'
gem 'airborne', '~> 0.2.13' gem 'airborne', '~> 0.2.13'
gem 'nokogiri', '~> 1.10.1' gem 'nokogiri', '~> 1.10.1'
gem 'rspec-retry', '~> 0.6.1'

View file

@ -76,6 +76,8 @@ GEM
rspec-mocks (3.7.0) rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0) rspec-support (~> 3.7.0)
rspec-retry (0.6.1)
rspec-core (> 3.3)
rspec-support (3.7.0) rspec-support (3.7.0)
rubyzip (1.2.2) rubyzip (1.2.2)
selenium-webdriver (3.141.0) selenium-webdriver (3.141.0)
@ -101,6 +103,7 @@ DEPENDENCIES
pry-byebug (~> 3.5.1) pry-byebug (~> 3.5.1)
rake (~> 12.3.0) rake (~> 12.3.0)
rspec (~> 3.7) rspec (~> 3.7)
rspec-retry (~> 0.6.1)
selenium-webdriver (~> 3.12) selenium-webdriver (~> 3.12)
BUNDLED WITH BUNDLED WITH

View file

@ -1,7 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module QA module QA
context 'Create' do # https://gitlab.com/gitlab-org/quality/staging/issues/40
context 'Create', :quarantine do
describe 'Push mirror a repository over HTTP' do describe 'Push mirror a repository over HTTP' do
it 'configures and syncs a (push) mirrored repository' do it 'configures and syncs a (push) mirrored repository' do
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)

View file

@ -1,4 +1,5 @@
require_relative '../qa' require_relative '../qa'
require 'rspec/retry'
Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
@ -43,6 +44,17 @@ RSpec.configure do |config|
config.profile_examples = 10 config.profile_examples = 10
config.order = :random config.order = :random
Kernel.srand config.seed Kernel.srand config.seed
# show retry status in spec process
config.verbose_retry = true
# show exception that triggers a retry if verbose_retry is set to true
config.display_try_failure_messages = true
config.around do |example|
retry_times = example.metadata.keys.include?(:quarantine) ? 1 : 3
example.run_with_retry retry: retry_times
end
end end
# Skip tests in quarantine unless we explicitly focus on them. # Skip tests in quarantine unless we explicitly focus on them.

View file

@ -28,6 +28,22 @@ describe 'rspec config tests' do
end end
end end
let(:group_2) do
RSpec.describe do
before(:all) do
@expectations = [1, 2, 3]
end
example 'not in quarantine' do
expect(@expectations.shift).to be(3)
end
example 'in quarantine', :quarantine do
expect(@expectations.shift).to be(3)
end
end
end
context 'with no tags focussed' do context 'with no tags focussed' do
before do before do
group.run group.run
@ -301,4 +317,39 @@ describe 'rspec config tests' do
end end
end end
end end
context 'rspec retry' do
context 'in an untagged context' do
before do
group_2.run
end
it 'should run example :retry times' do
examples = group_2.descendant_filtered_examples
ex = examples.find { |e| e.description == 'not in quarantine' }
expect(ex.execution_result.status).to eq(:passed)
end
end
context 'with :quarantine focussed' do
before do
RSpec.configure do |config|
config.inclusion_filter = :quarantine
end
group_2.run
end
after do
RSpec.configure do |config|
config.inclusion_filter.clear
end
end
it 'should run example once only' do
examples = group_2.descendant_filtered_examples
ex = examples.find { |e| e.description == 'in quarantine' }
expect(ex.execution_result.status).to eq(:failed)
end
end
end
end end

View file

@ -13,7 +13,7 @@ describe Dashboard::MilestonesController do
) )
end end
let(:issue) { create(:issue, project: project, milestone: project_milestone) } let(:issue) { create(:issue, project: project, milestone: project_milestone) }
let(:group_issue) { create(:issue, milestone: group_milestone) } let(:group_issue) { create(:issue, milestone: group_milestone, project: create(:project, group: group)) }
let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) } let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) }
let!(:group_label) { create(:group_label, group: group, title: 'Group Issue Label', issues: [group_issue]) } let!(:group_label) { create(:group_label, group: group, title: 'Group Issue Label', issues: [group_issue]) }

View file

@ -6,7 +6,7 @@ describe GoogleApi::AuthorizationsController do
let(:token) { 'token' } let(:token) { 'token' }
let(:expires_at) { 1.hour.since.strftime('%s') } let(:expires_at) { 1.hour.since.strftime('%s') }
subject { get :callback, params: { code: 'xxx', state: @state } } subject { get :callback, params: { code: 'xxx', state: state } }
before do before do
sign_in(user) sign_in(user)
@ -15,6 +15,26 @@ describe GoogleApi::AuthorizationsController do
.to receive(:get_token).and_return([token, expires_at]) .to receive(:get_token).and_return([token, expires_at])
end end
shared_examples_for 'access denied' do
it 'returns a 404' do
subject
expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil
expect(response).to have_http_status(:not_found)
end
end
context 'session key is present' do
let(:session_key) { 'session-key' }
let(:redirect_uri) { 'example.com' }
before do
session[GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(session_key)] = redirect_uri
end
context 'session key matches state param' do
let(:state) { session_key }
it 'sets token and expires_at in session' do it 'sets token and expires_at in session' do
subject subject
@ -24,26 +44,28 @@ describe GoogleApi::AuthorizationsController do
.to eq(expires_at) .to eq(expires_at)
end end
context 'when redirect uri key is stored in state' do
set(:project) { create(:project) }
let(:redirect_uri) { project_clusters_url(project).to_s }
before do
@state = GoogleApi::CloudPlatform::Client
.new_session_key_for_redirect_uri do |key|
session[key] = redirect_uri
end
end
it 'redirects to the URL stored in state param' do it 'redirects to the URL stored in state param' do
expect(subject).to redirect_to(redirect_uri) expect(subject).to redirect_to(redirect_uri)
end end
end end
context 'when redirection url is not stored in state' do context 'session key does not match state param' do
it 'redirects to root_path' do let(:state) { 'bad-key' }
expect(subject).to redirect_to(root_path)
end it_behaves_like 'access denied'
end
context 'state param is blank' do
let(:state) { '' }
it_behaves_like 'access denied'
end
end
context 'state param is present, but session key is blank' do
let(:state) { 'session-key' }
it_behaves_like 'access denied'
end end
end end
end end

View file

@ -6,6 +6,8 @@ describe Groups::SharedProjectsController do
end end
def share_project(project) def share_project(project)
group.add_developer(user)
Projects::GroupLinks::CreateService.new( Projects::GroupLinks::CreateService.new(
project, project,
user, user,

View file

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'spec_helper'
describe Projects::AutocompleteSourcesController do
describe 'GET milestones' do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, namespace: group) }
let!(:project_milestone) { create(:milestone, project: project) }
let!(:group_milestone) { create(:milestone, group: group) }
before do
sign_in(user)
end
it 'lists milestones' do
group.add_owner(user)
get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path }
milestone_titles = json_response.map { |milestone| milestone["title"] }
expect(milestone_titles).to match_array([project_milestone.title, group_milestone.title])
end
context 'when user cannot read project issues and merge requests' do
it 'renders 404' do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path }
expect(response).to have_gitlab_http_status(404)
end
end
end
end

View file

@ -65,8 +65,24 @@ describe Projects::GroupLinksController do
end end
end end
context 'when user does not have access to the public group' do
let(:group) { create(:group, :public) }
include_context 'link project to group'
it 'renders 404' do
expect(response.status).to eq 404
end
it 'does not share project with that group' do
expect(group.shared_projects).not_to include project
end
end
context 'when project group id equal link group id' do context 'when project group id equal link group id' do
before do before do
group2.add_developer(user)
post(:create, params: { post(:create, params: {
namespace_id: project.namespace, namespace_id: project.namespace,
project_id: project, project_id: project,
@ -102,5 +118,26 @@ describe Projects::GroupLinksController do
expect(flash[:alert]).to eq('Please select a group.') expect(flash[:alert]).to eq('Please select a group.')
end end
end end
context 'when link is not persisted in the database' do
before do
allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute)
.and_return({ status: :error, http_status: 409, message: 'error' })
post(:create, params: {
namespace_id: project.namespace,
project_id: project,
link_group_id: group.id,
link_group_access: ProjectGroupLink.default_access
})
end
it 'redirects to project group links page' do
expect(response).to redirect_to(
project_project_members_path(project)
)
expect(flash[:alert]).to eq('error')
end
end
end end
end end

View file

@ -205,6 +205,8 @@ describe SnippetsController do
end end
context 'when the snippet description contains a file' do context 'when the snippet description contains a file' do
include FileMoverHelpers
let(:picture_file) { '/-/system/temp/secret56/picture.jpg' } let(:picture_file) { '/-/system/temp/secret56/picture.jpg' }
let(:text_file) { '/-/system/temp/secret78/text.txt' } let(:text_file) { '/-/system/temp/secret78/text.txt' }
let(:description) do let(:description) do
@ -215,6 +217,8 @@ describe SnippetsController do
before do before do
allow(FileUtils).to receive(:mkdir_p) allow(FileUtils).to receive(:mkdir_p)
allow(FileUtils).to receive(:move) allow(FileUtils).to receive(:move)
stub_file_mover(text_file)
stub_file_mover(picture_file)
end end
subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) } subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) }

View file

@ -233,8 +233,8 @@ describe 'Issues' do
created_at: Time.now - (index * 60)) created_at: Time.now - (index * 60))
end end
end end
let(:newer_due_milestone) { create(:milestone, due_date: '2013-12-11') } let(:newer_due_milestone) { create(:milestone, project: project, due_date: '2013-12-11') }
let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') } let(:later_due_milestone) { create(:milestone, project: project, due_date: '2013-12-12') }
it 'sorts by newest' do it 'sorts by newest' do
visit project_issues_path(project, sort: sort_value_created_date) visit project_issues_path(project, sort: sort_value_created_date)

View file

@ -1,7 +1,11 @@
require 'rails_helper' require 'rails_helper'
describe 'Merge request > User sees versions', :js do describe 'Merge request > User sees versions', :js do
let(:merge_request) { create(:merge_request, importing: true) } let(:merge_request) do
create(:merge_request).tap do |mr|
mr.merge_request_diff.destroy
end
end
let(:project) { merge_request.source_project } let(:project) { merge_request.source_project }
let(:user) { project.creator } let(:user) { project.creator }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }

View file

@ -13,7 +13,7 @@ describe 'Merge requests > User lists merge requests' do
source_project: project, source_project: project,
source_branch: 'fix', source_branch: 'fix',
assignee: user, assignee: user,
milestone: create(:milestone, due_date: '2013-12-11'), milestone: create(:milestone, project: project, due_date: '2013-12-11'),
created_at: 1.minute.ago, created_at: 1.minute.ago,
updated_at: 1.minute.ago) updated_at: 1.minute.ago)
create(:merge_request, create(:merge_request,
@ -21,7 +21,7 @@ describe 'Merge requests > User lists merge requests' do
source_project: project, source_project: project,
source_branch: 'markdown', source_branch: 'markdown',
assignee: user, assignee: user,
milestone: create(:milestone, due_date: '2013-12-12'), milestone: create(:milestone, project: project, due_date: '2013-12-12'),
created_at: 2.minutes.ago, created_at: 2.minutes.ago,
updated_at: 2.minutes.ago) updated_at: 2.minutes.ago)
create(:merge_request, create(:merge_request,

View file

@ -7,6 +7,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
end end
end end
let(:admin) { create(:admin) }
around do |example| around do |example|
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
example.run example.run
@ -16,6 +18,7 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
it 'User sees their active sessions' do it 'User sees their active sessions' do
Capybara::Session.new(:session1) Capybara::Session.new(:session1)
Capybara::Session.new(:session2) Capybara::Session.new(:session2)
Capybara::Session.new(:session3)
# note: headers can only be set on the non-js (aka. rack-test) driver # note: headers can only be set on the non-js (aka. rack-test) driver
using_session :session1 do using_session :session1 do
@ -37,9 +40,27 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
gitlab_sign_in(user) gitlab_sign_in(user)
end end
# set an admin session impersonating the user
using_session :session3 do
Capybara.page.driver.header(
'User-Agent',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'
)
gitlab_sign_in(admin)
visit admin_user_path(user)
click_link 'Impersonate'
end
using_session :session1 do using_session :session1 do
visit profile_active_sessions_path visit profile_active_sessions_path
expect(page).to(
have_selector('ul.list-group li.list-group-item', { text: 'Signed in on',
count: 2 }))
expect(page).to have_content( expect(page).to have_content(
'127.0.0.1 ' \ '127.0.0.1 ' \
'This is your current session ' \ 'This is your current session ' \
@ -57,33 +78,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
) )
expect(page).to have_selector '[title="Smartphone"]', count: 1 expect(page).to have_selector '[title="Smartphone"]', count: 1
end
end
it 'User can revoke a session', :js, :redis_session_store do expect(page).not_to have_content('Chrome on Windows')
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
# set an additional session in another browser
using_session :session2 do
gitlab_sign_in(user)
end
using_session :session1 do
gitlab_sign_in(user)
visit profile_active_sessions_path
expect(page).to have_link('Revoke', count: 1)
accept_confirm { click_on 'Revoke' }
expect(page).not_to have_link('Revoke')
end
using_session :session2 do
visit profile_active_sessions_path
expect(page).to have_content('You need to sign in or sign up before continuing.')
end end
end end
end end

View file

@ -548,10 +548,7 @@ describe 'File blob', :js do
it 'displays an auxiliary viewer' do it 'displays an auxiliary viewer' do
aggregate_failures do aggregate_failures do
# shows names of dependency manager and package # shows names of dependency manager and package
expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.') expect(page).to have_content('This project manages its dependencies using RubyGems.')
# shows a link to the gem
expect(page).to have_link('activerecord', href: 'https://rubygems.org/gems/activerecord')
# shows a learn more link # shows a learn more link
expect(page).to have_link('Learn more', href: 'https://rubygems.org/') expect(page).to have_link('Learn more', href: 'https://rubygems.org/')

View file

@ -27,6 +27,7 @@ describe 'Project > Members > Invite group', :js do
before do before do
project.add_maintainer(maintainer) project.add_maintainer(maintainer)
group_to_share_with.add_guest(maintainer)
sign_in(maintainer) sign_in(maintainer)
end end
@ -112,6 +113,7 @@ describe 'Project > Members > Invite group', :js do
before do before do
project.add_maintainer(maintainer) project.add_maintainer(maintainer)
group.add_guest(maintainer)
sign_in(maintainer) sign_in(maintainer)
visit project_settings_members_path(project) visit project_settings_members_path(project)

View file

@ -10,6 +10,7 @@ describe 'Projects > Settings > User manages group links' do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
group_market.add_guest(user)
sign_in(user) sign_in(user)
share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER) share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)

View file

@ -93,4 +93,29 @@ describe 'Private Group access' do
it { is_expected.to be_denied_for(:visitor) } it { is_expected.to be_denied_for(:visitor) }
it { is_expected.to be_denied_for(:external) } it { is_expected.to be_denied_for(:external) }
end end
describe 'GET /groups/:path for shared projects' do
let(:project) { create(:project, :public) }
before do
Projects::GroupLinks::CreateService.new(
project,
create(:user),
link_group_access: ProjectGroupLink::DEVELOPER
).execute(group)
end
subject { group_path(group) }
it { is_expected.to be_allowed_for(:admin) }
it { is_expected.to be_allowed_for(:owner).of(group) }
it { is_expected.to be_allowed_for(:maintainer).of(group) }
it { is_expected.to be_allowed_for(:developer).of(group) }
it { is_expected.to be_allowed_for(:reporter).of(group) }
it { is_expected.to be_allowed_for(:guest).of(group) }
it { is_expected.to be_denied_for(project_guest) }
it { is_expected.to be_denied_for(:user) }
it { is_expected.to be_denied_for(:external) }
it { is_expected.to be_denied_for(:visitor) }
end
end end

View file

@ -13,6 +13,7 @@ describe MergeRequestsFinder do
end end
end end
context "multiple projects with merge requests" do
let(:user) { create :user } let(:user) { create :user }
let(:user2) { create :user } let(:user2) { create :user }
@ -31,7 +32,7 @@ describe MergeRequestsFinder do
p p
end end
end end
let(:project4) { create_project_without_n_plus_1(group: subgroup) } let(:project4) { create_project_without_n_plus_1(:repository, group: subgroup) }
let(:project5) { create_project_without_n_plus_1(group: subgroup) } let(:project5) { create_project_without_n_plus_1(group: subgroup) }
let(:project6) { create_project_without_n_plus_1(group: subgroup) } let(:project6) { create_project_without_n_plus_1(group: subgroup) }
@ -55,7 +56,7 @@ describe MergeRequestsFinder do
project6.add_developer(user) project6.add_developer(user)
end end
describe "#execute" do describe '#execute' do
it 'filters by scope' do it 'filters by scope' do
params = { scope: 'authored', state: 'opened' } params = { scope: 'authored', state: 'opened' }
merge_requests = described_class.new(user, params).execute merge_requests = described_class.new(user, params).execute
@ -68,6 +69,15 @@ describe MergeRequestsFinder do
expect(merge_requests.size).to eq(2) expect(merge_requests.size).to eq(2)
end end
it 'filters by commit sha' do
merge_requests = described_class.new(
user,
commit_sha: merge_request5.merge_request_diff.last_commit_sha
).execute
expect(merge_requests).to contain_exactly(merge_request5)
end
context 'filtering by group' do context 'filtering by group' do
it 'includes all merge requests when user has access' do it 'includes all merge requests when user has access' do
params = { group_id: group.id } params = { group_id: group.id }
@ -285,3 +295,127 @@ describe MergeRequestsFinder do
end end
end end
end end
context 'when projects require different access levels for merge requests' do
let(:user) { create(:user) }
let(:public_project) { create(:project, :public) }
let(:internal) { create(:project, :internal) }
let(:private_project) { create(:project, :private) }
let(:public_with_private_repo) { create(:project, :public, :repository, :repository_private) }
let(:internal_with_private_repo) { create(:project, :internal, :repository, :repository_private) }
let(:merge_requests) { described_class.new(user, {}).execute }
let!(:mr_public) { create(:merge_request, source_project: public_project) }
let!(:mr_private) { create(:merge_request, source_project: private_project) }
let!(:mr_internal) { create(:merge_request, source_project: internal) }
let!(:mr_private_repo_access) { create(:merge_request, source_project: public_with_private_repo) }
let!(:mr_internal_private_repo_access) { create(:merge_request, source_project: internal_with_private_repo) }
context 'with admin user' do
let(:user) { create(:user, :admin) }
it 'returns all merge requests' do
expect(merge_requests).to eq(
[mr_internal_private_repo_access, mr_private_repo_access, mr_internal, mr_private, mr_public]
)
end
end
context 'when project restricts merge requests' do
let(:non_member) { create(:user) }
let(:project) { create(:project, :repository, :public, :merge_requests_private) }
let!(:merge_request) { create(:merge_request, source_project: project) }
it "returns nothing to to non members" do
merge_requests = described_class.new(
non_member,
project_id: project.id
).execute
expect(merge_requests).to be_empty
end
end
context 'with external user' do
let(:user) { create(:user, :external) }
it 'returns only public merge requests' do
expect(merge_requests).to eq([mr_public])
end
end
context 'with authenticated user' do
it 'returns public and internal merge requests' do
expect(merge_requests).to eq([mr_internal, mr_public])
end
context 'being added to the private project' do
context 'as a guest' do
before do
private_project.add_guest(user)
end
it 'does not return merge requests from the private project' do
expect(merge_requests).to eq([mr_internal, mr_public])
end
end
context 'as a developer' do
before do
private_project.add_developer(user)
end
it 'returns merge requests from the private project' do
expect(merge_requests).to eq([mr_internal, mr_private, mr_public])
end
end
end
context 'being added to the public project with private repo access' do
context 'as a guest' do
before do
public_with_private_repo.add_guest(user)
end
it 'returns merge requests from the project' do
expect(merge_requests).to eq([mr_internal, mr_public])
end
end
context 'as a reporter' do
before do
public_with_private_repo.add_reporter(user)
end
it 'returns merge requests from the project' do
expect(merge_requests).to eq([mr_private_repo_access, mr_internal, mr_public])
end
end
end
context 'being added to the internal project with private repo access' do
context 'as a guest' do
before do
internal_with_private_repo.add_guest(user)
end
it 'returns merge requests from the project' do
expect(merge_requests).to eq([mr_internal, mr_public])
end
end
context 'as a reporter' do
before do
internal_with_private_repo.add_reporter(user)
end
it 'returns merge requests from the project' do
expect(merge_requests).to eq([mr_internal_private_repo_access, mr_internal, mr_public])
end
end
end
end
end
end

View file

@ -16,6 +16,10 @@ describe Constraints::ProjectUrlConstrainer do
let(:request) { build_request('foo', 'bar') } let(:request) { build_request('foo', 'bar') }
it { expect(subject.matches?(request)).to be_falsey } it { expect(subject.matches?(request)).to be_falsey }
context 'existence_check is false' do
it { expect(subject.matches?(request, existence_check: false)).to be_truthy }
end
end end
context "project id ending with .git" do context "project id ending with .git" do

View file

@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end end
it 'links the module name' do it 'does not link the module name' do
expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel')) expect(subject).not_to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel'))
end end
it 'links the homepage' do it 'links the homepage' do

View file

@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do
end end
it 'links dependencies' do it 'links dependencies' do
expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails'))
expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer')) expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer'))
expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders'))
expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets'))
expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for')) expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for'))
end end
it 'links to external dependencies' do
expect(subject).to include(link('rails', 'https://github.com/rails/rails'))
expect(subject).to include(link('responders', 'https://github.com/rails/responders'))
expect(subject).to include(link('sprockets', 'https://gitlab.example.com/gems/sprockets'))
end
it 'links GitHub repos' do it 'links GitHub repos' do
expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails')) expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails'))
expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders')) expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders'))

View file

@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end end
it 'links the gem name' do it 'does not link the gem name' do
expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git')) expect(subject).not_to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git'))
end end
it 'links the license' do it 'links the license' do

View file

@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
"express": "4.2.x", "express": "4.2.x",
"bigpipe": "bigpipe/pagelet", "bigpipe": "bigpipe/pagelet",
"plates": "https://github.com/flatiron/plates/tarball/master", "plates": "https://github.com/flatiron/plates/tarball/master",
"karma": "^1.4.1" "karma": "^1.4.1",
"random": "git+https://EdOverflow@github.com/example/example.git"
}, },
"devDependencies": { "devDependencies": {
"vows": "^0.7.0", "vows": "^0.7.0",
@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end end
it 'links the module name' do it 'does not link the module name' do
expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name')) expect(subject).not_to include(link('module-name', 'https://npmjs.com/package/module-name'))
end end
it 'links the homepage' do it 'links the homepage' do
@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
expect(subject).to include(link('primus', 'https://npmjs.com/package/primus')) expect(subject).to include(link('primus', 'https://npmjs.com/package/primus'))
expect(subject).to include(link('async', 'https://npmjs.com/package/async')) expect(subject).to include(link('async', 'https://npmjs.com/package/async'))
expect(subject).to include(link('express', 'https://npmjs.com/package/express')) expect(subject).to include(link('express', 'https://npmjs.com/package/express'))
expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe'))
expect(subject).to include(link('plates', 'https://npmjs.com/package/plates'))
expect(subject).to include(link('karma', 'https://npmjs.com/package/karma')) expect(subject).to include(link('karma', 'https://npmjs.com/package/karma'))
expect(subject).to include(link('vows', 'https://npmjs.com/package/vows')) expect(subject).to include(link('vows', 'https://npmjs.com/package/vows'))
expect(subject).to include(link('assume', 'https://npmjs.com/package/assume')) expect(subject).to include(link('assume', 'https://npmjs.com/package/assume'))
expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit')) expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit'))
end end
it 'links dependencies to URL detected on value' do
expect(subject).to include(link('bigpipe', 'https://github.com/bigpipe/pagelet'))
expect(subject).to include(link('plates', 'https://github.com/flatiron/plates/tarball/master'))
end
it 'does not link to NPM when invalid git URL' do
expect(subject).not_to include(link('random', 'https://npmjs.com/package/random'))
end
it 'links GitHub repos' do it 'links GitHub repos' do
expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet')) expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet'))
end end

View file

@ -0,0 +1,42 @@
require 'rails_helper'
describe Gitlab::DependencyLinker::Parser::Gemfile do
describe '#parse' do
let(:file_content) do
<<-CONTENT.strip_heredoc
source 'https://rubygems.org'
gem "rails", '4.2.6', github: "rails/rails"
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'responders', '~> 2.0', :github => 'rails/responders'
gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets'
gem 'default_value_for', '~> 3.0.0'
CONTENT
end
subject { described_class.new(file_content).parse(keyword: 'gem') }
def fetch_package(name)
subject.find { |package| package.name == name }
end
it 'returns parsed packages' do
expect(subject.size).to eq(5)
expect(subject).to all(be_a(Gitlab::DependencyLinker::Package))
end
it 'packages respond to name and external_ref accordingly' do
expect(fetch_package('rails')).to have_attributes(name: 'rails',
github_ref: 'rails/rails',
git_ref: nil)
expect(fetch_package('sprockets')).to have_attributes(name: 'sprockets',
github_ref: nil,
git_ref: 'https://gitlab.example.com/gems/sprockets')
expect(fetch_package('default_value_for')).to have_attributes(name: 'default_value_for',
github_ref: nil,
git_ref: nil)
end
end
end

View file

@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do
it 'links packages' do it 'links packages' do
expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking')) expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking'))
expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar')) end
it 'links external packages' do
expect(subject).to include(link('Interstellar/Core', 'https://github.com/ashfurrow/Interstellar.git'))
end end
it 'links Git repos' do it 'links Git repos' do

View file

@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>} %{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end end
it 'links the gem name' do it 'does not link the pod name' do
expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability')) expect(subject).not_to include(link('Reachability', 'https://cocoapods.org/pods/Reachability'))
end end
it 'links the license' do it 'links the license' do

View file

@ -41,4 +41,20 @@ describe Gitlab::ImportExport::MergeRequestParser do
expect(parsed_merge_request).to eq(merge_request) expect(parsed_merge_request).to eq(merge_request)
end end
context 'when the merge request has diffs' do
let(:merge_request) do
build(:merge_request, source_project: forked_project, target_project: project)
end
context 'when the diff is invalid' do
let(:merge_request_diff) { build(:merge_request_diff, merge_request: merge_request, base_commit_sha: 'foobar') }
it 'sets the diff to nil' do
expect(merge_request_diff).to be_invalid
expect(merge_request_diff.merge_request).to eq merge_request
expect(parsed_merge_request.merge_request_diff).to be_nil
end
end
end
end end

View file

@ -14,6 +14,16 @@ describe Gitlab::ImportExport::Shared do
expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]']) expect(subject.errors).to eq(['Error importing into [FILTERED] Permission denied @ unlink_internal - [FILTERED]'])
end end
it 'updates the import JID' do
import_state = create(:import_state, project: project, jid: 'jid-test')
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger).to receive(:error).with(hash_including(import_jid: import_state.jid))
end
subject.error(error)
end
it 'calls the error logger with the full message' do it 'calls the error logger with the full message' do
expect(subject).to receive(:log_error).with(hash_including(message: error.message)) expect(subject).to receive(:log_error).with(hash_including(message: error.message))

View file

@ -50,6 +50,36 @@ describe Gitlab::Kubernetes::KubeClient do
end end
end end
describe '#initialize' do
shared_examples 'local address' do
it 'blocks local addresses' do
expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
end
context 'when local requests are allowed' do
before do
stub_application_setting(allow_local_requests_from_hooks_and_services: true)
end
it 'allows local addresses' do
expect { client }.not_to raise_error
end
end
end
context 'localhost address' do
let(:api_url) { 'http://localhost:22' }
it_behaves_like 'local address'
end
context 'private network address' do
let(:api_url) { 'http://192.168.1.2:3003' }
it_behaves_like 'local address'
end
end
describe '#core_client' do describe '#core_client' do
subject { client.core_client } subject { client.core_client }

View file

@ -194,6 +194,11 @@ describe Notify do
let(:new_issue) { create(:issue) } let(:new_issue) { create(:issue) }
subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) } subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
context 'when a user has permissions to access the new issue' do
before do
new_issue.project.add_developer(recipient)
end
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue } let(:model) { issue }
end end
@ -213,6 +218,31 @@ describe Notify do
is_expected.to have_body_text(project_issue_path(project, issue)) is_expected.to have_body_text(project_issue_path(project, issue))
end end
end end
it 'contains the issue title' do
is_expected.to have_body_text new_issue.title
end
end
context 'when a user does not permissions to access the new issue' do
it 'has the correct subject and body' do
new_issue_url = project_issue_path(new_issue.project, new_issue)
aggregate_failures do
is_expected.to have_referable_subject(issue, reply: true)
is_expected.not_to have_body_text(new_issue_url)
is_expected.to have_body_text(project_issue_path(project, issue))
end
end
it 'does not contain the issue title' do
is_expected.not_to have_body_text new_issue.title
end
it 'contains information about missing permissions' do
is_expected.to have_body_text "You don't have access to the project."
end
end
end end
end end

View file

@ -7,7 +7,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end end
end end
let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') } let(:session) do
double(:session, { id: '6919a6f1bb119dd7396fadc38fd18d0d',
'[]': {} })
end
let(:request) do let(:request) do
double(:request, { double(:request, {

View file

@ -98,6 +98,22 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { expect(kubernetes.save).to be_truthy } it { expect(kubernetes.save).to be_truthy }
end end
context 'when api_url is localhost' do
let(:api_url) { 'http://localhost:22' }
it { expect(kubernetes.save).to be_falsey }
context 'Application settings allows local requests' do
before do
allow(ApplicationSetting)
.to receive(:current)
.and_return(ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: true))
end
it { expect(kubernetes.save).to be_truthy }
end
end
end end
context 'when validates token' do context 'when validates token' do

View file

@ -32,6 +32,7 @@ describe Issuable do
end end
describe "Validation" do describe "Validation" do
context 'general validations' do
subject { build(:issue) } subject { build(:issue) }
before do before do
@ -45,6 +46,44 @@ describe Issuable do
it { is_expected.to validate_length_of(:title).is_at_most(255) } it { is_expected.to validate_length_of(:title).is_at_most(255) }
end end
describe 'milestone' do
let(:project) { create(:project) }
let(:milestone_id) { create(:milestone, project: project).id }
let(:params) do
{
title: 'something',
project: project,
author: build(:user),
milestone_id: milestone_id
}
end
subject { issuable_class.new(params) }
context 'with correct params' do
it { is_expected.to be_valid }
end
context 'with empty string milestone' do
let(:milestone_id) { '' }
it { is_expected.to be_valid }
end
context 'with nil milestone id' do
let(:milestone_id) { nil }
it { is_expected.to be_valid }
end
context 'with a milestone id from another project' do
let(:milestone_id) { create(:milestone).id }
it { is_expected.to be_invalid }
end
end
end
describe "Scope" do describe "Scope" do
subject { build(:issue) } subject { build(:issue) }
@ -66,6 +105,48 @@ describe Issuable do
end end
end end
describe '#milestone_available?' do
let(:group) { create(:group) }
let(:project) { create(:project, group: group) }
let(:issue) { create(:issue, project: project) }
def build_issuable(milestone_id)
issuable_class.new(project: project, milestone_id: milestone_id)
end
it 'returns true with a milestone from the issue project' do
milestone = create(:milestone, project: project)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the issue project group' do
milestone = create(:milestone, group: group)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns true with a milestone from the the parent of the issue project group', :nested_groups do
parent = create(:group)
group.update(parent: parent)
milestone = create(:milestone, group: parent)
expect(build_issuable(milestone.id).milestone_available?).to be_truthy
end
it 'returns false with a milestone from another project' do
milestone = create(:milestone)
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
end
it 'returns false with a milestone from another group' do
milestone = create(:milestone, group: create(:group))
expect(build_issuable(milestone.id).milestone_available?).to be_falsey
end
end
describe ".search" do describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") } let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
let!(:searchable_issue2) { create(:issue, title: 'Aw') } let!(:searchable_issue2) { create(:issue, title: 'Aw') }

Some files were not shown because too many files have changed in this diff Show more