Backports every CE related change from ee-5484 to CE

This commit is contained in:
Tiago Botelho 2018-05-03 13:55:14 +01:00
parent 2d84de9ec9
commit 9a13059332
33 changed files with 1645 additions and 3 deletions

View file

@ -0,0 +1,62 @@
class Projects::MirrorsController < Projects::ApplicationController
include RepositorySettingsRedirect
# Authorize
before_action :authorize_admin_mirror!
before_action :remote_mirror, only: [:update]
layout "project_settings"
def show
redirect_to_repository_settings(project)
end
def update
if project.update_attributes(mirror_params)
flash[:notice] = 'Mirroring settings were successfully updated.'
else
flash[:alert] = project.errors.full_messages.join(', ').html_safe
end
respond_to do |format|
format.html { redirect_to_repository_settings(project) }
format.json do
if project.errors.present?
render json: project.errors, status: :unprocessable_entity
else
render json: ProjectMirrorSerializer.new.represent(project)
end
end
end
end
def update_now
if params[:sync_remote]
project.update_remote_mirrors
flash[:notice] = "The remote repository is being updated..."
end
redirect_to_repository_settings(project)
end
private
def remote_mirror
@remote_mirror = project.remote_mirrors.first_or_initialize
end
def mirror_params_attributes
[
remote_mirrors_attributes: %i[
url
id
enabled
only_protected_branches
]
]
end
def mirror_params
params.require(:project).permit(mirror_params_attributes)
end
end

View file

@ -2,6 +2,7 @@ module Projects
module Settings
class RepositoryController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :remote_mirror, only: [:show]
def show
render_show
@ -25,6 +26,7 @@ module Projects
define_deploy_token
define_protected_refs
remote_mirror
render 'show'
end
@ -41,6 +43,12 @@ module Projects
load_gon_index
end
def remote_mirror
return unless project.feature_available?(:repository_mirrors)
@remote_mirror = project.remote_mirrors.first_or_initialize
end
def access_levels_options
{
create_access_levels: levels_for_dropdown,

View file

@ -64,6 +64,9 @@ class Project < ActiveRecord::Base
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
add_authentication_token_field :runners_token
before_validation :mark_remote_mirrors_for_removal
before_save :ensure_runners_token
after_save :update_project_statistics, if: :namespace_id_changed?
@ -241,11 +244,17 @@ class Project < ActiveRecord::Base
has_many :project_badges, class_name: 'ProjectBadge'
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops, update_only: true
accepts_nested_attributes_for :remote_mirrors,
allow_destroy: true,
reject_if: ->(attrs) { attrs[:id].blank? && attrs[:url].blank? }
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, :add_users, to: :team
@ -335,6 +344,7 @@ class Project < ActiveRecord::Base
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
scope :with_remote_mirrors, -> { joins(:remote_mirrors).where(remote_mirrors: { enabled: true }).distinct }
scope :with_group_runners_enabled, -> do
joins(:ci_cd_settings)
@ -754,6 +764,37 @@ class Project < ActiveRecord::Base
import_type == 'gitea'
end
def has_remote_mirror?
remote_mirror_available? && remote_mirrors.enabled.exists?
end
def updating_remote_mirror?
remote_mirrors.enabled.started.exists?
end
def update_remote_mirrors
return unless remote_mirror_available?
remote_mirrors.enabled.each(&:sync)
end
def mark_stuck_remote_mirrors_as_failed!
remote_mirrors.stuck.update_all(
update_status: :failed,
last_error: 'The remote mirror took to long to complete.',
last_update_at: Time.now
)
end
def mark_remote_mirrors_for_removal
remote_mirrors.each(&:mark_for_delete_if_blank_url)
end
def remote_mirror_available?
remote_mirror_available_overridden ||
::Gitlab::CurrentSettings.mirror_available
end
def check_limit
unless creator.can_create_project? || namespace.kind == 'group'
projects_limit = creator.projects_limit

216
app/models/remote_mirror.rb Normal file
View file

@ -0,0 +1,216 @@
class RemoteMirror < ActiveRecord::Base
include AfterCommitQueue
PROTECTED_BACKOFF_DELAY = 1.minute
UNPROTECTED_BACKOFF_DELAY = 5.minutes
attr_encrypted :credentials,
key: Gitlab::Application.secrets.db_key_base,
marshal: true,
encode: true,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
algorithm: 'aes-256-cbc'
default_value_for :only_protected_branches, true
belongs_to :project, inverse_of: :remote_mirrors
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
validates :url, addressable_url: true, if: :url_changed?
before_save :set_new_remote_name, if: :mirror_url_changed?
after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
after_save :refresh_remote, if: :mirror_url_changed?
after_update :reset_fields, if: :mirror_url_changed?
after_commit :remove_remote, on: :destroy
scope :enabled, -> { where(enabled: true) }
scope :started, -> { with_update_status(:started) }
scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.day.ago, 1.day.ago) }
state_machine :update_status, initial: :none do
event :update_start do
transition [:none, :finished, :failed] => :started
end
event :update_finish do
transition started: :finished
end
event :update_fail do
transition started: :failed
end
state :started
state :finished
state :failed
after_transition any => :started do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_running, path: remote_mirror.project.full_path)
remote_mirror.update(last_update_started_at: Time.now)
end
after_transition started: :finished do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_finished, path: remote_mirror.project.full_path)
timestamp = Time.now
remote_mirror.update_attributes!(
last_update_at: timestamp, last_successful_update_at: timestamp, last_error: nil
)
end
after_transition started: :failed do |remote_mirror, _|
Gitlab::Metrics.add_event(:remote_mirrors_failed, path: remote_mirror.project.full_path)
remote_mirror.update(last_update_at: Time.now)
end
end
def remote_name
super || fallback_remote_name
end
def update_failed?
update_status == 'failed'
end
def update_in_progress?
update_status == 'started'
end
def update_repository(options)
raw.update(options)
end
def sync
return unless enabled?
return if Gitlab::Geo.secondary?
if recently_scheduled?
RepositoryUpdateRemoteMirrorWorker.perform_in(backoff_delay, self.id, Time.now)
else
RepositoryUpdateRemoteMirrorWorker.perform_async(self.id, Time.now)
end
end
def enabled
return false unless project && super
return false unless project.remote_mirror_available?
return false unless project.repository_exists?
return false if project.pending_delete?
true
end
alias_method :enabled?, :enabled
def updated_since?(timestamp)
last_update_started_at && last_update_started_at > timestamp && !update_failed?
end
def mark_for_delete_if_blank_url
mark_for_destruction if url.blank?
end
def mark_as_failed(error_message)
update_fail
update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message))
end
def url=(value)
super(value) && return unless Gitlab::UrlSanitizer.valid?(value)
mirror_url = Gitlab::UrlSanitizer.new(value)
self.credentials = mirror_url.credentials
super(mirror_url.sanitized_url)
end
def url
if super
Gitlab::UrlSanitizer.new(super, credentials: credentials).full_url
end
rescue
super
end
def safe_url
return if url.nil?
result = URI.parse(url)
result.password = '*****' if result.password
result.user = '*****' if result.user && result.user != "git" # tokens or other data may be saved as user
result.to_s
end
private
def raw
@raw ||= Gitlab::Git::RemoteMirror.new(project.repository.raw, remote_name)
end
def fallback_remote_name
return unless id
"remote_mirror_#{id}"
end
def recently_scheduled?
return false unless self.last_update_started_at
self.last_update_started_at >= Time.now - backoff_delay
end
def backoff_delay
if self.only_protected_branches
PROTECTED_BACKOFF_DELAY
else
UNPROTECTED_BACKOFF_DELAY
end
end
def reset_fields
update_columns(
last_error: nil,
last_update_at: nil,
last_successful_update_at: nil,
update_status: 'finished'
)
end
def set_override_remote_mirror_available
enabled = read_attribute(:enabled)
project.update(remote_mirror_available_overridden: enabled)
end
def set_new_remote_name
self.remote_name = "remote_mirror_#{SecureRandom.hex}"
end
def refresh_remote
return unless project
# Before adding a new remote we have to delete the data from
# the previous remote name
prev_remote_name = remote_name_was || fallback_remote_name
run_after_commit do
project.repository.async_remove_remote(prev_remote_name)
end
project.repository.add_remote(remote_name, url)
end
def remove_remote
return unless project # could be pending to delete so don't need to touch the git repository
project.repository.async_remove_remote(remote_name)
end
def mirror_url_changed?
url_changed? || encrypted_credentials_changed?
end
end

View file

@ -861,6 +861,20 @@ class Repository
gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
end
def async_remove_remote(remote_name)
return unless remote_name
job_id = RepositoryRemoveRemoteWorker.perform_async(project.id, remote_name)
if job_id
Rails.logger.info("Remove remote job scheduled for #{project.id} with remote name: #{remote_name} job ID #{job_id}.")
else
Rails.logger.info("Remove remote job failed to create for #{project.id} with remote name #{remote_name}.")
end
job_id
end
def fetch_source_branch!(source_repository, source_branch, local_ref)
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end

View file

@ -0,0 +1,13 @@
class ProjectMirrorEntity < Grape::Entity
prepend ::EE::ProjectMirrorEntity
expose :id
expose :remote_mirrors_attributes do |project|
next [] unless project.remote_mirrors.present?
project.remote_mirrors.map do |remote|
remote.as_json(only: %i[id url enabled])
end
end
end

View file

@ -55,6 +55,7 @@ class GitPushService < BaseService
execute_related_hooks
perform_housekeeping
update_remote_mirrors
update_caches
update_signatures
@ -119,6 +120,13 @@ class GitPushService < BaseService
protected
def update_remote_mirrors
return unless @project.has_remote_mirror?
@project.mark_stuck_remote_mirrors_as_failed!
@project.update_remote_mirrors
end
def execute_related_hooks
# Update merge requests that may be affected by this push. A new branch
# could cause the last commit of a merge request to change.

View file

@ -0,0 +1,30 @@
module Projects
class UpdateRemoteMirrorService < BaseService
attr_reader :errors
def execute(remote_mirror)
@errors = []
return success unless remote_mirror.enabled?
begin
repository.fetch_remote(remote_mirror.remote_name, no_tags: true)
opts = {}
if remote_mirror.only_protected_branches?
opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name)
end
remote_mirror.update_repository(opts)
rescue => e
errors << e.message.strip
end
if errors.present?
error(errors.join("\n\n"))
else
success
end
end
end
end

View file

@ -0,0 +1,50 @@
- expanded = Rails.env.test?
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
Push to a remote repository
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Set up the remote repository that you want to update with the content of the current repository
every time someone pushes to it.
= link_to 'Read more', help_page_path('workflow/repository_mirroring', anchor: 'pushing-to-a-remote-repository'), target: '_blank'
.settings-content
= form_for @project, url: project_mirror_path(@project) do |f|
%div
= form_errors(@project)
= render "shared/remote_mirror_update_button", remote_mirror: @remote_mirror
- if @remote_mirror.last_error.present?
.panel.panel-danger
.panel-heading
- if @remote_mirror.last_update_at
The remote repository failed to update #{time_ago_with_tooltip(@remote_mirror.last_update_at)}.
- else
The remote repository failed to update.
- if @remote_mirror.last_successful_update_at
Last successful update #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.
.panel-body
%pre
:preserve
#{h(@remote_mirror.last_error.strip)}
= f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
.form-group
= rm_form.check_box :enabled, class: "pull-left"
.prepend-left-20
= rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
%p.light.append-bottom-0
Automatically update the remote mirror's branches, tags, and commits from this repository every time someone pushes to it.
.form-group.has-feedback
= rm_form.label :url, "Git repository URL", class: "label-light"
= rm_form.text_field :url, class: "form-control", placeholder: 'https://username:password@gitlab.company.com/group/project.git'
= render "projects/mirrors/instructions"
.form-group
= rm_form.check_box :only_protected_branches, class: 'pull-left'
.prepend-left-20
= rm_form.label :only_protected_branches, class: 'label-light'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
= f.submit 'Save changes', class: 'btn btn-create', name: 'update_remote_mirror'

View file

@ -0,0 +1,3 @@
- if can?(current_user, :admin_mirror, @project)
= render 'projects/mirrors/push'

View file

@ -0,0 +1,13 @@
- if @project.has_remote_mirror?
.append-bottom-default
- if remote_mirror.update_in_progress?
%span.btn.disabled
= icon("refresh spin")
Updating&hellip;
- else
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn" do
= icon("refresh")
Update Now
- if @remote_mirror.last_successful_update_at
%p.inline.prepend-left-10
Successfully updated #{time_ago_with_tooltip(@remote_mirror.last_successful_update_at)}.

View file

@ -112,3 +112,4 @@
- update_user_activity
- upload_checksum
- web_hook
- repository_update_remote_mirror

View file

@ -0,0 +1,35 @@
class RepositoryRemoveRemoteWorker
include ApplicationWorker
include ExclusiveLeaseGuard
LEASE_TIMEOUT = 1.hour
attr_reader :project, :remote_name
def perform(project_id, remote_name)
@remote_name = remote_name
@project = Project.find_by_id(project_id)
return unless @project
logger.info("Removing remote #{remote_name} from project #{project.id}")
try_obtain_lease do
remove_remote = @project.repository.remove_remote(remote_name)
if remove_remote
logger.info("Remote #{remote_name} was successfully removed from project #{project.id}")
else
logger.error("Could not remove remote #{remote_name} from project #{project.id}")
end
end
end
def lease_timeout
LEASE_TIMEOUT
end
def lease_key
"remove_remote_#{project.id}_#{remote_name}"
end
end

View file

@ -0,0 +1,49 @@
class RepositoryUpdateRemoteMirrorWorker
UpdateAlreadyInProgressError = Class.new(StandardError)
UpdateError = Class.new(StandardError)
include ApplicationWorker
include Gitlab::ShellAdapter
sidekiq_options retry: 3, dead: false
sidekiq_retry_in { |count| 30 * count }
sidekiq_retries_exhausted do |msg, _|
Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}"
end
def perform(remote_mirror_id, scheduled_time)
remote_mirror = RemoteMirror.find(remote_mirror_id)
return if remote_mirror.updated_since?(scheduled_time)
raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress?
remote_mirror.update_start
project = remote_mirror.project
current_user = project.creator
result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror)
raise UpdateError, result[:message] if result[:status] == :error
remote_mirror.update_finish
rescue UpdateAlreadyInProgressError
raise
rescue UpdateError => ex
fail_remote_mirror(remote_mirror, ex.message)
raise
rescue => ex
return unless remote_mirror
fail_remote_mirror(remote_mirror, ex.message)
raise UpdateError, "#{ex.class}: #{ex.message}"
end
private
def fail_remote_mirror(remote_mirror, message)
remote_mirror.mark_as_failed(message)
Rails.logger.error(message)
end
end

View file

@ -73,3 +73,6 @@
- [object_storage, 1]
- [plugin, 1]
- [pipeline_background, 1]
- [repository_update_remote_mirror, 1]
- [repository_remove_remote, 1]

View file

@ -314,10 +314,10 @@ ActiveRecord::Schema.define(version: 20180503200320) do
t.integer "auto_canceled_by_id"
t.boolean "retried"
t.integer "stage_id"
t.integer "artifacts_file_store"
t.integer "artifacts_metadata_store"
t.boolean "protected"
t.integer "failure_reason"
t.integer "artifacts_file_store"
t.integer "artifacts_metadata_store"
end
add_index "ci_builds", ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
@ -365,13 +365,13 @@ ActiveRecord::Schema.define(version: 20180503200320) do
t.integer "project_id", null: false
t.integer "job_id", null: false
t.integer "file_type", null: false
t.integer "file_store"
t.integer "size", limit: 8
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.datetime_with_timezone "expire_at"
t.string "file"
t.binary "file_sha256"
t.integer "file_store"
end
add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree

View file

@ -106,6 +106,7 @@ excluded_attributes:
- :last_repository_updated_at
- :last_repository_check_at
- :storage_version
- :remote_mirror_available_overridden
- :description_html
snippets:
- :expired_at

View file

@ -71,6 +71,7 @@ module Gitlab
projects_imported_from_github: Project.where(import_type: 'github').count,
protected_branches: ProtectedBranch.count,
releases: Release.count,
remote_mirrors: RemoteMirror.count,
snippets: Snippet.count,
todos: Todo.count,
uploads: Upload.count,

View file

@ -0,0 +1,72 @@
require 'spec_helper'
describe Projects::MirrorsController do
include ReactiveCachingHelpers
describe 'setting up a remote mirror' do
set(:project) { create(:project, :repository) }
context 'when the current project is not a mirror' do
it 'allows to create a remote mirror' do
sign_in(project.owner)
expect do
do_put(project, remote_mirrors_attributes: { '0' => { 'enabled' => 1, 'url' => 'http://foo.com' } })
end.to change { RemoteMirror.count }.to(1)
end
end
end
describe '#update' do
let(:project) { create(:project, :repository, :remote_mirror) }
before do
sign_in(project.owner)
end
around do |example|
Sidekiq::Testing.fake! { example.run }
end
context 'With valid URL for a push' do
let(:remote_mirror_attributes) do
{ "0" => { "enabled" => "0", url: 'https://updated.example.com' } }
end
it 'processes a successful update' do
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
expect(response).to redirect_to(project_settings_repository_path(project))
expect(flash[:notice]).to match(/successfully updated/)
end
it 'should create a RemoteMirror object' do
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1)
end
end
context 'With invalid URL for a push' do
let(:remote_mirror_attributes) do
{ "0" => { "enabled" => "0", url: 'ftp://invalid.invalid' } }
end
it 'processes an unsuccessful update' do
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
expect(response).to redirect_to(project_settings_repository_path(project))
expect(flash[:alert]).to match(/must be a valid URL/)
end
it 'should not create a RemoteMirror object' do
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count)
end
end
end
def do_put(project, options, extra_attrs = {})
attrs = extra_attrs.merge(namespace_id: project.namespace.to_param, project_id: project.to_param)
attrs[:project] = options
put :update, attrs
end
end

View file

@ -183,6 +183,17 @@ FactoryBot.define do
end
end
trait :remote_mirror do
transient do
remote_name "remote_mirror_#{SecureRandom.hex}"
url "http://foo.com"
enabled true
end
after(:create) do |project, evaluator|
project.remote_mirrors.create!(url: evaluator.url, enabled: evaluator.enabled)
end
end
trait :stubbed_repository do
after(:build) do |project|
allow(project).to receive(:empty_repo?).and_return(false)

View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :remote_mirror, class: 'RemoteMirror' do
association :project, :repository
url "http://foo:bar@test.com"
end
end

View file

@ -0,0 +1,34 @@
require 'spec_helper'
feature 'Project remote mirror', :feature do
let(:project) { create(:project, :repository, :remote_mirror) }
let(:remote_mirror) { project.remote_mirrors.first }
let(:user) { create(:user) }
describe 'On a project', :js do
before do
project.add_master(user)
sign_in user
end
context 'when last_error is present but last_update_at is not' do
it 'renders error message without timstamp' do
remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: nil)
visit project_mirror_path(project)
expect(page).to have_content('The remote repository failed to update.')
end
end
context 'when last_error and last_update_at are present' do
it 'renders error message with timestamp' do
remote_mirror.update_attributes(last_error: 'Some new error', last_update_at: Time.now - 5.minutes)
visit project_mirror_path(project)
expect(page).to have_content('The remote repository failed to update 5 minutes ago.')
end
end
end
end

View file

@ -115,5 +115,20 @@ describe 'Projects > Settings > Repository settings' do
expect(page).to have_content('Your new project deploy token has been created')
end
end
context 'remote mirror settings' do
let(:user2) { create(:user) }
before do
project.add_master(user2)
visit project_settings_repository_path(project)
end
it 'shows push mirror settings' do
expect(page).to have_selector('#project_remote_mirrors_attributes_0_enabled')
expect(page).to have_selector('#project_remote_mirrors_attributes_0_url')
end
end
end
end

View file

@ -268,6 +268,7 @@ project:
- pages_domains
- authorized_users
- project_authorizations
- remote_mirrors
- route
- redirect_routes
- statistics

View file

@ -96,6 +96,7 @@ describe Gitlab::UsageData do
pages_domains
protected_branches
releases
remote_mirrors
snippets
todos
uploads

View file

@ -1852,6 +1852,85 @@ describe Project do
it { expect(project.gitea_import?).to be true }
end
describe '#has_remote_mirror?' do
let(:project) { create(:project, :remote_mirror, :import_started) }
subject { project.has_remote_mirror? }
before do
allow_any_instance_of(RemoteMirror).to receive(:refresh_remote)
end
it 'returns true when a remote mirror is enabled' do
is_expected.to be_truthy
end
it 'returns false when remote mirror is disabled' do
project.remote_mirrors.first.update_attributes(enabled: false)
is_expected.to be_falsy
end
end
describe '#update_remote_mirrors' do
let(:project) { create(:project, :remote_mirror, :import_started) }
delegate :update_remote_mirrors, to: :project
before do
allow_any_instance_of(RemoteMirror).to receive(:refresh_remote)
end
it 'syncs enabled remote mirror' do
expect_any_instance_of(RemoteMirror).to receive(:sync)
update_remote_mirrors
end
# TODO: study if remote_mirror_available_overridden is still a necessary attribute considering that
# it is no longer under any license
it 'does nothing when remote mirror is disabled globally and not overridden' do
stub_application_setting(mirror_available: false)
project.remote_mirror_available_overridden = false
expect_any_instance_of(RemoteMirror).not_to receive(:sync)
update_remote_mirrors
end
it 'does not sync disabled remote mirrors' do
project.remote_mirrors.first.update_attributes(enabled: false)
expect_any_instance_of(RemoteMirror).not_to receive(:sync)
update_remote_mirrors
end
end
describe '#remote_mirror_available?' do
let(:project) { create(:project) }
context 'when remote mirror global setting is enabled' do
it 'returns true' do
expect(project.remote_mirror_available?).to be(true)
end
end
context 'when remote mirror global setting is disabled' do
before do
stub_application_setting(mirror_available: false)
end
it 'returns true when overridden' do
project.remote_mirror_available_overridden = true
expect(project.remote_mirror_available?).to be(true)
end
it 'returns false when not overridden' do
expect(project.remote_mirror_available?).to be(false)
end
end
end
describe '#ancestors_upto', :nested_groups do
let(:parent) { create(:group) }
let(:child) { create(:group, parent: parent) }

View file

@ -0,0 +1,275 @@
require 'rails_helper'
describe RemoteMirror do
describe 'URL validation' do
context 'with a valid URL' do
it 'should be valid' do
remote_mirror = build(:remote_mirror)
expect(remote_mirror).to be_valid
end
end
context 'with an invalid URL' do
it 'should not be valid' do
remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid')
expect(remote_mirror).not_to be_valid
expect(remote_mirror.errors[:url].size).to eq(2)
end
end
end
describe 'encrypting credentials' do
context 'when setting URL for a first time' do
it 'stores the URL without credentials' do
mirror = create_mirror(url: 'http://foo:bar@test.com')
expect(mirror.read_attribute(:url)).to eq('http://test.com')
end
it 'stores the credentials on a separate field' do
mirror = create_mirror(url: 'http://foo:bar@test.com')
expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' })
end
it 'handles credentials with large content' do
mirror = create_mirror(url: 'http://bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif:9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75@test.com')
expect(mirror.credentials).to eq({
user: 'bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif',
password: '9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75'
})
end
end
context 'when updating the URL' do
it 'allows a new URL without credentials' do
mirror = create_mirror(url: 'http://foo:bar@test.com')
mirror.update_attribute(:url, 'http://test.com')
expect(mirror.url).to eq('http://test.com')
expect(mirror.credentials).to eq({ user: nil, password: nil })
end
it 'allows a new URL with credentials' do
mirror = create_mirror(url: 'http://test.com')
mirror.update_attribute(:url, 'http://foo:bar@test.com')
expect(mirror.url).to eq('http://foo:bar@test.com')
expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' })
end
it 'updates the remote config if credentials changed' do
mirror = create_mirror(url: 'http://foo:bar@test.com')
repo = mirror.project.repository
mirror.update_attribute(:url, 'http://foo:baz@test.com')
config = repo.raw_repository.rugged.config
expect(config["remote.#{mirror.remote_name}.url"]).to eq('http://foo:baz@test.com')
end
it 'removes previous remote' do
mirror = create_mirror(url: 'http://foo:bar@test.com')
expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original
mirror.update_attributes(url: 'http://test.com')
end
end
end
describe '#remote_name' do
context 'when remote name is persisted in the database' do
it 'returns remote name with random value' do
allow(SecureRandom).to receive(:hex).and_return('secret')
remote_mirror = create(:remote_mirror)
expect(remote_mirror.remote_name).to eq("remote_mirror_secret")
end
end
context 'when remote name is not persisted in the database' do
it 'returns remote name with remote mirror id' do
remote_mirror = create(:remote_mirror)
remote_mirror.remote_name = nil
expect(remote_mirror.remote_name).to eq("remote_mirror_#{remote_mirror.id}")
end
end
context 'when remote is not persisted in the database' do
it 'returns nil' do
remote_mirror = build(:remote_mirror, remote_name: nil)
expect(remote_mirror.remote_name).to be_nil
end
end
end
describe '#safe_url' do
context 'when URL contains credentials' do
it 'masks the credentials' do
mirror = create_mirror(url: 'http://foo:bar@test.com')
expect(mirror.safe_url).to eq('http://*****:*****@test.com')
end
end
context 'when URL does not contain credentials' do
it 'shows the full URL' do
mirror = create_mirror(url: 'http://test.com')
expect(mirror.safe_url).to eq('http://test.com')
end
end
end
context 'when remote mirror gets destroyed' do
it 'removes remote' do
mirror = create_mirror(url: 'http://foo:bar@test.com')
expect(RepositoryRemoveRemoteWorker).to receive(:perform_async).with(mirror.project.id, mirror.remote_name).and_call_original
mirror.destroy!
end
end
context 'stuck mirrors' do
it 'includes mirrors stuck in started with no last_update_at set' do
mirror = create_mirror(url: 'http://cantbeblank',
update_status: 'started',
last_update_at: nil,
updated_at: 25.hours.ago)
expect(described_class.stuck.last).to eq(mirror)
end
end
context '#sync' do
let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
around do |example|
Timecop.freeze { example.run }
end
context 'with remote mirroring disabled' do
it 'returns nil' do
remote_mirror.update_attributes(enabled: false)
expect(remote_mirror.sync).to be_nil
end
end
context 'as a Geo secondary' do
it 'returns nil' do
allow(Gitlab::Geo).to receive(:secondary?).and_return(true)
expect(remote_mirror.sync).to be_nil
end
end
context 'with remote mirroring enabled' do
context 'with only protected branches enabled' do
context 'when it did not update in the last minute' do
it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do
expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now)
remote_mirror.sync
end
end
context 'when it did update in the last minute' do
it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next minute' do
remote_mirror.last_update_started_at = Time.now - 30.seconds
expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::PROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.now)
remote_mirror.sync
end
end
end
context 'with only protected branches disabled' do
before do
remote_mirror.only_protected_branches = false
end
context 'when it did not update in the last 5 minutes' do
it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do
expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.now)
remote_mirror.sync
end
end
context 'when it did update within the last 5 minutes' do
it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next 5 minutes' do
remote_mirror.last_update_started_at = Time.now - 30.seconds
expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::UNPROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.now)
remote_mirror.sync
end
end
end
end
end
context '#updated_since?' do
let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
let(:timestamp) { Time.now - 5.minutes }
around do |example|
Timecop.freeze { example.run }
end
before do
remote_mirror.update_attributes(last_update_started_at: Time.now)
end
context 'when remote mirror does not have status failed' do
it 'returns true when last update started after the timestamp' do
expect(remote_mirror.updated_since?(timestamp)).to be true
end
it 'returns false when last update started before the timestamp' do
expect(remote_mirror.updated_since?(Time.now + 5.minutes)).to be false
end
end
context 'when remote mirror has status failed' do
it 'returns false when last update started after the timestamp' do
remote_mirror.update_attributes(update_status: 'failed')
expect(remote_mirror.updated_since?(timestamp)).to be false
end
end
end
context 'no project' do
it 'includes mirror with a project in pending_delete' do
mirror = create_mirror(url: 'http://cantbeblank',
update_status: 'finished',
enabled: true,
last_update_at: nil,
updated_at: 25.hours.ago)
project = mirror.project
project.pending_delete = true
project.save
mirror.reload
expect(mirror.sync).to be_nil
expect(mirror.valid?).to be_truthy
expect(mirror.update_status).to eq('finished')
end
end
def create_mirror(params)
project = FactoryBot.create(:project, :repository)
project.remote_mirrors.create!(params)
end
end

View file

@ -758,6 +758,38 @@ describe Repository do
end
end
describe '#async_remove_remote' do
before do
masterrev = repository.find_branch('master').dereferenced_target
create_remote_branch('joe', 'remote_branch', masterrev)
end
context 'when worker is scheduled successfully' do
before do
masterrev = repository.find_branch('master').dereferenced_target
create_remote_branch('remote_name', 'remote_branch', masterrev)
allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return('1234')
end
it 'returns job_id' do
expect(repository.async_remove_remote('joe')).to eq('1234')
end
end
context 'when worker does not schedule successfully' do
before do
allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return(nil)
end
it 'returns nil' do
expect(Rails.logger).to receive(:info).with("Remove remote job failed to create for #{project.id} with remote name joe.")
expect(repository.async_remove_remote('joe')).to be_nil
end
end
end
describe '#fetch_ref' do
let(:broken_repository) { create(:project, :broken_storage).repository }

View file

@ -14,6 +14,72 @@ describe GitPushService, services: true do
project.add_master(user)
end
describe 'with remote mirrors' do
let(:project) { create(:project, :repository, :remote_mirror) }
subject do
described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
end
context 'when remote mirror feature is enabled' do
it 'fails stuck remote mirrors' do
allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
subject.execute
end
it 'updates remote mirrors' do
expect(project).to receive(:update_remote_mirrors)
subject.execute
end
end
context 'when remote mirror feature is disabled' do
before do
stub_application_setting(mirror_available: false)
end
context 'with remote mirrors global setting overridden' do
before do
project.remote_mirror_available_overridden = true
end
it 'fails stuck remote mirrors' do
allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
subject.execute
end
it 'updates remote mirrors' do
expect(project).to receive(:update_remote_mirrors)
subject.execute
end
end
context 'without remote mirrors global setting overridden' do
before do
project.remote_mirror_available_overridden = false
end
it 'does not fails stuck remote mirrors' do
expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
subject.execute
end
it 'does not updates remote mirrors' do
expect(project).not_to receive(:update_remote_mirrors)
subject.execute
end
end
end
end
describe 'Push branches' do
subject do
execute_service(project, user, oldrev, newrev, ref)

View file

@ -65,6 +65,19 @@ describe Projects::DestroyService do
Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
end
context 'when has remote mirrors' do
let!(:project) do
create(:project, :repository, namespace: user.namespace).tap do |project|
project.remote_mirrors.create(url: 'http://test.com')
end
end
let!(:async) { true }
it 'destroys them' do
expect(RemoteMirror.count).to eq(0)
end
end
it_behaves_like 'deleting the project'
it 'invalidates personal_project_count cache' do

View file

@ -0,0 +1,355 @@
require 'spec_helper'
describe Projects::UpdateRemoteMirrorService do
let(:project) { create(:project, :repository) }
let(:remote_project) { create(:forked_project_with_submodules) }
let(:repository) { project.repository }
let(:raw_repository) { repository.raw }
let(:remote_mirror) { project.remote_mirrors.create!(url: remote_project.http_url_to_repo, enabled: true, only_protected_branches: false) }
subject { described_class.new(project, project.creator) }
describe "#execute", :skip_gitaly_mock do
before do
create_branch(repository, 'existing-branch')
allow(raw_repository).to receive(:remote_tags) do
generate_tags(repository, 'v1.0.0', 'v1.1.0')
end
allow(raw_repository).to receive(:push_remote_branches).and_return(true)
end
it "fetches the remote repository" do
expect(repository).to receive(:fetch_remote).with(remote_mirror.remote_name, no_tags: true) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
end
subject.execute(remote_mirror)
end
it "succeeds" do
allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
result = subject.execute(remote_mirror)
expect(result[:status]).to eq(:success)
end
describe 'Syncing branches' do
it "push all the branches the first time" do
allow(repository).to receive(:fetch_remote)
expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, local_branch_names)
subject.execute(remote_mirror)
end
it "does not push anything is remote is up to date" do
allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
expect(raw_repository).not_to receive(:push_remote_branches)
subject.execute(remote_mirror)
end
it "sync new branches" do
# call local_branch_names early so it is not called after the new branch has been created
current_branches = local_branch_names
allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, current_branches) }
create_branch(repository, 'my-new-branch')
expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['my-new-branch'])
subject.execute(remote_mirror)
end
it "sync updated branches" do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
update_branch(repository, 'existing-branch')
end
expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
subject.execute(remote_mirror)
end
context 'when push only protected branches option is set' do
let(:unprotected_branch_name) { 'existing-branch' }
let(:protected_branch_name) do
project.repository.branch_names.find { |n| n != unprotected_branch_name }
end
let!(:protected_branch) do
create(:protected_branch, project: project, name: protected_branch_name)
end
before do
project.reload
remote_mirror.only_protected_branches = true
end
it "sync updated protected branches" do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
update_branch(repository, protected_branch_name)
end
expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
subject.execute(remote_mirror)
end
it 'does not sync unprotected branches' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
update_branch(repository, unprotected_branch_name)
end
expect(raw_repository).not_to receive(:push_remote_branches).with(remote_mirror.remote_name, [unprotected_branch_name])
subject.execute(remote_mirror)
end
end
context 'when branch exists in local and remote repo' do
context 'when it has diverged' do
it 'syncs branches' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
update_remote_branch(repository, remote_mirror.remote_name, 'markdown')
end
expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['markdown'])
subject.execute(remote_mirror)
end
end
end
describe 'for delete' do
context 'when branch exists in local and remote repo' do
it 'deletes the branch from remote repo' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
delete_branch(repository, 'existing-branch')
end
expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
subject.execute(remote_mirror)
end
end
context 'when push only protected branches option is set' do
before do
remote_mirror.only_protected_branches = true
end
context 'when branch exists in local and remote repo' do
let!(:protected_branch_name) { local_branch_names.first }
before do
create(:protected_branch, project: project, name: protected_branch_name)
project.reload
end
it 'deletes the protected branch from remote repo' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
delete_branch(repository, protected_branch_name)
end
expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
subject.execute(remote_mirror)
end
it 'does not delete the unprotected branch from remote repo' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
delete_branch(repository, 'existing-branch')
end
expect(raw_repository).not_to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['existing-branch'])
subject.execute(remote_mirror)
end
end
context 'when branch only exists on remote repo' do
let!(:protected_branch_name) { 'remote-branch' }
before do
create(:protected_branch, project: project, name: protected_branch_name)
end
context 'when it has diverged' do
it 'does not delete the remote branch' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
rev = repository.find_branch('markdown').dereferenced_target
create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
end
expect(raw_repository).not_to receive(:delete_remote_branches)
subject.execute(remote_mirror)
end
end
context 'when it has not diverged' do
it 'deletes the remote branch' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
masterrev = repository.find_branch('master').dereferenced_target
create_remote_branch(repository, remote_mirror.remote_name, protected_branch_name, masterrev.id)
end
expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, [protected_branch_name])
subject.execute(remote_mirror)
end
end
end
end
context 'when branch only exists on remote repo' do
context 'when it has diverged' do
it 'does not delete the remote branch' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
rev = repository.find_branch('markdown').dereferenced_target
create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', rev.id)
end
expect(raw_repository).not_to receive(:delete_remote_branches)
subject.execute(remote_mirror)
end
end
context 'when it has not diverged' do
it 'deletes the remote branch' do
allow(repository).to receive(:fetch_remote) do
sync_remote(repository, remote_mirror.remote_name, local_branch_names)
masterrev = repository.find_branch('master').dereferenced_target
create_remote_branch(repository, remote_mirror.remote_name, 'remote-branch', masterrev.id)
end
expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['remote-branch'])
subject.execute(remote_mirror)
end
end
end
end
end
describe 'Syncing tags' do
before do
allow(repository).to receive(:fetch_remote) { sync_remote(repository, remote_mirror.remote_name, local_branch_names) }
end
context 'when there are not tags to push' do
it 'does not try to push tags' do
allow(repository).to receive(:remote_tags) { {} }
allow(repository).to receive(:tags) { [] }
expect(repository).not_to receive(:push_tags)
subject.execute(remote_mirror)
end
end
context 'when there are some tags to push' do
it 'pushes tags to remote' do
allow(raw_repository).to receive(:remote_tags) { {} }
expect(raw_repository).to receive(:push_remote_branches).with(remote_mirror.remote_name, ['v1.0.0', 'v1.1.0'])
subject.execute(remote_mirror)
end
end
context 'when there are some tags to delete' do
it 'deletes tags from remote' do
remote_tags = generate_tags(repository, 'v1.0.0', 'v1.1.0')
allow(raw_repository).to receive(:remote_tags) { remote_tags }
repository.rm_tag(create(:user), 'v1.0.0')
expect(raw_repository).to receive(:delete_remote_branches).with(remote_mirror.remote_name, ['v1.0.0'])
subject.execute(remote_mirror)
end
end
end
end
def create_branch(repository, branch_name)
rugged = repository.rugged
masterrev = repository.find_branch('master').dereferenced_target
parentrev = repository.commit(masterrev).parent_id
rugged.references.create("refs/heads/#{branch_name}", parentrev)
repository.expire_branches_cache
end
def create_remote_branch(repository, remote_name, branch_name, source_id)
rugged = repository.rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_id)
end
def sync_remote(repository, remote_name, local_branch_names)
rugged = repository.rugged
local_branch_names.each do |branch|
target = repository.find_branch(branch).try(:dereferenced_target)
rugged.references.create("refs/remotes/#{remote_name}/#{branch}", target.id) if target
end
end
def update_remote_branch(repository, remote_name, branch)
rugged = repository.rugged
masterrev = repository.find_branch('master').dereferenced_target.id
rugged.references.create("refs/remotes/#{remote_name}/#{branch}", masterrev, force: true)
repository.expire_branches_cache
end
def update_branch(repository, branch)
rugged = repository.rugged
masterrev = repository.find_branch('master').dereferenced_target.id
# Updated existing branch
rugged.references.create("refs/heads/#{branch}", masterrev, force: true)
repository.expire_branches_cache
end
def delete_branch(repository, branch)
rugged = repository.rugged
rugged.references.delete("refs/heads/#{branch}")
repository.expire_branches_cache
end
def generate_tags(repository, *tag_names)
tag_names.each_with_object([]) do |name, tags|
tag = repository.find_tag(name)
target = tag.try(:target)
target_commit = tag.try(:dereferenced_target)
tags << Gitlab::Git::Tag.new(repository.raw_repository, name, target, target_commit)
end
end
def local_branch_names
branch_names = repository.branches.map(&:name)
# we want the protected branch to be pushed first
branch_names.unshift(branch_names.delete('master'))
end
end

View file

@ -0,0 +1,50 @@
require 'rails_helper'
describe RepositoryRemoveRemoteWorker do
subject(:worker) { described_class.new }
describe '#perform' do
let(:remote_name) { 'joe'}
let!(:project) { create(:project, :repository) }
context 'when it cannot obtain lease' do
it 'logs error' do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) { nil }
expect_any_instance_of(Repository).not_to receive(:remove_remote)
expect(worker).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
worker.perform(project.id, remote_name)
end
end
context 'when it gets the lease' do
before do
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(true)
end
context 'when project does not exist' do
it 'returns nil' do
expect(worker.perform(-1, 'remote_name')).to be_nil
end
end
context 'when project exists' do
it 'removes remote from repository' do
masterrev = project.repository.find_branch('master').dereferenced_target
create_remote_branch(remote_name, 'remote_branch', masterrev)
expect_any_instance_of(Repository).to receive(:remove_remote).with(remote_name).and_call_original
worker.perform(project.id, remote_name)
end
end
end
end
def create_remote_branch(remote_name, branch_name, target)
rugged = project.repository.rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
end
end

View file

@ -0,0 +1,84 @@
require 'rails_helper'
describe RepositoryUpdateRemoteMirrorWorker do
subject { described_class.new }
let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first }
let(:scheduled_time) { Time.now - 5.minutes }
around do |example|
Timecop.freeze(Time.now) { example.run }
end
describe '#perform' do
context 'with status none' do
before do
remote_mirror.update_attributes(update_status: 'none')
end
it 'sets status as finished when update remote mirror service executes successfully' do
expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished')
end
it 'sets status as failed when update remote mirror service executes with errors' do
error_message = 'fail!'
expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message)
expect do
subject.perform(remote_mirror.id, Time.now)
end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message)
expect(remote_mirror.reload.update_status).to eq('failed')
end
it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do
remote_mirror.update_attributes(last_update_started_at: Time.now)
expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true)
expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror)
expect(subject.perform(remote_mirror.id, scheduled_time)).to be_nil
end
end
context 'with unexpected error' do
it 'marks mirror as failed' do
allow_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_raise(RuntimeError)
expect do
subject.perform(remote_mirror.id, Time.now)
end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError)
expect(remote_mirror.reload.update_status).to eq('failed')
end
end
context 'with another worker already running' do
before do
remote_mirror.update_attributes(update_status: 'started')
end
it 'raises RemoteMirrorUpdateAlreadyInProgressError' do
expect do
subject.perform(remote_mirror.id, Time.now)
end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateAlreadyInProgressError)
end
end
context 'with status failed' do
before do
remote_mirror.update_attributes(update_status: 'failed')
end
it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do
remote_mirror.update_attributes(last_update_started_at: Time.now)
expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false)
expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success)
expect { subject.perform(remote_mirror.id, scheduled_time) }.to change { remote_mirror.reload.update_status }.to('finished')
end
end
end
end