1# frozen_string_literal: true 2 3require 'spec_helper' 4 5RSpec.describe RemoteMirror, :mailer do 6 include GitHelpers 7 8 describe 'URL validation' do 9 context 'with a valid URL' do 10 it 'is valid' do 11 remote_mirror = build(:remote_mirror) 12 expect(remote_mirror).to be_valid 13 end 14 end 15 16 context 'with an invalid URL' do 17 it 'is not valid' do 18 remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid') 19 20 expect(remote_mirror).not_to be_valid 21 end 22 23 it 'does not allow url with an invalid user' do 24 remote_mirror = build(:remote_mirror, url: 'http://$user:password@invalid.invalid') 25 26 expect(remote_mirror).to be_invalid 27 expect(remote_mirror.errors[:url].first).to include('Username needs to start with an alphanumeric character') 28 end 29 30 it 'does not allow url pointing to localhost' do 31 remote_mirror = build(:remote_mirror, url: 'http://127.0.0.2/t.git') 32 33 expect(remote_mirror).to be_invalid 34 expect(remote_mirror.errors[:url].first).to include('Requests to loopback addresses are not allowed') 35 end 36 37 it 'does not allow url pointing to the local network' do 38 remote_mirror = build(:remote_mirror, url: 'https://192.168.1.1') 39 40 expect(remote_mirror).to be_invalid 41 expect(remote_mirror.errors[:url].first).to include('Requests to the local network are not allowed') 42 end 43 44 it 'returns a nil safe_url' do 45 remote_mirror = build(:remote_mirror, url: 'http://[0:0:0:0:ffff:123.123.123.123]/foo.git') 46 47 expect(remote_mirror.url).to eq('http://[0:0:0:0:ffff:123.123.123.123]/foo.git') 48 expect(remote_mirror.safe_url).to be_nil 49 end 50 end 51 end 52 53 describe 'encrypting credentials' do 54 context 'when setting URL for a first time' do 55 it 'stores the URL without credentials' do 56 mirror = create_mirror(url: 'http://foo:bar@test.com') 57 58 expect(mirror.read_attribute(:url)).to eq('http://test.com') 59 end 60 61 it 'stores the credentials on a separate field' do 62 mirror = create_mirror(url: 'http://foo:bar@test.com') 63 64 expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' }) 65 end 66 67 it 'handles credentials with large content' do 68 mirror = create_mirror(url: 'http://bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif:9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75@test.com') 69 70 expect(mirror.credentials).to eq({ 71 user: 'bxnhm8dote33ct932r3xavslj81wxmr7o8yux8do10oozckkif', 72 password: '9ne7fuvjn40qjt35dgt8v86q9m9g9essryxj76sumg2ccl2fg26c0krtz2gzfpyq4hf22h328uhq6npuiq6h53tpagtsj7vsrz75' 73 }) 74 end 75 end 76 77 context 'when updating the URL' do 78 it 'allows a new URL without credentials' do 79 mirror = create_mirror(url: 'http://foo:bar@test.com') 80 81 mirror.update_attribute(:url, 'http://test.com') 82 83 expect(mirror.url).to eq('http://test.com') 84 expect(mirror.credentials).to eq({ user: nil, password: nil }) 85 end 86 87 it 'allows a new URL with credentials' do 88 mirror = create_mirror(url: 'http://test.com') 89 90 mirror.update_attribute(:url, 'http://foo:bar@test.com') 91 92 expect(mirror.url).to eq('http://foo:bar@test.com') 93 expect(mirror.credentials).to eq({ user: 'foo', password: 'bar' }) 94 end 95 96 it 'does not update the repository config if credentials changed' do 97 mirror = create_mirror(url: 'http://foo:bar@test.com') 98 repo = mirror.project.repository 99 old_config = rugged_repo(repo).config 100 101 mirror.update_attribute(:url, 'http://foo:baz@test.com') 102 103 expect(rugged_repo(repo).config.to_hash).to eq(old_config.to_hash) 104 end 105 end 106 end 107 108 describe '#bare_url' do 109 it 'returns the URL without any credentials' do 110 remote_mirror = build(:remote_mirror, url: 'http://user:pass@example.com/foo') 111 112 expect(remote_mirror.bare_url).to eq('http://example.com/foo') 113 end 114 115 it 'returns an empty string when the URL is nil' do 116 remote_mirror = build(:remote_mirror, url: nil) 117 118 expect(remote_mirror.bare_url).to eq('') 119 end 120 end 121 122 describe '#update_repository' do 123 it 'performs update including options' do 124 git_remote_mirror = stub_const('Gitlab::Git::RemoteMirror', spy) 125 mirror = build(:remote_mirror) 126 127 expect(mirror).to receive(:options_for_update).and_return(keep_divergent_refs: true) 128 mirror.update_repository 129 130 expect(git_remote_mirror).to have_received(:new).with( 131 mirror.project.repository.raw, 132 mirror.url, 133 keep_divergent_refs: true 134 ) 135 expect(git_remote_mirror).to have_received(:update) 136 end 137 end 138 139 describe '#options_for_update' do 140 it 'includes the `keep_divergent_refs` option' do 141 mirror = build_stubbed(:remote_mirror, keep_divergent_refs: true) 142 143 options = mirror.options_for_update 144 145 expect(options).to include(keep_divergent_refs: true) 146 end 147 148 it 'includes the `only_branches_matching` option' do 149 branch = create(:protected_branch) 150 mirror = build_stubbed(:remote_mirror, project: branch.project, only_protected_branches: true) 151 152 options = mirror.options_for_update 153 154 expect(options).to include(only_branches_matching: [branch.name]) 155 end 156 157 it 'includes the `ssh_key` option' do 158 mirror = build(:remote_mirror, :ssh, ssh_private_key: 'private-key') 159 160 options = mirror.options_for_update 161 162 expect(options).to include(ssh_key: 'private-key') 163 end 164 165 it 'includes the `known_hosts` option' do 166 mirror = build(:remote_mirror, :ssh, ssh_known_hosts: 'known-hosts') 167 168 options = mirror.options_for_update 169 170 expect(options).to include(known_hosts: 'known-hosts') 171 end 172 end 173 174 describe '#safe_url' do 175 context 'when URL contains credentials' do 176 it 'masks the credentials' do 177 mirror = create_mirror(url: 'http://foo:bar@test.com') 178 179 expect(mirror.safe_url).to eq('http://*****:*****@test.com') 180 end 181 end 182 183 context 'when URL does not contain credentials' do 184 it 'shows the full URL' do 185 mirror = create_mirror(url: 'http://test.com') 186 187 expect(mirror.safe_url).to eq('http://test.com') 188 end 189 end 190 end 191 192 describe '#mark_as_failed!' do 193 let(:remote_mirror) { create(:remote_mirror) } 194 let(:error_message) { 'http://user:pass@test.com/root/repoC.git/' } 195 let(:sanitized_error_message) { 'http://*****:*****@test.com/root/repoC.git/' } 196 197 subject do 198 remote_mirror.update_start 199 remote_mirror.mark_as_failed!(error_message) 200 end 201 202 it 'sets the update_status to failed' do 203 subject 204 205 expect(remote_mirror.reload.update_status).to eq('failed') 206 end 207 208 it 'saves the sanitized error' do 209 subject 210 211 expect(remote_mirror.last_error).to eq(sanitized_error_message) 212 end 213 214 context 'notifications' do 215 let(:user) { create(:user) } 216 217 before do 218 remote_mirror.project.add_maintainer(user) 219 end 220 221 it 'notifies the project maintainers', :sidekiq_might_not_need_inline do 222 perform_enqueued_jobs { subject } 223 224 should_email(user) 225 end 226 end 227 end 228 229 describe '#hard_retry!' do 230 let(:remote_mirror) { create(:remote_mirror).tap {|mirror| mirror.update_column(:url, 'invalid') } } 231 232 it 'transitions an invalid mirror to the to_retry state' do 233 remote_mirror.hard_retry!('Invalid') 234 235 expect(remote_mirror.update_status).to eq('to_retry') 236 expect(remote_mirror.last_error).to eq('Invalid') 237 end 238 end 239 240 describe '#hard_fail!' do 241 let(:remote_mirror) { create(:remote_mirror).tap {|mirror| mirror.update_column(:url, 'invalid') } } 242 243 it 'transitions an invalid mirror to the failed state' do 244 remote_mirror.hard_fail!('Invalid') 245 246 expect(remote_mirror.update_status).to eq('failed') 247 expect(remote_mirror.last_error).to eq('Invalid') 248 expect(remote_mirror.last_update_at).not_to be_nil 249 expect(RemoteMirrorNotificationWorker.jobs).not_to be_empty 250 end 251 end 252 253 context 'when remote mirror gets destroyed' do 254 it 'does not remove the remote' do 255 mirror = create_mirror(url: 'http://foo:bar@test.com') 256 257 expect(RepositoryRemoveRemoteWorker).not_to receive(:perform_async) 258 259 mirror.destroy! 260 end 261 end 262 263 context 'stuck mirrors' do 264 it 'includes mirrors that were started over an hour ago' do 265 mirror = create_mirror(url: 'http://cantbeblank', 266 update_status: 'started', 267 last_update_started_at: 3.hours.ago, 268 last_update_at: 2.hours.ago) 269 270 expect(described_class.stuck.last).to eq(mirror) 271 end 272 273 it 'includes mirrors started over 3 hours ago for their first sync' do 274 mirror = create_mirror(url: 'http://cantbeblank', 275 update_status: 'started', 276 last_update_at: nil, 277 last_update_started_at: 4.hours.ago) 278 279 expect(described_class.stuck.last).to eq(mirror) 280 end 281 end 282 283 describe '#sync' do 284 let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } 285 286 around do |example| 287 freeze_time { example.run } 288 end 289 290 context 'with remote mirroring disabled' do 291 it 'returns nil' do 292 remote_mirror.update!(enabled: false) 293 294 expect(remote_mirror.sync).to be_nil 295 end 296 end 297 298 context 'with remote mirroring enabled' do 299 it 'defaults to disabling only protected branches' do 300 expect(remote_mirror.only_protected_branches?).to be_falsey 301 end 302 303 context 'with only protected branches enabled' do 304 before do 305 remote_mirror.only_protected_branches = true 306 end 307 308 context 'when it did not update in the last minute' do 309 it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do 310 expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.current) 311 312 remote_mirror.sync 313 end 314 end 315 316 context 'when it did update in the last minute' do 317 it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next minute' do 318 remote_mirror.last_update_started_at = Time.current - 30.seconds 319 320 expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::PROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.current) 321 322 remote_mirror.sync 323 end 324 end 325 end 326 327 context 'with only protected branches disabled' do 328 before do 329 remote_mirror.only_protected_branches = false 330 end 331 332 context 'when it did not update in the last 5 minutes' do 333 it 'schedules a RepositoryUpdateRemoteMirrorWorker to run now' do 334 expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_async).with(remote_mirror.id, Time.current) 335 336 remote_mirror.sync 337 end 338 end 339 340 context 'when it did update within the last 5 minutes' do 341 it 'schedules a RepositoryUpdateRemoteMirrorWorker to run in the next 5 minutes' do 342 remote_mirror.last_update_started_at = Time.current - 30.seconds 343 344 expect(RepositoryUpdateRemoteMirrorWorker).to receive(:perform_in).with(RemoteMirror::UNPROTECTED_BACKOFF_DELAY, remote_mirror.id, Time.current) 345 346 remote_mirror.sync 347 end 348 end 349 end 350 end 351 end 352 353 describe '#url=' do 354 let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } 355 356 it 'resets all the columns when URL changes' do 357 remote_mirror.update!(last_error: Time.current, 358 last_update_at: Time.current, 359 last_successful_update_at: Time.current, 360 update_status: 'started', 361 error_notification_sent: true) 362 363 expect { remote_mirror.update_attribute(:url, 'http://new.example.com') } 364 .to change { remote_mirror.last_error }.to(nil) 365 .and change { remote_mirror.last_update_at }.to(nil) 366 .and change { remote_mirror.last_successful_update_at }.to(nil) 367 .and change { remote_mirror.update_status }.to('finished') 368 .and change { remote_mirror.error_notification_sent }.to(false) 369 end 370 end 371 372 describe '#updated_since?' do 373 let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } 374 let(:timestamp) { Time.current - 5.minutes } 375 376 around do |example| 377 freeze_time { example.run } 378 end 379 380 before do 381 remote_mirror.update!(last_update_started_at: Time.current) 382 end 383 384 context 'when remote mirror does not have status failed' do 385 it 'returns true when last update started after the timestamp' do 386 expect(remote_mirror.updated_since?(timestamp)).to be true 387 end 388 389 it 'returns false when last update started before the timestamp' do 390 expect(remote_mirror.updated_since?(Time.current + 5.minutes)).to be false 391 end 392 end 393 394 context 'when remote mirror has status failed' do 395 it 'returns false when last update started after the timestamp' do 396 remote_mirror.update!(update_status: 'failed') 397 398 expect(remote_mirror.updated_since?(timestamp)).to be false 399 end 400 end 401 end 402 403 context 'no project' do 404 it 'includes mirror with a project in pending_delete' do 405 mirror = create_mirror(url: 'http://cantbeblank', 406 update_status: 'finished', 407 enabled: true, 408 last_update_at: nil, 409 updated_at: 25.hours.ago) 410 project = mirror.project 411 project.pending_delete = true 412 project.save! 413 mirror.reload 414 415 expect(mirror.sync).to be_nil 416 expect(mirror.valid?).to be_truthy 417 expect(mirror.update_status).to eq('finished') 418 end 419 end 420 421 describe '#disabled?' do 422 let_it_be(:project) { create(:project, :repository) } 423 424 subject { remote_mirror.disabled? } 425 426 context 'when disabled' do 427 let(:remote_mirror) { build(:remote_mirror, project: project, enabled: false) } 428 429 it { is_expected.to be_truthy } 430 end 431 432 context 'when enabled' do 433 let(:remote_mirror) { build(:remote_mirror, project: project, enabled: true) } 434 435 it { is_expected.to be_falsy } 436 end 437 end 438 439 def create_mirror(params) 440 project = FactoryBot.create(:project, :repository) 441 project.remote_mirrors.create!(params) 442 end 443end 444