Commit e646439e authored by Ruben Davila's avatar Ruben Davila

Merge commit 'dev/8-12-stable' into 8-12-stable

parents 467e1ca4 ba8aeb7c
Please view this file on the master branch, on stable branches it's out of date.
v 8.12.4 (unreleased)
v 8.12.5 (unreleased)
v 8.12.4
- Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
- Fix padding in build sidebar. !6506
- Changed compare dropdowns to dropdowns with isolated search input. !6550
- Fix race condition on LFS Token. !6592
- Fix type mismatch bug when closing Jira issue. !6619
- Fix lint-doc error. !6623
- Skip wiki creation when GitHub project has wiki enabled. !6665
- Fix issues importing services via Import/Export. !6667
- Restrict failed login attempts for users with 2FA enabled. !6668
- Fix failed project deletion when feature visibility set to private. !6688
- Prevent claiming associated model IDs via import.
- Set GitLab project exported file permissions to owner only
v 8.12.3
- Update Gitlab Shell to support low IO priority for storage moves
......@@ -8,6 +22,7 @@ v 8.12.3
v 8.12.2
- Fix Import/Export not recognising correctly the imported services.
- Fix snippets pagination
- Fix "Create project" button layout when visibility options are restricted
- Fix List-Unsubscribe header in emails
- Fix IssuesController#show degradation including project on loaded notes
- Fix an issue with the "Commits" section of the cycle analytics summary. !6513
......
......@@ -23,8 +23,9 @@
selectable: true,
filterable: true,
filterByText: true,
fieldName: $dropdown.attr('name'),
filterInput: 'input[type="text"]',
toggleLabel: true,
fieldName: $dropdown.data('field-name'),
filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
......
......@@ -26,15 +26,15 @@
};
showTooltip = function(target, title) {
return $(target).tooltip({
container: 'body',
html: 'true',
placement: 'auto bottom',
title: title,
trigger: 'manual'
}).tooltip('show').one('mouseleave', function() {
return $(this).tooltip('hide');
});
var $target = $(target);
var originalTitle = $target.data('original-title');
$target
.attr('title', 'Copied!')
.tooltip('fixTitle')
.tooltip('show')
.attr('title', originalTitle)
.tooltip('fixTitle');
};
$(function() {
......
......@@ -107,10 +107,14 @@
.block {
width: 100%;
}
.block-first {
padding: 5px 16px 11px;
&.coverage {
padding: 0 16px 11px;
}
.btn-group-justified {
margin-top: 5px;
}
}
.js-build-variable {
......@@ -214,6 +218,9 @@
.build-detail-row {
margin-bottom: 5px;
&:last-of-type {
margin-bottom: 0;
}
}
.build-light-text {
......
......@@ -743,6 +743,62 @@ pre.light-well {
.dropdown-menu {
width: 300px;
}
&.from .compare-dropdown-toggle {
width: 237px;
}
&.to .compare-dropdown-toggle {
width: 254px;
}
.dropdown-toggle-text {
display: block;
height: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
.compare-ellipsis {
display: inline;
}
@media (max-width: $screen-xs-max) {
.compare-form-group {
.input-group {
width: 100%;
& > .compare-dropdown-toggle {
width: 100%;
}
}
.dropdown-menu {
width: 100%;
}
}
.compare-switch-container {
text-align: center;
padding: 0 0 $gl-padding;
.commits-compare-switch {
float: none;
}
}
.compare-ellipsis {
display: block;
text-align: center;
padding: 0 0 $gl-padding;
}
.commits-compare-btn {
width: 100%;
}
}
.clearable-input {
......@@ -779,4 +835,4 @@ pre.light-well {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
\ No newline at end of file
}
......@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
return locked_user_redirect(user) if user.access_locked?
session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
def locked_user_redirect(user)
flash.now[:alert] = 'Invalid Login or password'
render 'devise/sessions/new'
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:otp_attempt].present? && session[:otp_user_id]
if user.access_locked?
locked_user_redirect(user)
elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
......@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.'
render :two_factor
prompt_for_two_factor(user)
end
end
......@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
......
......@@ -507,9 +507,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author)
return if project.has_external_issue_tracker?
transaction do
self.merge_requests_closing_issues.delete_all
closes_issues(current_user).each do |issue|
self.merge_requests_closing_issues.create!(issue: issue)
end
......
......@@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds)
belongs_to :project
# Default scopes force us to unscope here since a service may need to check
# permissions for a project in pending_delete
# http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
belongs_to :project, -> { unscope(where: :pending_delete) }
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false
......
......@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end
def #{arg}=(value)
self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value
end
......
......@@ -827,6 +827,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true)
end
# This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
# flow means we don't call that automatically (and can't conveniently do so).
#
# See:
# <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
#
def increment_failed_attempts!
self.failed_attempts ||= 0
self.failed_attempts += 1
if attempts_exceeded?
lock_access! unless access_locked?
else
save(validate: false)
end
end
private
def projects_union(min_access_level = nil)
......
......@@ -7,6 +7,8 @@ module Projects
def execute
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
@skip_wiki = params.delete(:skip_wiki)
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
......@@ -92,7 +94,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
@project.create_wiki if @project.feature_available?(:wiki, current_user)
@project.create_wiki unless skip_wiki?
@project.build_missing_services
@project.create_labels
......@@ -106,6 +108,10 @@ module Projects
end
end
def skip_wiki?
!@project.feature_available?(:wiki, current_user) || @skip_wiki
end
def save_project_and_import_data(import_data)
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
......
......@@ -8,7 +8,7 @@
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right')
- if @build.coverage
.block.block-first
.block.coverage
.title
Test coverage
%p.build-detail-row
......@@ -95,7 +95,7 @@
- @build.trigger_request.variables.each do |key, value|
.hide.js-build
.js-build-variable= key
.js-build-variable= key
.js-build-value= value
.block
......
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
.clearfix
- if params[:to] && params[:from]
= link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
.form-group.dropdown.compare-form-group.js-compare-from-dropdown
.compare-switch-container
= link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
.form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group
%span.input-group-addon from
= text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence }
= hidden_field_tag :from, params[:from]
= button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
.dropdown-toggle-text= params[:from] || 'Select branch/tag'
= render "ref_dropdown"
= "..."
.form-group.dropdown.compare-form-group.js-compare-to-dropdown
.compare-ellipsis ...
.form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon to
= text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence }
= hidden_field_tag :to, params[:to]
= button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
.dropdown-toggle-text= params[:to] || 'Select branch/tag'
= render "ref_dropdown"
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
......
.dropdown-menu.dropdown-menu-selectable
= dropdown_title "Select branch/tag"
= dropdown_filter "Filter by branch/tag"
= dropdown_content
= dropdown_loading
......@@ -165,10 +165,9 @@ module Gitlab
end
def import_wiki
unless project.wiki_enabled?
unless project.wiki.repository_exists?
wiki = WikiFormatter.new(project)
gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
end
rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created,
......
module Gitlab
module GithubImport
class ProjectCreator
attr_reader :repo, :namespace, :current_user, :session_data
attr_reader :repo, :name, :namespace, :current_user, :session_data
def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
......@@ -12,24 +12,37 @@ module Gitlab
end
def execute
project = ::Projects::CreateService.new(
::Projects::CreateService.new(
current_user,
name: @name,
path: @name,
name: name,
path: name,
description: repo.description,
namespace_id: namespace.id,
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
visibility_level: visibility_level,
import_type: "github",
import_source: repo.full_name,
import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
import_url: import_url,
skip_wiki: skip_wiki
).execute
end
private
# If repo has wiki we'll import it later
if repo.has_wiki? && project
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
end
def import_url
repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@")
end
def visibility_level
repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
end
project
#
# If the GitHub project repository has wiki, we should not create the
# default wiki. Otherwise the GitHub importer will fail because the wiki
# repository already exist.
#
def skip_wiki
repo.has_wiki?
end
end
end
......
module Gitlab
module ImportExport
class AttributeCleaner
ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES
def self.clean!(relation_hash:)
relation_hash.reject! do |key, _value|
key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
end
end
end
end
end
module Gitlab
module ImportExport
module CommandLineUtil
DEFAULT_MODE = 0700
def tar_czf(archive:, dir:)
tar_with_options(archive: archive, dir: dir, options: 'czf')
end
......@@ -21,6 +23,11 @@ module Gitlab
execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
end
def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path)
end
private
def tar_with_options(archive:, dir:, options:)
......@@ -45,7 +52,7 @@ module Gitlab
# if we are copying files, create the destination folder
destination_folder = File.file?(source) ? File.dirname(destination) : destination
FileUtils.mkdir_p(destination_folder)
mkdir_p(destination_folder)
FileUtils.copy_entry(source, destination)
true
end
......
......@@ -15,7 +15,7 @@ module Gitlab
end
def import
FileUtils.mkdir_p(@shared.export_path)
mkdir_p(@shared.export_path)
wait_for_archived_file do
decompress_archive
......
......@@ -110,9 +110,10 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
relation_hash: relation_hash.merge('project_id' => restored_project.id),
relation_hash: relation_hash,
members_mapper: members_mapper,
user: @user)
user: @user,
project_id: restored_project.id)
end
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
......
module Gitlab
module ImportExport
class ProjectTreeSaver
include Gitlab::ImportExport::CommandLineUtil
attr_reader :full_path
def initialize(project:, shared:)
......@@ -10,7 +12,7 @@ module Gitlab
end
def save
FileUtils.mkdir_p(@shared.export_path)
mkdir_p(@shared.export_path)
File.write(full_path, project_json_tree)
true
......
......@@ -13,6 +13,8 @@ module Gitlab
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
BUILD_MODELS = %w[Ci::Build commit_status].freeze
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
......@@ -25,9 +27,9 @@ module Gitlab
new(*args).create
end
def initialize(relation_sym:, relation_hash:, members_mapper:, user:)
def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
@relation_hash = relation_hash.except('id', 'noteable_id')
@relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id)
@members_mapper = members_mapper
@user = user
@imported_object_retries = 0
......@@ -153,7 +155,11 @@ module Gitlab
end
def parsed_relation_hash
@parsed_relation_hash ||= @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
@parsed_relation_hash ||= begin
Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash)
@relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
end
end
def set_st_diffs
......
......@@ -12,7 +12,7 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
FileUtils.mkdir_p(path_to_repo)
mkdir_p(path_to_repo)
git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
rescue => e
......
......@@ -20,7 +20,7 @@ module Gitlab
private
def bundle_to_disk
FileUtils.mkdir_p(@shared.export_path)
mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
rescue => e
@shared.error(e)
......
module Gitlab
module ImportExport
class VersionSaver
include Gitlab::ImportExport::CommandLineUtil
def initialize(shared:)
@shared = shared
end
def save
FileUtils.mkdir_p(@shared.export_path)
mkdir_p(@shared.export_path)
File.write(version_file, Gitlab::ImportExport.version, mode: 'w')
rescue => e
......
......@@ -9,7 +9,7 @@ module Gitlab
end
def bundle_to_disk(full_path)
FileUtils.mkdir_p(@shared.export_path)
mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: full_path)
rescue => e
@shared.error(e)
......
......@@ -20,13 +20,8 @@ module Gitlab
def token
Gitlab::Redis.with do |redis|
token = redis.get(redis_key)
if token
redis.expire(redis_key, EXPIRY_TIME)
else
token = Devise.friendly_token(TOKEN_LENGTH)
redis.set(redis_key, token, ex: EXPIRY_TIME)
end
token ||= Devise.friendly_token(TOKEN_LENGTH)
redis.set(redis_key, token, ex: EXPIRY_TIME)
token
end
......
......@@ -109,6 +109,44 @@ describe SessionsController do
end
end
context 'when the user is on their last attempt' do
before do
user.update(failed_attempts: User.maximum_attempts.pred)
end
context 'when OTP is valid' do
it 'authenticates correctly' do
authenticate_2fa(otp_attempt: user.current_otp)
expect(subject.current_user).to eq user
end
end
context 'when OTP is invalid' do
before { authenticate_2fa(otp_attempt: 'invalid') }
it 'does not authenticate' do
expect(subject.current_user).not_to eq user
end
it 'warns about invalid login' do
expect(response).to set_flash.now[:alert]
.to /Invalid Login or password/
end
it 'locks the user' do
expect(user.reload).to be_access_locked
end
it 'keeps the user locked on future login attempts' do
post(:create, user: { login: user.username, password: user.password })
expect(response)
.to set_flash.now[:alert].to /Invalid Login or password/
end
end
end
context 'when another user does not have 2FA enabled' do
let(:another_user) { create(:user) }
......
......@@ -12,15 +12,16 @@ describe "Compare", js: true do
describe "branches" do
it "pre-populates fields" do
expect(page.find_field("from").value).to eq("master")
expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master")
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
end
it "compares branches" do
fill_in "from", with: "fea"
find("#from").click
select_using_dropdown "from", "feature"
expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature")
click_link "feature"
expect(page.find_field("from").value).to eq("feature")
select_using_dropdown "to", "binary-encoding"
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding")
click_button "Compare"
expect(page).to have_content "Commits"
......@@ -29,14 +30,21 @@ describe "Compare", js: true do
describe "tags" do
it "compares tags" do
fill_in "from", with: "v1.0"
find("#from").click
select_using_dropdown "from", "v1.0.0"
expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0")
click_link "v1.0.0"
expect(page.find_field("from").value).to eq("v1.0.0")
select_using_dropdown "to", "v1.1.0"
expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0")
click_button "Compare"
expect(page).to have_content "Commits"
end
end
def select_using_dropdown(dropdown_type, selection)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
dropdown.fill_in("Filter by branch/tag", with: selection)
click_link selection
end
end
......@@ -47,6 +47,8 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
expect(page).to have_content('Download export')
expect(file_permissions(project.export_path)).to eq(0700)
in_directory_with_expanded_export(project) do |exit_status, tmpdir|
expect(exit_status).to eq(0)
......
......@@ -33,7 +33,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
end
context 'when Github project is private' do
context 'when GitHub project is private' do
it 'sets project visibility to private' do
repo.private = true
......@@ -43,7 +43,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
end
end
context 'when Github project is public' do
context 'when GitHub project is public' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
end
......@@ -56,5 +56,25 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
context 'when GitHub project has wiki' do
it 'does not create the wiki repository' do
allow(repo).to receive(:has_wiki?).and_return(true)
project = service.execute
expect(project.wiki.repository_exists?).to eq false
end
end
context 'when GitHub project does not have wiki' do
it 'creates the wiki repository' do
allow(repo).to receive(:has_wiki?).and_return(false)
project = service.execute
expect(project.wiki.repository_exists?).to eq true
end
end
end
end
require 'spec_helper'
describe Gitlab::ImportExport::AttributeCleaner, lib: true do
let(:unsafe_hash) do
{
'service_id' => 99,
'moved_to_id' => 99,
'namespace_id' => 99,
'ci_id' => 99,
'random_project_id' => 99,
'random_id' => 99,
'milestone_id' => 99,
'project_id' => 99,
'user_id' => 99,
'random_id_in_the_middle' => 99,
'notid' => 99
}
end
let(:post_safe_hash) do
{
'project_id' => 99,
'user_id' => 99,
'random_id_in_the_middle' => 99,
'notid' => 99
}
end
it 'removes unwanted attributes from the hash' do
described_class.clean!(relation_hash: unsafe_hash)
expect(unsafe_hash).to eq(post_safe_hash)
end
end
require 'spec_helper'
describe Gitlab::ImportExport::RelationFactory, lib: true do
let(:project) { create(:empty_project) }
let(:members_mapper) { double('members_mapper').as_null_object }
let(:user) { create(:user) }
let(:created_object) do
described_class.create(relation_sym: relation_sym,
relation_hash: relation_hash,
members_mapper: members_mapper,
user: user,
project_id: project.id)
end
context 'hook object' do
let(:relation_sym) { :hooks }
let(:id) { 999 }
let(:service_id) { 99 }
let(:original_project_id) { 8 }
let(:token) { 'secret' }
let(:relation_hash) do
{
'id' => id,
'url' => 'https://example.json',
'project_id' => original_project_id,
'created_at' => '2016-08-12T09:41:03.462Z',
'updated_at' => '2016-08-12T09:41:03.462Z',
'service_id' => service_id,
'push_events' => true,
'issues_events' => false,
'merge_requests_events' => true,
'tag_push_events' => false,
'note_events' => true,
'enable_ssl_verification' => true,
'build_events' => false,
'wiki_page_events' => true,
'token' => token
}
end
it 'does not have the original ID' do
expect(created_object.id).not_to eq(id)
end
it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id)
end
it 'does not have the original project_id' do
expect(created_object.project_id).not_to eq(original_project_id)
end
it 'has the new project_id' do
expect(created_object.project_id).to eq(project.id)
end
it 'has a token' do
expect(created_object.token).to eq(token)
end
context 'original service exists' do
let(:service_id) { Service.create(project: project).id }
it 'does not have the original service_id' do
expect(created_object.service_id).not_to eq(service_id)
end
end
end
# Mocks an ActiveRecordish object with the dodgy columns
class FooModel
include ActiveModel::Model
def initialize(params)
params.each { |key, value| send("#{key}=", value) }
end
def values
instance_variables.map { |ivar| instance_variable_get(ivar) }
end
end
# `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already
# re-assigned by described_class.
context 'Potentially hazardous foreign keys' do
let(:relation_sym) { :hazardous_foo_model }
let(:relation_hash) do
{
'service_id' => 99,
'moved_to_id' => 99,
'namespace_id' => 99,
'ci_id' => 99,
'random_project_id' => 99,
'random_id' => 99,
'milestone_id' => 99,
'project_id' => 99,
'user_id' => 99,
}
end
class HazardousFooModel < FooModel
attr_accessor :service_id, :moved_to_id, :namespace_id, :ci_id, :random_project_id, :random_id, :milestone_id, :project_id
end
it 'does not preserve any foreign key IDs' do
expect(created_object.values).not_to include(99)
end
end
context 'Project references' do
let(:relation_sym) { :project_foo_model }
let(:relation_hash) do
Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
end
class ProjectFooModel < FooModel
attr_accessor(*Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES)
end
it 'does not preserve any project foreign key IDs' do
expect(created_object.values).not_to include(99)
end
end
end
......@@ -86,6 +86,30 @@ describe MergeRequest, models: true do
end
end
describe '#cache_merge_request_closes_issues!' do
before do
subject.project.team << [subject.author, :developer]
subject.target_branch = subject.project.default_branch
end
it 'caches closed issues' do
issue = create :issue, project: subject.project
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
it 'does not cache issues from external trackers' do
subject.project.update_attribute(:has_external_issue_tracker, true)
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
end
end
describe '#source_branch_sha' do
let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
......
......@@ -203,6 +203,23 @@ describe Service, models: true do
end
end
describe 'initialize service with no properties' do
let(:service) do
GitlabIssueTrackerService.create(
project: create(:project),
title: 'random title'
)
end
it 'does not raise error' do
expect { service }.not_to raise_error
end
it 'creates the properties' do
expect(service.properties).to eq({ "title" => "random title" })
end
end
describe "callbacks" do
let(:project) { create(:project) }
let!(:service) do
......
......@@ -38,6 +38,42 @@ describe MergeRequests::MergeService, services: true do
end
end
context 'closes related issues' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
before do
allow(project).to receive(:default_branch).and_return(merge_request.target_branch)
end
it 'closes GitLab issue tracker issues' do
issue = create :issue, project: project
commit = double('commit', safe_message: "Fixes #{issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
service.execute(merge_request)
expect(issue.reload.closed?).to be_truthy
end
context 'with JIRA integration' do
include JiraServiceHelper
let(:jira_tracker) { project.create_jira_service }
before { jira_service_settings }
it 'closes issues on JIRA issue tracker' do
jira_issue = ExternalIssue.new('JIRA-123', project)
commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
allow(merge_request).to receive(:commits).and_return([commit])
expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
service.execute(merge_request)
end
end
end
context 'remove source branch by author' do
let(:service) do
merge_request.merge_params['force_remove_source_branch'] = '1'
......
......@@ -5,6 +5,7 @@ describe Projects::DestroyService, services: true do
let!(:project) { create(:project, namespace: user.namespace) }
let!(:path) { project.repository.path_to_repo }
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
let!(:async) { false } # execute or async_execute
context 'Sidekiq inline' do
before do
......@@ -28,6 +29,22 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy }
end
context 'async delete of project with private issue visibility' do
let!(:async) { true }
before do
project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
# Run sidekiq immediately to check that renamed repository will be removed
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
it 'deletes the project' do
expect(Project.all).not_to include(project)
expect(Dir.exist?(path)).to be_falsey
expect(Dir.exist?(remove_path)).to be_falsey
end
end
context 'container registry' do
before do
stub_container_registry_config(enabled: true)
......@@ -52,6 +69,10 @@ describe Projects::DestroyService, services: true do
end
def destroy_project(project, user, params)
Projects::DestroyService.new(project, user, params).execute
if async
Projects::DestroyService.new(project, user, params).async_execute
else
Projects::DestroyService.new(project, user, params).execute
end
end
end
......@@ -130,4 +130,8 @@ module ExportFileHelper
(parsed_model_attributes - parent.keys - excluded_attributes).empty?
end
def file_permissions(file)
File.stat(file).mode & 0777
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment