1# frozen_string_literal: true 2 3require 'spec_helper' 4 5RSpec.describe Projects::UpdateService do 6 include ExternalAuthorizationServiceHelpers 7 include ProjectForksHelper 8 9 let(:user) { create(:user) } 10 let(:project) do 11 create(:project, creator: user, namespace: user.namespace) 12 end 13 14 describe '#execute' do 15 let(:admin) { create(:admin) } 16 17 context 'when changing visibility level' do 18 context 'when visibility_level changes to INTERNAL' do 19 it 'updates the project to internal' do 20 expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in) 21 22 result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) 23 24 expect(result).to eq({ status: :success }) 25 expect(project).to be_internal 26 end 27 end 28 29 context 'when visibility_level changes to PUBLIC' do 30 it 'updates the project to public' do 31 expect(TodosDestroyer::ProjectPrivateWorker).not_to receive(:perform_in) 32 33 result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) 34 35 expect(result).to eq({ status: :success }) 36 expect(project).to be_public 37 end 38 39 context 'and project is PRIVATE' do 40 it 'does not unlink project from fork network' do 41 expect(Projects::UnlinkForkService).not_to receive(:new) 42 43 update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) 44 end 45 end 46 end 47 48 context 'when visibility_level changes to PRIVATE' do 49 before do 50 project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC) 51 end 52 53 it 'updates the project to private' do 54 expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id) 55 expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id) 56 57 result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) 58 59 expect(result).to eq({ status: :success }) 60 expect(project).to be_private 61 end 62 end 63 64 context 'when visibility levels are restricted to PUBLIC only' do 65 before do 66 stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) 67 end 68 69 context 'when visibility_level is INTERNAL' do 70 it 'updates the project to internal' do 71 result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) 72 73 expect(result).to eq({ status: :success }) 74 expect(project).to be_internal 75 end 76 end 77 78 context 'when visibility_level is PUBLIC' do 79 it 'does not update the project to public' do 80 result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) 81 82 expect(result).to eq({ status: :error, message: 'New visibility level not allowed!' }) 83 expect(project).to be_private 84 end 85 86 context 'when updated by an admin' do 87 context 'when admin mode is enabled', :enable_admin_mode do 88 it 'updates the project to public' do 89 result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) 90 91 expect(result).to eq({ status: :success }) 92 expect(project).to be_public 93 end 94 end 95 96 context 'when admin mode is disabled' do 97 it 'does not update the project to public' do 98 result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) 99 100 expect(result).to eq({ status: :error, message: 'New visibility level not allowed!' }) 101 expect(project).to be_private 102 end 103 end 104 end 105 end 106 end 107 108 context 'when project visibility is higher than parent group' do 109 let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } 110 111 before do 112 project.update!(namespace: group, visibility_level: group.visibility_level) 113 end 114 115 it 'does not update project visibility level even if admin', :enable_admin_mode do 116 result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) 117 118 expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' }) 119 expect(project.reload).to be_internal 120 end 121 end 122 123 context 'when updating shared runners' do 124 context 'can enable shared runners' do 125 let(:group) { create(:group, shared_runners_enabled: true) } 126 let(:project) { create(:project, namespace: group, shared_runners_enabled: false) } 127 128 it 'enables shared runners' do 129 result = update_project(project, user, shared_runners_enabled: true) 130 131 expect(result).to eq({ status: :success }) 132 expect(project.reload.shared_runners_enabled).to be_truthy 133 end 134 end 135 136 context 'cannot enable shared runners' do 137 let(:group) { create(:group, :shared_runners_disabled) } 138 let(:project) { create(:project, namespace: group, shared_runners_enabled: false) } 139 140 it 'does not enable shared runners' do 141 result = update_project(project, user, shared_runners_enabled: true) 142 143 expect(result).to eq({ status: :error, message: 'Shared runners enabled cannot be enabled because parent group does not allow it' }) 144 expect(project.reload.shared_runners_enabled).to be_falsey 145 end 146 end 147 end 148 end 149 150 describe 'when updating project that has forks' do 151 let(:project) { create(:project, :internal) } 152 let(:user) { project.owner } 153 let(:forked_project) { fork_project(project) } 154 155 context 'and unlink forks feature flag is off' do 156 before do 157 stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false) 158 end 159 160 it 'updates forks visibility level when parent set to more restrictive' do 161 opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } 162 163 expect(project).to be_internal 164 expect(forked_project).to be_internal 165 166 expect(update_project(project, user, opts)).to eq({ status: :success }) 167 168 expect(project).to be_private 169 expect(forked_project.reload).to be_private 170 end 171 172 it 'does not update forks visibility level when parent set to less restrictive' do 173 opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } 174 175 expect(project).to be_internal 176 expect(forked_project).to be_internal 177 178 expect(update_project(project, user, opts)).to eq({ status: :success }) 179 180 expect(project).to be_public 181 expect(forked_project.reload).to be_internal 182 end 183 end 184 185 context 'and unlink forks feature flag is on' do 186 it 'does not change visibility of forks' do 187 opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } 188 189 expect(project).to be_internal 190 expect(forked_project).to be_internal 191 192 expect(update_project(project, user, opts)).to eq({ status: :success }) 193 194 expect(project).to be_private 195 expect(forked_project.reload).to be_internal 196 end 197 end 198 end 199 200 context 'when updating a default branch' do 201 let(:project) { create(:project, :repository) } 202 203 it 'changes default branch, tracking the previous branch' do 204 previous_default_branch = project.default_branch 205 206 update_project(project, admin, default_branch: 'feature') 207 208 project.reload 209 210 expect(project.default_branch).to eq('feature') 211 expect(project.previous_default_branch).to eq(previous_default_branch) 212 213 update_project(project, admin, default_branch: previous_default_branch) 214 215 project.reload 216 217 expect(project.default_branch).to eq(previous_default_branch) 218 expect(project.previous_default_branch).to eq('feature') 219 end 220 221 it 'does not change a default branch' do 222 # The branch 'unexisted-branch' does not exist. 223 update_project(project, admin, default_branch: 'unexisted-branch') 224 225 project.reload 226 227 expect(project.default_branch).to eq 'master' 228 expect(project.previous_default_branch).to be_nil 229 end 230 end 231 232 context 'when we update project but not enabling a wiki' do 233 it 'does not try to create an empty wiki' do 234 TestEnv.rm_storage_dir(project.repository_storage, project.wiki.path) 235 236 result = update_project(project, user, { name: 'test1' }) 237 238 expect(result).to eq({ status: :success }) 239 expect(project.wiki_repository_exists?).to be false 240 end 241 242 it 'handles empty project feature attributes' do 243 project.project_feature.update!(wiki_access_level: ProjectFeature::DISABLED) 244 245 result = update_project(project, user, { name: 'test1' }) 246 247 expect(result).to eq({ status: :success }) 248 expect(project.wiki_repository_exists?).to be false 249 end 250 end 251 252 context 'when enabling a wiki' do 253 it 'creates a wiki' do 254 project.project_feature.update!(wiki_access_level: ProjectFeature::DISABLED) 255 TestEnv.rm_storage_dir(project.repository_storage, project.wiki.path) 256 257 result = update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED }) 258 259 expect(result).to eq({ status: :success }) 260 expect(project.wiki_repository_exists?).to be true 261 expect(project.wiki_enabled?).to be true 262 end 263 264 it 'logs an error and creates a metric when wiki can not be created' do 265 project.project_feature.update!(wiki_access_level: ProjectFeature::DISABLED) 266 267 expect_any_instance_of(ProjectWiki).to receive(:wiki).and_raise(Wiki::CouldNotCreateWikiError) 268 expect_any_instance_of(described_class).to receive(:log_error).with("Could not create wiki for #{project.full_name}") 269 270 counter = double(:counter) 271 expect(Gitlab::Metrics).to receive(:counter).with(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki').and_return(counter) 272 expect(counter).to receive(:increment) 273 274 update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED }) 275 end 276 end 277 278 context 'when changing feature visibility to private' do 279 it 'updates the visibility correctly' do 280 expect(TodosDestroyer::PrivateFeaturesWorker) 281 .to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id) 282 283 result = update_project(project, user, project_feature_attributes: 284 { issues_access_level: ProjectFeature::PRIVATE } 285 ) 286 287 expect(result).to eq({ status: :success }) 288 expect(project.project_feature.issues_access_level).to be(ProjectFeature::PRIVATE) 289 end 290 end 291 292 context 'when updating a project that contains container images' do 293 before do 294 stub_container_registry_config(enabled: true) 295 stub_container_registry_tags(repository: /image/, tags: %w[rc1]) 296 create(:container_repository, project: project, name: :image) 297 end 298 299 it 'does not allow to rename the project' do 300 result = update_project(project, admin, path: 'renamed') 301 302 expect(result).to include(status: :error) 303 expect(result[:message]).to match(/contains container registry tags/) 304 end 305 306 it 'allows to update other settings' do 307 result = update_project(project, admin, public_builds: true) 308 309 expect(result[:status]).to eq :success 310 expect(project.reload.public_builds).to be true 311 end 312 end 313 314 context 'when renaming a project' do 315 let(:fake_repo_path) { File.join(TestEnv.repos_path, user.namespace.full_path, 'existing.git') } 316 317 context 'with legacy storage' do 318 let(:project) { create(:project, :legacy_storage, :repository, creator: user, namespace: user.namespace) } 319 320 before do 321 TestEnv.create_bare_repository(fake_repo_path) 322 end 323 324 after do 325 FileUtils.rm_rf(fake_repo_path) 326 end 327 328 it 'does not allow renaming when new path matches existing repository on disk' do 329 result = update_project(project, admin, path: 'existing') 330 331 expect(result).to include(status: :error) 332 expect(result[:message]).to match('There is already a repository with that name on disk') 333 expect(project).not_to be_valid 334 expect(project.errors.messages).to have_key(:base) 335 expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk') 336 end 337 338 context 'when hashed storage is enabled' do 339 before do 340 stub_application_setting(hashed_storage_enabled: true) 341 end 342 343 it 'migrates project to a hashed storage instead of renaming the repo to another legacy name' do 344 result = update_project(project, admin, path: 'new-path') 345 346 expect(result).not_to include(status: :error) 347 expect(project).to be_valid 348 expect(project.errors).to be_empty 349 expect(project.reload.hashed_storage?(:repository)).to be_truthy 350 end 351 end 352 end 353 354 context 'with hashed storage' do 355 let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } 356 357 before do 358 stub_application_setting(hashed_storage_enabled: true) 359 end 360 361 it 'does not check if new path matches existing repository on disk' do 362 expect(project).not_to receive(:repository_with_same_path_already_exists?) 363 364 result = update_project(project, admin, path: 'existing') 365 366 expect(result).to include(status: :success) 367 end 368 end 369 end 370 371 context 'when passing invalid parameters' do 372 it 'returns an error result when record cannot be updated' do 373 result = update_project(project, admin, { name: 'foo&bar' }) 374 375 expect(result).to eq({ 376 status: :error, 377 message: "Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces. It must start with a letter, digit, emoji, or '_'." 378 }) 379 end 380 end 381 382 shared_examples 'updating pages configuration' do 383 it 'schedules the `PagesUpdateConfigurationWorker` when pages are deployed' do 384 project.mark_pages_as_deployed 385 386 expect(PagesUpdateConfigurationWorker).to receive(:perform_async).with(project.id) 387 388 subject 389 end 390 391 it "does not schedule a job when pages aren't deployed" do 392 project.mark_pages_as_not_deployed 393 394 expect(PagesUpdateConfigurationWorker).not_to receive(:perform_async).with(project.id) 395 396 subject 397 end 398 end 399 400 context 'when updating #pages_https_only', :https_pages_enabled do 401 subject(:call_service) do 402 update_project(project, admin, pages_https_only: false) 403 end 404 405 it 'updates the attribute' do 406 expect { call_service } 407 .to change { project.pages_https_only? } 408 .to(false) 409 end 410 411 it_behaves_like 'updating pages configuration' 412 end 413 414 context 'when updating #pages_access_level' do 415 subject(:call_service) do 416 update_project(project, admin, project_feature_attributes: { pages_access_level: ProjectFeature::ENABLED }) 417 end 418 419 it 'updates the attribute' do 420 expect { call_service } 421 .to change { project.project_feature.pages_access_level } 422 .to(ProjectFeature::ENABLED) 423 end 424 425 it_behaves_like 'updating pages configuration' 426 end 427 428 context 'when updating #emails_disabled' do 429 it 'updates the attribute for the project owner' do 430 expect { update_project(project, user, emails_disabled: true) } 431 .to change { project.emails_disabled } 432 .to(true) 433 end 434 435 it 'does not update when not project owner' do 436 maintainer = create(:user) 437 project.add_user(maintainer, :maintainer) 438 439 expect { update_project(project, maintainer, emails_disabled: true) } 440 .not_to change { project.emails_disabled } 441 end 442 end 443 444 context 'when updating runners settings' do 445 let(:settings) do 446 { instance_runners_enabled: true, namespace_traversal_ids: [123] } 447 end 448 449 let!(:pending_build) do 450 create(:ci_pending_build, project: project, **settings) 451 end 452 453 context 'when project has shared runners enabled' do 454 let(:project) { create(:project, shared_runners_enabled: true) } 455 456 it 'updates builds queue when shared runners get disabled' do 457 expect { update_project(project, admin, shared_runners_enabled: false) } 458 .to change { pending_build.reload.instance_runners_enabled }.to(false) 459 460 expect(pending_build.reload.instance_runners_enabled).to be false 461 end 462 end 463 464 context 'when project has shared runners disabled' do 465 let(:project) { create(:project, shared_runners_enabled: false) } 466 467 it 'updates builds queue when shared runners get enabled' do 468 expect { update_project(project, admin, shared_runners_enabled: true) } 469 .to not_change { pending_build.reload.instance_runners_enabled } 470 471 expect(pending_build.reload.instance_runners_enabled).to be true 472 end 473 end 474 475 context 'when project has group runners enabled' do 476 let(:project) { create(:project, group_runners_enabled: true) } 477 478 before do 479 project.ci_cd_settings.update!(group_runners_enabled: true) 480 end 481 482 it 'updates builds queue when group runners get disabled' do 483 update_project(project, admin, group_runners_enabled: false) 484 485 expect(pending_build.reload.namespace_traversal_ids).to be_empty 486 end 487 end 488 489 context 'when project has group runners disabled' do 490 let(:project) { create(:project, :in_subgroup, group_runners_enabled: false) } 491 492 before do 493 project.reload.ci_cd_settings.update!(group_runners_enabled: false) 494 end 495 496 it 'updates builds queue when group runners get enabled' do 497 update_project(project, admin, group_runners_enabled: true) 498 499 expect(pending_build.reload.namespace_traversal_ids).to include(project.namespace.id) 500 end 501 end 502 end 503 504 context 'with external authorization enabled' do 505 before do 506 enable_external_authorization_service_check 507 508 allow(::Gitlab::ExternalAuthorization) 509 .to receive(:access_allowed?).with(user, 'default_label', project.full_path).and_call_original 510 end 511 512 it 'does not save the project with an error if the service denies access' do 513 expect(::Gitlab::ExternalAuthorization) 514 .to receive(:access_allowed?).with(user, 'new-label') { false } 515 516 result = update_project(project, user, { external_authorization_classification_label: 'new-label' }) 517 518 expect(result[:message]).to be_present 519 expect(result[:status]).to eq(:error) 520 end 521 522 it 'saves the new label if the service allows access' do 523 expect(::Gitlab::ExternalAuthorization) 524 .to receive(:access_allowed?).with(user, 'new-label') { true } 525 526 result = update_project(project, user, { external_authorization_classification_label: 'new-label' }) 527 528 expect(result[:status]).to eq(:success) 529 expect(project.reload.external_authorization_classification_label).to eq('new-label') 530 end 531 532 it 'checks the default label when the classification label was cleared' do 533 expect(::Gitlab::ExternalAuthorization) 534 .to receive(:access_allowed?).with(user, 'default_label') { true } 535 536 update_project(project, user, { external_authorization_classification_label: '' }) 537 end 538 539 it 'does not check the label when it does not change' do 540 expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).once 541 542 update_project(project, user, { name: 'New name' }) 543 end 544 end 545 546 context 'when updating nested attributes for prometheus integration' do 547 context 'prometheus integration exists' do 548 let(:prometheus_integration_attributes) do 549 attributes_for(:prometheus_integration, 550 project: project, 551 properties: { api_url: "http://new.prometheus.com", manual_configuration: "0" } 552 ) 553 end 554 555 let!(:prometheus_integration) do 556 create(:prometheus_integration, 557 project: project, 558 properties: { api_url: "http://old.prometheus.com", manual_configuration: "0" } 559 ) 560 end 561 562 it 'updates existing record' do 563 expect { update_project(project, user, prometheus_integration_attributes: prometheus_integration_attributes) } 564 .to change { prometheus_integration.reload.api_url } 565 .from("http://old.prometheus.com") 566 .to("http://new.prometheus.com") 567 end 568 end 569 570 context 'prometheus integration does not exist' do 571 context 'valid parameters' do 572 let(:prometheus_integration_attributes) do 573 attributes_for(:prometheus_integration, 574 project: project, 575 properties: { api_url: "http://example.prometheus.com", manual_configuration: "0" } 576 ) 577 end 578 579 it 'creates new record' do 580 expect { update_project(project, user, prometheus_integration_attributes: prometheus_integration_attributes) } 581 .to change { ::Integrations::Prometheus.where(project: project).count } 582 .from(0) 583 .to(1) 584 end 585 end 586 587 context 'invalid parameters' do 588 let(:prometheus_integration_attributes) do 589 attributes_for(:prometheus_integration, 590 project: project, 591 properties: { api_url: nil, manual_configuration: "1" } 592 ) 593 end 594 595 it 'does not create new record' do 596 expect { update_project(project, user, prometheus_integration_attributes: prometheus_integration_attributes) } 597 .not_to change { ::Integrations::Prometheus.where(project: project).count } 598 end 599 end 600 end 601 end 602 603 describe 'when changing repository_storage' do 604 let(:repository_read_only) { false } 605 let(:project) { create(:project, :repository, repository_read_only: repository_read_only) } 606 let(:opts) { { repository_storage: 'test_second_storage' } } 607 608 before do 609 stub_storage_settings('test_second_storage' => { 'path' => 'tmp/tests/extra_storage' }) 610 end 611 612 shared_examples 'the transfer was not scheduled' do 613 it 'does not schedule the transfer' do 614 expect do 615 update_project(project, user, opts) 616 end.not_to change(project.repository_storage_moves, :count) 617 end 618 end 619 620 context 'authenticated as admin' do 621 let(:user) { create(:admin) } 622 623 context 'when admin mode is enabled', :enable_admin_mode do 624 it 'schedules the transfer of the repository to the new storage and locks the project' do 625 update_project(project, admin, opts) 626 627 expect(project).to be_repository_read_only 628 expect(project.repository_storage_moves.last).to have_attributes( 629 state: ::Projects::RepositoryStorageMove.state_machines[:state].states[:scheduled].value, 630 source_storage_name: 'default', 631 destination_storage_name: 'test_second_storage' 632 ) 633 end 634 end 635 636 context 'when admin mode is disabled' do 637 it_behaves_like 'the transfer was not scheduled' 638 end 639 640 context 'the repository is read-only' do 641 let(:repository_read_only) { true } 642 643 it_behaves_like 'the transfer was not scheduled' 644 end 645 646 context 'the storage has not changed' do 647 let(:opts) { { repository_storage: 'default' } } 648 649 it_behaves_like 'the transfer was not scheduled' 650 end 651 652 context 'the storage does not exist' do 653 let(:opts) { { repository_storage: 'nonexistent' } } 654 655 it_behaves_like 'the transfer was not scheduled' 656 end 657 end 658 659 context 'authenticated as user' do 660 let(:user) { create(:user) } 661 662 it_behaves_like 'the transfer was not scheduled' 663 end 664 end 665 666 describe 'when updating topics' do 667 let(:project) { create(:project, topic_list: 'topic1, topic2') } 668 669 it 'update using topics' do 670 result = update_project(project, user, { topics: 'topics' }) 671 672 expect(result[:status]).to eq(:success) 673 expect(project.topic_list).to eq(%w[topics]) 674 end 675 676 it 'update using topic_list' do 677 result = update_project(project, user, { topic_list: 'topic_list' }) 678 679 expect(result[:status]).to eq(:success) 680 expect(project.topic_list).to eq(%w[topic_list]) 681 end 682 683 it 'update using tag_list (deprecated)' do 684 result = update_project(project, user, { tag_list: 'tag_list' }) 685 686 expect(result[:status]).to eq(:success) 687 expect(project.topic_list).to eq(%w[tag_list]) 688 end 689 end 690 end 691 692 describe '#run_auto_devops_pipeline?' do 693 subject { described_class.new(project, user).run_auto_devops_pipeline? } 694 695 context 'when master contains a .gitlab-ci.yml file' do 696 before do 697 allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']") 698 end 699 700 it { is_expected.to eq(false) } 701 end 702 703 context 'when auto devops is nil' do 704 it { is_expected.to eq(false) } 705 end 706 707 context 'when auto devops is explicitly enabled' do 708 before do 709 project.create_auto_devops!(enabled: true) 710 end 711 712 it { is_expected.to eq(true) } 713 end 714 715 context 'when auto devops is explicitly disabled' do 716 before do 717 project.create_auto_devops!(enabled: false) 718 end 719 720 it { is_expected.to eq(false) } 721 end 722 723 context 'when auto devops is set to instance setting' do 724 before do 725 project.create_auto_devops!(enabled: nil) 726 project.reload 727 728 allow(project.auto_devops).to receive(:previous_changes).and_return('enabled' => true) 729 end 730 731 context 'when auto devops is enabled system-wide' do 732 before do 733 stub_application_setting(auto_devops_enabled: true) 734 end 735 736 it { is_expected.to eq(true) } 737 end 738 739 context 'when auto devops is disabled system-wide' do 740 before do 741 stub_application_setting(auto_devops_enabled: false) 742 end 743 744 it { is_expected.to eq(false) } 745 end 746 end 747 end 748 749 def update_project(project, user, opts) 750 described_class.new(project, user, opts).execute 751 end 752end 753