project_spec.rb 142 KB
Newer Older
gitlabhq's avatar
gitlabhq committed
1 2
require 'spec_helper'

3
describe Project do
4
  include ProjectForksHelper
5
  include GitHelpers
6

Shinya Maeda's avatar
Shinya Maeda committed
7
  it_behaves_like 'having unique enum values'
Shinya Maeda's avatar
Shinya Maeda committed
8

9
  describe 'associations' do
10 11 12
    it { is_expected.to belong_to(:group) }
    it { is_expected.to belong_to(:namespace) }
    it { is_expected.to belong_to(:creator).class_name('User') }
13
    it { is_expected.to belong_to(:pool_repository) }
14
    it { is_expected.to have_many(:users) }
ubudzisz's avatar
ubudzisz committed
15
    it { is_expected.to have_many(:services) }
16 17 18 19 20
    it { is_expected.to have_many(:events) }
    it { is_expected.to have_many(:merge_requests) }
    it { is_expected.to have_many(:issues) }
    it { is_expected.to have_many(:milestones) }
    it { is_expected.to have_many(:project_members).dependent(:delete_all) }
21
    it { is_expected.to have_many(:users).through(:project_members) }
22 23 24 25
    it { is_expected.to have_many(:requesters).dependent(:delete_all) }
    it { is_expected.to have_many(:notes) }
    it { is_expected.to have_many(:snippets).class_name('ProjectSnippet') }
    it { is_expected.to have_many(:deploy_keys_projects) }
26
    it { is_expected.to have_many(:deploy_keys) }
27 28 29 30 31
    it { is_expected.to have_many(:hooks) }
    it { is_expected.to have_many(:protected_branches) }
    it { is_expected.to have_one(:slack_service) }
    it { is_expected.to have_one(:microsoft_teams_service) }
    it { is_expected.to have_one(:mattermost_service) }
32
    it { is_expected.to have_one(:hangouts_chat_service) }
Matt Coleman's avatar
Matt Coleman committed
33
    it { is_expected.to have_one(:packagist_service) }
34 35 36 37
    it { is_expected.to have_one(:pushover_service) }
    it { is_expected.to have_one(:asana_service) }
    it { is_expected.to have_many(:boards) }
    it { is_expected.to have_one(:campfire_service) }
blackst0ne's avatar
blackst0ne committed
38
    it { is_expected.to have_one(:discord_service) }
39 40 41 42 43 44 45 46 47 48 49 50 51 52
    it { is_expected.to have_one(:drone_ci_service) }
    it { is_expected.to have_one(:emails_on_push_service) }
    it { is_expected.to have_one(:pipelines_email_service) }
    it { is_expected.to have_one(:irker_service) }
    it { is_expected.to have_one(:pivotaltracker_service) }
    it { is_expected.to have_one(:flowdock_service) }
    it { is_expected.to have_one(:assembla_service) }
    it { is_expected.to have_one(:slack_slash_commands_service) }
    it { is_expected.to have_one(:mattermost_slash_commands_service) }
    it { is_expected.to have_one(:buildkite_service) }
    it { is_expected.to have_one(:bamboo_service) }
    it { is_expected.to have_one(:teamcity_service) }
    it { is_expected.to have_one(:jira_service) }
    it { is_expected.to have_one(:redmine_service) }
53
    it { is_expected.to have_one(:youtrack_service) }
54 55 56 57 58
    it { is_expected.to have_one(:custom_issue_tracker_service) }
    it { is_expected.to have_one(:bugzilla_service) }
    it { is_expected.to have_one(:gitlab_issue_tracker_service) }
    it { is_expected.to have_one(:external_wiki_service) }
    it { is_expected.to have_one(:project_feature) }
59
    it { is_expected.to have_one(:project_repository) }
60 61
    it { is_expected.to have_one(:statistics).class_name('ProjectStatistics') }
    it { is_expected.to have_one(:import_data).class_name('ProjectImportData') }
ubudzisz's avatar
ubudzisz committed
62
    it { is_expected.to have_one(:last_event).class_name('Event') }
63
    it { is_expected.to have_one(:forked_from_project).through(:fork_network_member) }
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
64
    it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
65
    it { is_expected.to have_one(:error_tracking_setting).class_name('ErrorTracking::ProjectErrorTrackingSetting') }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
66
    it { is_expected.to have_many(:commit_statuses) }
67
    it { is_expected.to have_many(:ci_pipelines) }
68
    it { is_expected.to have_many(:builds) }
69
    it { is_expected.to have_many(:build_trace_section_names)}
70 71 72 73
    it { is_expected.to have_many(:runner_projects) }
    it { is_expected.to have_many(:runners) }
    it { is_expected.to have_many(:variables) }
    it { is_expected.to have_many(:triggers) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
74
    it { is_expected.to have_many(:pages_domains) }
75 76
    it { is_expected.to have_many(:labels).class_name('ProjectLabel') }
    it { is_expected.to have_many(:users_star_projects) }
77
    it { is_expected.to have_many(:repository_languages) }
78 79 80 81 82 83 84
    it { is_expected.to have_many(:environments) }
    it { is_expected.to have_many(:deployments) }
    it { is_expected.to have_many(:todos) }
    it { is_expected.to have_many(:releases) }
    it { is_expected.to have_many(:lfs_objects_projects) }
    it { is_expected.to have_many(:project_group_links) }
    it { is_expected.to have_many(:notification_settings).dependent(:delete_all) }
85 86
    it { is_expected.to have_many(:forked_to_members).class_name('ForkNetworkMember') }
    it { is_expected.to have_many(:forks).through(:forked_to_members) }
Jan Provaznik's avatar
Jan Provaznik committed
87
    it { is_expected.to have_many(:uploads) }
88
    it { is_expected.to have_many(:pipeline_schedules) }
89
    it { is_expected.to have_many(:members_and_requesters) }
90
    it { is_expected.to have_many(:clusters) }
91
    it { is_expected.to have_many(:kubernetes_namespaces) }
92
    it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
93
    it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
94
    it { is_expected.to have_many(:lfs_file_locks) }
Mayra Cabrera's avatar
Mayra Cabrera committed
95 96
    it { is_expected.to have_many(:project_deploy_tokens) }
    it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
97

98 99 100 101
    it 'has an inverse relationship with merge requests' do
      expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
    end

102 103
    context 'after initialized' do
      it "has a project_feature" do
104
        expect(described_class.new.project_feature).to be_present
105 106 107
      end
    end

108 109 110 111 112 113 114 115 116
    context 'when creating a new project' do
      it 'automatically creates a CI/CD settings row' do
        project = create(:project)

        expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
        expect(project.ci_cd_settings).to be_persisted
      end
    end

117 118 119 120
    context 'updating cd_cd_settings' do
      it 'does not raise an error' do
        project = create(:project)

James Lopez's avatar
James Lopez committed
121
        expect { project.update(ci_cd_settings: nil) }.not_to raise_exception
122 123 124
      end
    end

125
    describe '#members & #requesters' do
126
      let(:project) { create(:project, :public, :access_requestable) }
127 128 129 130
      let(:requester) { create(:user) }
      let(:developer) { create(:user) }
      before do
        project.request_access(requester)
131
        project.add_developer(developer)
132 133
      end

134 135
      it_behaves_like 'members and requesters associations' do
        let(:namespace) { project }
136 137
      end
    end
138 139 140 141 142

    describe '#boards' do
      it 'raises an error when attempting to add more than one board to the project' do
        subject.boards.build

143
        expect { subject.boards.build }.to raise_error(Project::BoardLimitExceeded, 'Number of permitted boards exceeded')
144 145 146
        expect(subject.boards.size).to eq 1
      end
    end
147 148

    describe 'ci_pipelines association' do
149 150
      it 'returns only pipelines from ci_sources' do
        expect(Ci::Pipeline).to receive(:ci_sources).and_call_original
151

152
        subject.ci_pipelines
153 154
      end
    end
gitlabhq's avatar
gitlabhq committed
155 156
  end

157 158 159 160 161 162 163 164
  describe 'modules' do
    subject { described_class }

    it { is_expected.to include_module(Gitlab::ConfigHelper) }
    it { is_expected.to include_module(Gitlab::ShellAdapter) }
    it { is_expected.to include_module(Gitlab::VisibilityLevel) }
    it { is_expected.to include_module(Referable) }
    it { is_expected.to include_module(Sortable) }
165 166
  end

167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
  describe '.missing_kubernetes_namespace' do
    let!(:project) { create(:project) }
    let!(:cluster) { create(:cluster, :provided_by_user, :group) }
    let(:kubernetes_namespaces) { project.kubernetes_namespaces }

    subject { described_class.missing_kubernetes_namespace(kubernetes_namespaces) }

    it { is_expected.to contain_exactly(project) }

    context 'kubernetes namespace exists' do
      before do
        create(:cluster_kubernetes_namespace, project: project, cluster: cluster)
      end

      it { is_expected.to be_empty }
    end
  end

185
  describe 'validation' do
186
    let!(:project) { create(:project) }
187

188 189
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
190
    it { is_expected.to validate_length_of(:name).is_at_most(255) }
191
    it { is_expected.to validate_presence_of(:path) }
192 193
    it { is_expected.to validate_length_of(:path).is_at_most(255) }
    it { is_expected.to validate_length_of(:description).is_at_most(2000) }
194 195 196
    it { is_expected.to validate_length_of(:ci_config_path).is_at_most(255) }
    it { is_expected.to allow_value('').for(:ci_config_path) }
    it { is_expected.not_to allow_value('test/../foo').for(:ci_config_path) }
197
    it { is_expected.not_to allow_value('/test/foo').for(:ci_config_path) }
198 199
    it { is_expected.to validate_presence_of(:creator) }
    it { is_expected.to validate_presence_of(:namespace) }
200
    it { is_expected.to validate_presence_of(:repository_storage) }
201

202 203 204 205 206 207 208 209
    it 'validates build timeout constraints' do
      is_expected.to validate_numericality_of(:build_timeout)
        .only_integer
        .is_greater_than_or_equal_to(10.minutes)
        .is_less_than(1.month)
        .with_message('needs to be beetween 10 minutes and 1 month')
    end

210
    it 'does not allow new projects beyond user limits' do
211
      project2 = build(:project)
212 213 214 215 216 217 218

      allow(project2)
        .to receive(:creator)
        .and_return(
          double(can_create_project?: false, projects_limit: 0).as_null_object
        )

219
      expect(project2).not_to be_valid
220
    end
221 222 223

    describe 'wiki path conflict' do
      context "when the new path has been used by the wiki of other Project" do
224
        it 'has an error on the name attribute' do
225
          new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
226 227 228 229 230 231 232

          expect(new_project).not_to be_valid
          expect(new_project.errors[:name].first).to eq('has already been taken')
        end
      end

      context "when the new wiki path has been used by the path of other Project" do
233
        it 'has an error on the name attribute' do
234 235
          project_with_wiki_suffix = create(:project, path: 'foo.wiki')
          new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
236 237 238 239 240 241

          expect(new_project).not_to be_valid
          expect(new_project.errors[:name].first).to eq('has already been taken')
        end
      end
    end
242

243
    context 'repository storages inclusion' do
244
      let(:project2) { build(:project, repository_storage: 'missing') }
245 246

      before do
247
        storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
248 249 250
        allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
      end

251
      it "does not allow repository storages that don't match a label in the configuration" do
252 253 254 255
        expect(project2).not_to be_valid
        expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
      end
    end
256

257 258 259
    describe 'import_url' do
      it 'does not allow an invalid URI as import_url' do
        project = build(:project, import_url: 'invalid://')
James Lopez's avatar
James Lopez committed
260

261 262
        expect(project).not_to be_valid
      end
263

264 265 266
      it 'does allow a SSH URI as import_url for persisted projects' do
        project = create(:project)
        project.import_url = 'ssh://test@gitlab.com/project.git'
267

268 269
        expect(project).to be_valid
      end
270

271 272
      it 'does not allow a SSH URI as import_url for new projects' do
        project = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
273

274 275
        expect(project).not_to be_valid
      end
James Lopez's avatar
James Lopez committed
276

277 278
      it 'does allow a valid URI as import_url' do
        project = build(:project, import_url: 'http://gitlab.com/project.git')
James Lopez's avatar
James Lopez committed
279

280 281
        expect(project).to be_valid
      end
282

283 284
      it 'allows an empty URI' do
        project = build(:project, import_url: '')
285

286 287
        expect(project).to be_valid
      end
288

289 290
      it 'does not produce import data on an empty URI' do
        project = build(:project, import_url: '')
291

292 293
        expect(project.import_data).to be_nil
      end
294

295 296
      it 'does not produce import data on an invalid URI' do
        project = build(:project, import_url: 'test://')
297

298 299
        expect(project.import_data).to be_nil
      end
300

301 302
      it "does not allow import_url pointing to localhost" do
        project = build(:project, import_url: 'http://localhost:9000/t.git')
303

304 305 306
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed')
      end
307

308 309 310 311 312 313 314
      it 'does not allow import_url pointing to the local network' do
        project = build(:project, import_url: 'https://192.168.1.1')

        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Requests to the local network are not allowed')
      end

315 316
      it "does not allow import_url with invalid ports for new projects" do
        project = build(:project, import_url: 'http://github.com:25/t.git')
317

318 319 320
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443')
      end
321

322 323 324
      it "does not allow import_url with invalid ports for persisted projects" do
        project = create(:project)
        project.import_url = 'http://github.com:25/t.git'
325

326 327 328
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
      end
329

330 331
      it "does not allow import_url with invalid user" do
        project = build(:project, import_url: 'http://$user:password@github.com/t.git')
332

333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Username needs to start with an alphanumeric character')
      end

      include_context 'invalid urls'

      it 'does not allow urls with CR or LF characters' do
        project = build(:project)

        aggregate_failures do
          urls_with_CRLF.each do |url|
            project.import_url = url

            expect(project).not_to be_valid
            expect(project.errors.full_messages.first).to match(/is blocked: URI is invalid/)
          end
        end
      end
351 352
    end

353 354
    describe 'project pending deletion' do
      let!(:project_pending_deletion) do
355
        create(:project,
356 357 358
               pending_delete: true)
      end
      let(:new_project) do
359
        build(:project,
360 361 362 363 364 365 366 367 368 369 370 371
              name: project_pending_deletion.name,
              namespace: project_pending_deletion.namespace)
      end

      before do
        new_project.validate
      end

      it 'contains errors related to the project being deleted' do
        expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.')
      end
    end
372 373 374

    describe 'path validation' do
      it 'allows paths reserved on the root namespace' do
375
        project = build(:project, path: 'api')
376 377 378 379 380

        expect(project).to be_valid
      end

      it 'rejects paths reserved on another level' do
381
        project = build(:project, path: 'tree')
382 383 384

        expect(project).not_to be_valid
      end
385 386 387

      it 'rejects nested paths' do
        parent = create(:group, :nested, path: 'environments')
388
        project = build(:project, path: 'folders', namespace: parent)
389 390 391

        expect(project).not_to be_valid
      end
392 393 394

      it 'allows a reserved group name' do
        parent = create(:group)
395
        project = build(:project, path: 'avatar', namespace: parent)
396 397 398

        expect(project).to be_valid
      end
399 400 401 402 403 404

      it 'allows a path ending in a period' do
        project = build(:project, path: 'foo.')

        expect(project).to be_valid
      end
405
    end
gitlabhq's avatar
gitlabhq committed
406
  end
407

408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431
  describe '#all_pipelines' do
    let(:project) { create(:project) }

    before do
      create(:ci_pipeline, project: project, ref: 'master', source: :web)
      create(:ci_pipeline, project: project, ref: 'master', source: :external)
    end

    it 'has all pipelines' do
      expect(project.all_pipelines.size).to eq(2)
    end

    context 'when builds are disabled' do
      before do
        project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
      end

      it 'should return .external pipelines' do
        expect(project.all_pipelines).to all(have_attributes(source: 'external'))
        expect(project.all_pipelines.size).to eq(1)
      end
    end
  end

432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
  describe '#ci_pipelines' do
    let(:project) { create(:project) }

    before do
      create(:ci_pipeline, project: project, ref: 'master', source: :web)
      create(:ci_pipeline, project: project, ref: 'master', source: :external)
    end

    it 'has ci pipelines' do
      expect(project.ci_pipelines.size).to eq(2)
    end

    context 'when builds are disabled' do
      before do
        project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
      end

      it 'should return .external pipelines' do
        expect(project.ci_pipelines).to all(have_attributes(source: 'external'))
        expect(project.ci_pipelines.size).to eq(1)
      end
    end
  end

456
  describe 'project token' do
457
    it 'sets an random token if none provided' do
458
      project = FactoryBot.create(:project, runners_token: '')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
459
      expect(project.runners_token).not_to eq('')
460 461
    end

ubudzisz's avatar
ubudzisz committed
462
    it 'does not set an random token if one provided' do
463
      project = FactoryBot.create(:project, runners_token: 'my-token')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
464
      expect(project.runners_token).to eq('my-token')
465 466
    end
  end
gitlabhq's avatar
gitlabhq committed
467

468
  describe 'Respond to' do
469 470 471 472 473
    it { is_expected.to respond_to(:url_to_repo) }
    it { is_expected.to respond_to(:repo_exists?) }
    it { is_expected.to respond_to(:execute_hooks) }
    it { is_expected.to respond_to(:owner) }
    it { is_expected.to respond_to(:path_with_namespace) }
474
    it { is_expected.to respond_to(:full_path) }
gitlabhq's avatar
gitlabhq committed
475 476
  end

477
  describe 'delegation' do
478
    [:add_guest, :add_reporter, :add_developer, :add_maintainer, :add_user, :add_users].each do |method|
479 480 481 482 483
      it { is_expected.to delegate_method(method).to(:team) }
    end

    it { is_expected.to delegate_method(:members).to(:team).with_prefix(true) }
    it { is_expected.to delegate_method(:name).to(:owner).with_prefix(true).with_arguments(allow_nil: true) }
484 485
    it { is_expected.to delegate_method(:group_clusters_enabled?).to(:group).with_arguments(allow_nil: true) }
    it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
486
    it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
487 488
  end

489 490 491 492 493 494 495 496 497
  describe '#to_reference_with_postfix' do
    it 'returns the full path with reference_postfix' do
      namespace = create(:namespace, path: 'sample-namespace')
      project = create(:project, path: 'sample-project', namespace: namespace)

      expect(project.to_reference_with_postfix).to eq 'sample-namespace/sample-project>'
    end
  end

498
  describe '#to_reference' do
499
    let(:owner)     { create(:user, name: 'Gitlab') }
500
    let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
501
    let(:project)   { create(:project, path: 'sample-project', namespace: namespace) }
502
    let(:group)     { create(:group, name: 'Group', path: 'sample-group') }
503

504
    context 'when nil argument' do
505 506 507 508 509
      it 'returns nil' do
        expect(project.to_reference).to be_nil
      end
    end

510
    context 'when full is true' do
511
      it 'returns complete path to the project' do
512 513 514
        expect(project.to_reference(full: true)).to          eq 'sample-namespace/sample-project'
        expect(project.to_reference(project, full: true)).to eq 'sample-namespace/sample-project'
        expect(project.to_reference(group, full: true)).to   eq 'sample-namespace/sample-project'
515 516 517 518 519 520 521 522 523 524
      end
    end

    context 'when same project argument' do
      it 'returns nil' do
        expect(project.to_reference(project)).to be_nil
      end
    end

    context 'when cross namespace project argument' do
525
      let(:another_namespace_project) { create(:project, name: 'another-project') }
526 527 528 529 530 531 532

      it 'returns complete path to the project' do
        expect(project.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project'
      end
    end

    context 'when same namespace / cross-project argument' do
533
      let(:another_project) { create(:project, namespace: namespace) }
534

535
      it 'returns path to the project' do
536 537 538
        expect(project.to_reference(another_project)).to eq 'sample-project'
      end
    end
539

540 541
    context 'when different namespace / cross-project argument' do
      let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
542
      let(:another_project)   { create(:project, path: 'another-project', namespace: another_namespace) }
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559

      it 'returns full path to the project' do
        expect(project.to_reference(another_project)).to eq 'sample-namespace/sample-project'
      end
    end

    context 'when argument is a namespace' do
      context 'with same project path' do
        it 'returns path to the project' do
          expect(project.to_reference(namespace)).to eq 'sample-project'
        end
      end

      context 'with different project path' do
        it 'returns full path to the project' do
          expect(project.to_reference(group)).to eq 'sample-namespace/sample-project'
        end
560 561
      end
    end
562 563 564 565 566
  end

  describe '#to_human_reference' do
    let(:owner) { create(:user, name: 'Gitlab') }
    let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
567
    let(:project) { create(:project, name: 'Sample project', namespace: namespace) }
568 569 570 571 572 573 574 575 576 577 578 579 580 581

    context 'when nil argument' do
      it 'returns nil' do
        expect(project.to_human_reference).to be_nil
      end
    end

    context 'when same project argument' do
      it 'returns nil' do
        expect(project.to_human_reference(project)).to be_nil
      end
    end

    context 'when cross namespace project argument' do
582
      let(:another_namespace_project) { create(:project, name: 'another-project') }
583 584 585 586 587 588 589

      it 'returns complete name with namespace of the project' do
        expect(project.to_human_reference(another_namespace_project)).to eq 'Gitlab / Sample project'
      end
    end

    context 'when same namespace / cross-project argument' do
590
      let(:another_project) { create(:project, namespace: namespace) }
591 592 593 594

      it 'returns name of the project' do
        expect(project.to_human_reference(another_project)).to eq 'Sample project'
      end
595 596 597
    end
  end

598
  describe '#merge_method' do
599 600 601 602 603 604 605
    using RSpec::Parameterized::TableSyntax

    where(:ff, :rebase, :method) do
      true  | true  | :ff
      true  | false | :ff
      false | true  | :rebase_merge
      false | false | :merge
606 607
    end

608 609 610 611 612 613
    with_them do
      let(:project) { build(:project, merge_requests_rebase_enabled: rebase, merge_requests_ff_only_enabled: ff) }

      subject { project.merge_method }

      it { is_expected.to eq(method) }
614 615 616
    end
  end

617
  it 'returns valid url to repo' do
618
    project = described_class.new(path: 'somewhere')
619
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
620 621
  end

Douwe Maan's avatar
Douwe Maan committed
622
  describe "#web_url" do
623
    let(:project) { create(:project, path: "somewhere") }
Douwe Maan's avatar
Douwe Maan committed
624 625

    it 'returns the full web URL for this repo' do
626
      expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere")
Douwe Maan's avatar
Douwe Maan committed
627
    end
628 629
  end

630 631
  describe "#readme_url" do
    context 'with a non-existing repository' do
632
      let(:project) { create(:project) }
633

634
      it 'returns nil' do
635 636 637 638 639 640
        expect(project.readme_url).to be_nil
      end
    end

    context 'with an existing repository' do
      context 'when no README exists' do
641
        let(:project) { create(:project, :empty_repo) }
642

643
        it 'returns nil' do
644 645 646 647 648
          expect(project.readme_url).to be_nil
        end
      end

      context 'when a README exists' do
649 650
        let(:project) { create(:project, :repository) }

651
        it 'returns the README' do
652
          expect(project.readme_url).to eq("#{project.web_url}/blob/master/README.md")
653 654 655 656 657
        end
      end
    end
  end

658
  describe "#new_issuable_address" do
659
    let(:project) { create(:project, path: "somewhere") }
660 661
    let(:user) { create(:user) }

662 663 664 665 666 667
    context 'incoming email enabled' do
      before do
        stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
      end

      it 'returns the address to create a new issue' do
668
        address = "p+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-issue@gl.ab"
669

670 671 672 673
        expect(project.new_issuable_address(user, 'issue')).to eq(address)
      end

      it 'returns the address to create a new merge request' do
674
        address = "p+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-merge-request@gl.ab"
675 676

        expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
677
      end
678 679 680 681

      it 'returns nil with invalid address type' do
        expect(project.new_issuable_address(user, 'invalid_param')).to be_nil
      end
682 683 684 685 686 687
    end

    context 'incoming email disabled' do
      before do
        stub_incoming_email_setting(enabled: false)
      end
688

689
      it 'returns nil' do
690 691 692 693 694
        expect(project.new_issuable_address(user, 'issue')).to be_nil
      end

      it 'returns nil' do
        expect(project.new_issuable_address(user, 'merge_request')).to be_nil
695
      end
696 697 698
    end
  end

699
  describe 'last_activity methods' do
700 701
    let(:timestamp) { 2.hours.ago }
    # last_activity_at gets set to created_at upon creation
702
    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
703

704
    describe 'last_activity' do
705
      it 'alias last_activity to last_event' do
706
        last_event = create(:event, :closed, project: project)
707

708
        expect(project.last_activity).to eq(last_event)
709
      end
gitlabhq's avatar
gitlabhq committed
710 711
    end

712 713
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
714
        new_event = create(:event, :closed, project: project, created_at: Time.now)
715

716
        project.reload
717
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
718
      end
719

720
      it 'returns the project\'s last update date if it has no events' do
721
        expect(project.last_activity_date).to eq(project.updated_at)
722
      end
723 724

      it 'returns the most recent timestamp' do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
725 726 727
        project.update(updated_at: nil,
                       last_activity_at: timestamp,
                       last_repository_updated_at: timestamp - 1.hour)
728

729
        expect(project.last_activity_date).to be_like_time(timestamp)
730

Lin Jen-Shin's avatar
Lin Jen-Shin committed
731 732 733
        project.update(updated_at: timestamp,
                       last_activity_at: timestamp - 1.hour,
                       last_repository_updated_at: nil)
734

735
        expect(project.last_activity_date).to be_like_time(timestamp)
736
      end
737 738
    end
  end
739

740
  describe '#get_issue' do
741
    let(:project) { create(:project) }
742
    let!(:issue)  { create(:issue, project: project) }
743 744 745
    let(:user)    { create(:user) }

    before do
746
      project.add_developer(user)
747
    end
748 749 750

    context 'with default issues tracker' do
      it 'returns an issue' do
751
        expect(project.get_issue(issue.iid, user)).to eq issue
752 753
      end

754 755 756 757
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      end

758
      it 'returns nil when no issue found' do
759 760 761 762 763 764
        expect(project.get_issue(999, user)).to be_nil
      end

      it "returns nil when user doesn't have access" do
        user = create(:user)
        expect(project.get_issue(issue.iid, user)).to eq nil
765 766 767 768
      end
    end

    context 'with external issues tracker' do
769
      let!(:internal_issue) { create(:issue, project: project) }
770
      before do
771
        allow(project).to receive(:external_issue_tracker).and_return(true)
772 773
      end

774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
      context 'when internal issues are enabled' do
        it 'returns interlan issue' do
          issue = project.get_issue(internal_issue.iid, user)

          expect(issue).to be_kind_of(Issue)
          expect(issue.iid).to eq(internal_issue.iid)
          expect(issue.project).to eq(project)
        end

        it 'returns an ExternalIssue when internal issue does not exists' do
          issue = project.get_issue('FOO-1234', user)

          expect(issue).to be_kind_of(ExternalIssue)
          expect(issue.iid).to eq('FOO-1234')
          expect(issue.project).to eq(project)
        end
      end

      context 'when internal issues are disabled' do
        before do
          project.issues_enabled = false
          project.save!
        end

        it 'returns always an External issues' do
          issue = project.get_issue(internal_issue.iid, user)
          expect(issue).to be_kind_of(ExternalIssue)
          expect(issue.iid).to eq(internal_issue.iid.to_s)
          expect(issue.project).to eq(project)
        end

        it 'returns an ExternalIssue when internal issue does not exists' do
          issue = project.get_issue('FOO-1234', user)
          expect(issue).to be_kind_of(ExternalIssue)
          expect(issue.iid).to eq('FOO-1234')
          expect(issue.project).to eq(project)
        end
811 812 813 814 815
      end
    end
  end

  describe '#issue_exists?' do
816
    let(:project) { create(:project) }
817 818 819 820 821 822 823 824 825 826 827 828

    it 'is truthy when issue exists' do
      expect(project).to receive(:get_issue).and_return(double)
      expect(project.issue_exists?(1)).to be_truthy
    end

    it 'is falsey when issue does not exist' do
      expect(project).to receive(:get_issue).and_return(nil)
      expect(project.issue_exists?(1)).to be_falsey
    end
  end

829
  describe '#to_param' do
830 831
    context 'with namespace' do
      before do
832
        @group = create(:group, name: 'gitlab')
833
        @project = create(:project, name: 'gitlabhq', namespace: @group)
834 835
      end

Vinnie Okada's avatar
Vinnie Okada committed
836
      it { expect(@project.to_param).to eq('gitlabhq') }
837
    end
838 839 840

    context 'with invalid path' do
      it 'returns previous path to keep project suitable for use in URLs when persisted' do
841
        project = create(:project, path: 'gitlab')
842 843 844 845 846 847 848
        project.path = 'foo&bar'

        expect(project).not_to be_valid
        expect(project.to_param).to eq 'gitlab'
      end

      it 'returns current path when new record' do
849
        project = build(:project, path: 'gitlab')
850 851 852 853 854 855
        project.path = 'foo&bar'

        expect(project).not_to be_valid
        expect(project.to_param).to eq 'foo&bar'
      end
    end
856
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
857

858
  describe '#repository' do
859
    let(:project) { create(:project, :repository) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
860

861
    it 'returns valid repo' do
862
      expect(project.repository).to be_kind_of(Repository)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
863 864
    end
  end
865

866
  describe '#default_issues_tracker?' do
867
    it "is true if used internal tracker" do
868
      project = build(:project)
869

870
      expect(project.default_issues_tracker?).to be_truthy
871 872
    end

873
    it "is false if used other tracker" do
874 875 876 877
      # NOTE: The current nature of this factory requires persistence
      project = create(:redmine_project)

      expect(project.default_issues_tracker?).to be_falsey
878 879 880
    end
  end

881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898
  describe '#empty_repo?' do
    context 'when the repo does not exist' do
      let(:project) { build_stubbed(:project) }

      it 'returns true' do
        expect(project.empty_repo?).to be(true)
      end
    end

    context 'when the repo exists' do
      let(:project) { create(:project, :repository) }
      let(:empty_project) { create(:project, :empty_repo) }

      it { expect(empty_project.empty_repo?).to be(true) }
      it { expect(project.empty_repo?).to be(false) }
    end
  end

899
  describe '#external_issue_tracker' do
900
    let(:project) { create(:project) }
901 902 903
    let(:ext_project) { create(:redmine_project) }

    context 'on existing projects with no value for has_external_issue_tracker' do
904
      before do
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933
        project.update_column(:has_external_issue_tracker, nil)
        ext_project.update_column(:has_external_issue_tracker, nil)
      end

      it 'updates the has_external_issue_tracker boolean' do
        expect do
          project.external_issue_tracker
        end.to change { project.reload.has_external_issue_tracker }.to(false)

        expect do
          ext_project.external_issue_tracker
        end.to change { ext_project.reload.has_external_issue_tracker }.to(true)
      end
    end

    it 'returns nil and does not query services when there is no external issue tracker' do
      expect(project).not_to receive(:services)

      expect(project.external_issue_tracker).to eq(nil)
    end

    it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do
      ext_project.reload # Factory returns a project with changed attributes
      expect(ext_project).to receive(:services).once.and_call_original

      2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) }
    end
  end

934
  describe '#cache_has_external_issue_tracker' do
935
    let(:project) { create(:project, has_external_issue_tracker: nil) }
936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953

    it 'stores true if there is any external_issue_tracker' do
      services = double(:service, external_issue_trackers: [RedmineService.new])
      expect(project).to receive(:services).and_return(services)

      expect do
        project.cache_has_external_issue_tracker
      end.to change { project.has_external_issue_tracker}.to(true)
    end

    it 'stores false if there is no external_issue_tracker' do
      services = double(:service, external_issue_trackers: [])
      expect(project).to receive(:services).and_return(services)

      expect do
        project.cache_has_external_issue_tracker
      end.to change { project.has_external_issue_tracker}.to(false)
    end
954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991

    it 'does not cache data when in a read-only GitLab instance' do
      allow(Gitlab::Database).to receive(:read_only?) { true }

      expect do
        project.cache_has_external_issue_tracker
      end.not_to change { project.has_external_issue_tracker }
    end
  end

  describe '#cache_has_external_wiki' do
    let(:project) { create(:project, has_external_wiki: nil) }

    it 'stores true if there is any external_wikis' do
      services = double(:service, external_wikis: [ExternalWikiService.new])
      expect(project).to receive(:services).and_return(services)

      expect do
        project.cache_has_external_wiki
      end.to change { project.has_external_wiki}.to(true)
    end

    it 'stores false if there is no external_wikis' do
      services = double(:service, external_wikis: [])
      expect(project).to receive(:services).and_return(services)

      expect do
        project.cache_has_external_wiki
      end.to change { project.has_external_wiki}.to(false)
    end

    it 'does not cache data when in a read-only GitLab instance' do
      allow(Gitlab::Database).to receive(:read_only?) { true }

      expect do
        project.cache_has_external_wiki
      end.not_to change { project.has_external_wiki }
    end
992 993
  end

994
  describe '#has_wiki?' do
995 996 997
    let(:no_wiki_project)       { create(:project, :wiki_disabled, has_external_wiki: false) }
    let(:wiki_enabled_project)  { create(:project) }
    let(:external_wiki_project) { create(:project, has_external_wiki: true) }
998 999 1000 1001 1002 1003 1004 1005

    it 'returns true if project is wiki enabled or has external wiki' do
      expect(wiki_enabled_project).to have_wiki
      expect(external_wiki_project).to have_wiki
      expect(no_wiki_project).not_to have_wiki
    end
  end

1006
  describe '#external_wiki' do
1007
    let(:project) { create(:project) }
1008

1009 1010 1011 1012 1013
    context 'with an active external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: true)
        project.external_wiki
      end
1014

1015 1016 1017
      it 'sets :has_external_wiki as true' do
        expect(project.has_external_wiki).to be(true)
      end
1018

1019 1020
      it 'sets :has_external_wiki as false if an external wiki service is destroyed later' do
        expect(project.has_external_wiki).to be(true)
1021

1022 1023 1024 1025
        project.services.external_wikis.first.destroy

        expect(project.has_external_wiki).to be(false)
      end
1026 1027
    end

1028 1029 1030 1031
    context 'with an inactive external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: false)
      end
1032

1033 1034 1035
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end
1036 1037
    end

1038 1039 1040 1041
    context 'with no external wiki' do
      before do
        project.external_wiki
      end
1042

1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end

      it 'sets :has_external_wiki as true if an external wiki service is created later' do
        expect(project.has_external_wiki).to be(false)

        create(:service, project: project, type: 'ExternalWikiService', active: true)

        expect(project.has_external_wiki).to be(true)
      end
1054 1055 1056
    end
  end

1057 1058
  describe '#star_count' do
    it 'counts stars from multiple users' do
1059 1060
      user1 = create(:user)
      user2 = create(:user)
1061
      project = create(:project, :public)
Ciro Santilli's avatar
Ciro Santilli committed
1062 1063

      expect(project.star_count).to eq(0)
1064

Ciro Santilli's avatar
Ciro Santilli committed
1065
      user1.toggle_star(project)
1066 1067
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
1068
      user2.toggle_star(project)
1069 1070 1071
      project.reload
      expect(project.reload.star_count).to eq(2)

Ciro Santilli's avatar
Ciro Santilli committed
1072
      user1.toggle_star(project)
1073 1074 1075
      project.reload
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
1076
      user2.toggle_star(project)
1077 1078 1079 1080
      project.reload
      expect(project.reload.star_count).to eq(0)
    end

1081
    it 'counts stars on the right project' do
1082
      user = create(:user)
1083 1084
      project1 = create(:project, :public)
      project2 = create(:project, :public)
1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111

      expect(project1.star_count).to eq(0)
      expect(project2.star_count).to eq(0)

      user.toggle_star(project1)
      project1.reload
      project2.reload
      expect(project1.star_count).to eq(1)
      expect(project2.star_count).to eq(0)

      user.toggle_star(project1)
      project1.reload
      project2.reload
      expect(project1.star_count).to eq(0)
      expect(project2.star_count).to eq(0)

      user.toggle_star(project2)
      project1.reload
      project2.reload
      expect(project1.star_count).to eq(0)
      expect(project2.star_count).to eq(1)

      user.toggle_star(project2)
      project1.reload
      project2.reload
      expect(project1.star_count).to eq(0)
      expect(project2.star_count).to eq(0)
Ciro Santilli's avatar
Ciro Santilli committed
1112 1113
    end
  end
1114

1115
  describe '#avatar_type' do
1116
    let(:project) { create(:project) }
1117

1118
    it 'is true if avatar is image' do
1119
      project.update_attribute(:avatar, 'uploads/avatar.png')
1120
      expect(project.avatar_type).to be_truthy
1121 1122
    end

1123
    it 'is false if avatar is html page' do
1124
      project.update_attribute(:avatar, 'uploads/avatar.html')
1125
      expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico'])
1126 1127
    end
  end
sue445's avatar
sue445 committed
1128