project_spec.rb 184 KB
Newer Older
1
2
# frozen_string_literal: true

gitlabhq's avatar
gitlabhq committed
3
4
require 'spec_helper'

5
describe Project do
6
  include ProjectForksHelper
7
  include GitHelpers
8
  include ExternalAuthorizationServiceHelpers
9

Shinya Maeda's avatar
Shinya Maeda committed
10
  it_behaves_like 'having unique enum values'
Shinya Maeda's avatar
Shinya Maeda committed
11

12
  describe 'associations' do
13
14
15
    it { is_expected.to belong_to(:group) }
    it { is_expected.to belong_to(:namespace) }
    it { is_expected.to belong_to(:creator).class_name('User') }
16
    it { is_expected.to belong_to(:pool_repository) }
17
    it { is_expected.to have_many(:users) }
ubudzisz's avatar
ubudzisz committed
18
    it { is_expected.to have_many(:services) }
19
20
21
22
23
    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) }
24
    it { is_expected.to have_many(:users).through(:project_members) }
25
26
27
28
    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) }
29
    it { is_expected.to have_many(:deploy_keys) }
30
31
32
33
34
    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) }
35
    it { is_expected.to have_one(:hangouts_chat_service) }
36
    it { is_expected.to have_one(:unify_circuit_service) }
Matt Coleman's avatar
Matt Coleman committed
37
    it { is_expected.to have_one(:packagist_service) }
38
39
40
41
    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
42
    it { is_expected.to have_one(:discord_service) }
43
44
45
46
47
    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) }
48
    it { is_expected.to have_one(:hipchat_service) }
49
50
51
52
53
54
55
56
57
    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) }
58
    it { is_expected.to have_one(:youtrack_service) }
59
60
61
62
63
    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) }
64
    it { is_expected.to have_one(:project_repository) }
65
    it { is_expected.to have_one(:container_expiration_policy) }
66
67
    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
68
    it { is_expected.to have_one(:last_event).class_name('Event') }
69
    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
70
    it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
71
    it { is_expected.to have_one(:error_tracking_setting).class_name('ErrorTracking::ProjectErrorTrackingSetting') }
72
    it { is_expected.to have_one(:project_setting) }
73
    it { is_expected.to have_one(:alerting_setting).class_name('Alerting::ProjectAlertingSetting') }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
74
    it { is_expected.to have_many(:commit_statuses) }
75
    it { is_expected.to have_many(:ci_pipelines) }
76
    it { is_expected.to have_many(:ci_refs) }
77
    it { is_expected.to have_many(:builds) }
78
    it { is_expected.to have_many(:build_trace_section_names)}
79
80
81
82
    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
83
    it { is_expected.to have_many(:pages_domains) }
84
85
    it { is_expected.to have_many(:labels).class_name('ProjectLabel') }
    it { is_expected.to have_many(:users_star_projects) }
86
    it { is_expected.to have_many(:repository_languages) }
87
88
89
90
91
92
93
    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) }
94
95
    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
96
    it { is_expected.to have_many(:uploads) }
97
    it { is_expected.to have_many(:pipeline_schedules) }
98
    it { is_expected.to have_many(:members_and_requesters) }
99
    it { is_expected.to have_many(:clusters) }
100
    it { is_expected.to have_many(:management_clusters).class_name('Clusters::Cluster') }
101
    it { is_expected.to have_many(:kubernetes_namespaces) }
102
    it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
103
    it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
104
    it { is_expected.to have_many(:lfs_file_locks) }
Mayra Cabrera's avatar
Mayra Cabrera committed
105
106
    it { is_expected.to have_many(:project_deploy_tokens) }
    it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
107
    it { is_expected.to have_many(:cycle_analytics_stages) }
108
    it { is_expected.to have_many(:external_pull_requests) }
109
110
    it { is_expected.to have_many(:sourced_pipelines) }
    it { is_expected.to have_many(:source_pipelines) }
111
112
    it { is_expected.to have_many(:prometheus_alert_events) }
    it { is_expected.to have_many(:self_managed_prometheus_alert_events) }
113

114
115
116
117
118
119
    it_behaves_like 'model with repository' do
      let_it_be(:container) { create(:project, :repository, path: 'somewhere') }
      let(:stubbed_container) { build_stubbed(:project) }
      let(:expected_full_path) { "#{container.namespace.full_path}/somewhere" }
      let(:expected_repository_klass) { Repository }
      let(:expected_storage_klass) { Storage::Hashed }
120
      let(:expected_web_url_path) { "#{container.namespace.full_path}/somewhere" }
121
122
    end

123
124
125
126
    it 'has an inverse relationship with merge requests' do
      expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
    end

127
128
129
130
131
132
133
134
135
136
137
138
139
140
    it 'has a distinct has_many :lfs_objects relation through lfs_objects_projects' do
      project = create(:project)
      lfs_object = create(:lfs_object)
      [:project, :design].each do |repository_type|
        create(:lfs_objects_project, project: project,
                                     lfs_object: lfs_object,
                                     repository_type: repository_type)
      end

      expect(project.lfs_objects_projects.size).to eq(2)
      expect(project.lfs_objects.size).to eq(1)
      expect(project.lfs_objects.to_a).to eql([lfs_object])
    end

141
142
    context 'after initialized' do
      it "has a project_feature" do
143
        expect(described_class.new.project_feature).to be_present
144
145
146
      end
    end

147
    context 'when creating a new project' do
148
      let_it_be(:project) { create(:project) }
149

150
      it 'automatically creates a CI/CD settings row' do
151
152
153
        expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
        expect(project.ci_cd_settings).to be_persisted
      end
154

155
156
157
158
159
      it 'automatically creates a container expiration policy row' do
        expect(project.container_expiration_policy).to be_an_instance_of(ContainerExpirationPolicy)
        expect(project.container_expiration_policy).to be_persisted
      end

160
161
162
163
164
165
166
167
168
169
      it 'does not create another container expiration policy if there is already one' do
        project = build(:project)

        expect do
          container_expiration_policy = create(:container_expiration_policy, project: project)

          expect(project.container_expiration_policy).to eq(container_expiration_policy)
        end.to change { ContainerExpirationPolicy.count }.by(1)
      end

170
171
172
173
      it 'automatically creates a Pages metadata row' do
        expect(project.pages_metadatum).to be_an_instance_of(ProjectPagesMetadatum)
        expect(project.pages_metadatum).to be_persisted
      end
174
175
176
177
178

      it 'automatically creates a project setting row' do
        expect(project.project_setting).to be_an_instance_of(ProjectSetting)
        expect(project.project_setting).to be_persisted
      end
179
180
    end

181
182
183
184
    context 'updating cd_cd_settings' do
      it 'does not raise an error' do
        project = create(:project)

James Lopez's avatar
James Lopez committed
185
        expect { project.update(ci_cd_settings: nil) }.not_to raise_exception
186
187
188
      end
    end

189
    describe '#members & #requesters' do
190
      let(:project) { create(:project, :public) }
191
192
      let(:requester) { create(:user) }
      let(:developer) { create(:user) }
193

194
195
      before do
        project.request_access(requester)
196
        project.add_developer(developer)
197
198
      end

199
200
      it_behaves_like 'members and requesters associations' do
        let(:namespace) { project }
201
202
      end
    end
203

204
    describe 'ci_pipelines association' do
205
206
      it 'returns only pipelines from ci_sources' do
        expect(Ci::Pipeline).to receive(:ci_sources).and_call_original
207

208
        subject.ci_pipelines
209
210
      end
    end
gitlabhq's avatar
gitlabhq committed
211
212
  end

213
214
215
216
217
218
219
220
  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) }
221
222
  end

223
  describe 'validation' do
224
    let!(:project) { create(:project) }
225

226
227
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
228
    it { is_expected.to validate_length_of(:name).is_at_most(255) }
229
    it { is_expected.to validate_presence_of(:path) }
230
231
    it { is_expected.to validate_length_of(:path).is_at_most(255) }
    it { is_expected.to validate_length_of(:description).is_at_most(2000) }
232
233
234
    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) }
235
    it { is_expected.not_to allow_value('/test/foo').for(:ci_config_path) }
236
237
    it { is_expected.to validate_presence_of(:creator) }
    it { is_expected.to validate_presence_of(:namespace) }
238
    it { is_expected.to validate_presence_of(:repository_storage) }
239
    it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) }
240

241
242
243
244
245
    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)
246
        .with_message('needs to be between 10 minutes and 1 month')
247
248
    end

249
    it 'does not allow new projects beyond user limits' do
250
      project2 = build(:project)
251
252
253
254
255
256
257

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

258
      expect(project2).not_to be_valid
259
    end
260

261
262
263
264
265
266
267
    it 'validates the visibility' do
      expect_any_instance_of(described_class).to receive(:visibility_level_allowed_as_fork).and_call_original
      expect_any_instance_of(described_class).to receive(:visibility_level_allowed_by_group).and_call_original

      create(:project)
    end

268
269
    describe 'wiki path conflict' do
      context "when the new path has been used by the wiki of other Project" do
270
        it 'has an error on the name attribute' do
271
          new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
272
273

          expect(new_project).not_to be_valid
274
          expect(new_project.errors[:name].first).to eq(_('has already been taken'))
275
276
277
278
        end
      end

      context "when the new wiki path has been used by the path of other Project" do
279
        it 'has an error on the name attribute' do
280
281
          project_with_wiki_suffix = create(:project, path: 'foo.wiki')
          new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
282
283

          expect(new_project).not_to be_valid
284
          expect(new_project.errors[:name].first).to eq(_('has already been taken'))
285
286
287
        end
      end
    end
288

289
    context 'repository storages inclusion' do
290
      let(:project2) { build(:project, repository_storage: 'missing') }
291
292

      before do
293
        storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
294
295
296
        allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
      end

297
      it "does not allow repository storages that don't match a label in the configuration" do
298
299
300
301
        expect(project2).not_to be_valid
        expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
      end
    end
302

303
304
305
306
307
308
    it 'validates presence of project_feature' do
      project = build(:project, project_feature: nil)

      expect(project).not_to be_valid
    end

309
310
311
    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
312

313
314
        expect(project).not_to be_valid
      end
315

316
317
318
      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'
319

320
321
        expect(project).to be_valid
      end
322

323
324
      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')
325

326
327
        expect(project).not_to be_valid
      end
James Lopez's avatar
James Lopez committed
328

329
330
      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
331

332
333
        expect(project).to be_valid
      end
334

335
336
      it 'allows an empty URI' do
        project = build(:project, import_url: '')
337

338
339
        expect(project).to be_valid
      end
340

341
342
      it 'does not produce import data on an empty URI' do
        project = build(:project, import_url: '')
343

344
345
        expect(project.import_data).to be_nil
      end
346

347
348
      it 'does not produce import data on an invalid URI' do
        project = build(:project, import_url: 'test://')
349

350
351
        expect(project.import_data).to be_nil
      end
352

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

356
357
358
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed')
      end
359

360
361
362
363
364
365
366
      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

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

370
371
372
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443')
      end
373

374
375
376
      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'
377

378
379
380
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
      end
381

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

385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
        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
403
404
    end

405
406
    describe 'project pending deletion' do
      let!(:project_pending_deletion) do
407
        create(:project,
408
409
410
               pending_delete: true)
      end
      let(:new_project) do
411
        build(:project,
412
413
414
415
416
417
418
419
420
              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
421
        expect(new_project.errors.full_messages.first).to eq(_('The project is still being deleted. Please try again later.'))
422
423
      end
    end
424
425
426

    describe 'path validation' do
      it 'allows paths reserved on the root namespace' do
427
        project = build(:project, path: 'api')
428
429
430
431
432

        expect(project).to be_valid
      end

      it 'rejects paths reserved on another level' do
433
        project = build(:project, path: 'tree')
434
435
436

        expect(project).not_to be_valid
      end
437
438
439

      it 'rejects nested paths' do
        parent = create(:group, :nested, path: 'environments')
440
        project = build(:project, path: 'folders', namespace: parent)
441
442
443

        expect(project).not_to be_valid
      end
444
445
446

      it 'allows a reserved group name' do
        parent = create(:group)
447
        project = build(:project, path: 'avatar', namespace: parent)
448
449
450

        expect(project).to be_valid
      end
451
452
453
454
455
456

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

        expect(project).to be_valid
      end
457
    end
gitlabhq's avatar
gitlabhq committed
458
  end
459

460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
  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

477
      it 'returns .external pipelines' do
478
479
480
481
482
483
        expect(project.all_pipelines).to all(have_attributes(source: 'external'))
        expect(project.all_pipelines.size).to eq(1)
      end
    end
  end

484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
  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

501
      it 'returns .external pipelines' do
502
503
504
505
506
507
        expect(project.ci_pipelines).to all(have_attributes(source: 'external'))
        expect(project.ci_pipelines.size).to eq(1)
      end
    end
  end

508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
  describe '#autoclose_referenced_issues' do
    context 'when DB entry is nil' do
      let(:project) { create(:project, autoclose_referenced_issues: nil) }

      it 'returns true' do
        expect(project.autoclose_referenced_issues).to be_truthy
      end
    end

    context 'when DB entry is true' do
      let(:project) { create(:project, autoclose_referenced_issues: true) }

      it 'returns true' do
        expect(project.autoclose_referenced_issues).to be_truthy
      end
    end

    context 'when DB entry is false' do
      let(:project) { create(:project, autoclose_referenced_issues: false) }

      it 'returns false' do
        expect(project.autoclose_referenced_issues).to be_falsey
      end
    end
  end

534
  describe 'project token' do
535
    it 'sets an random token if none provided' do
536
      project = FactoryBot.create(:project, runners_token: '')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
537
      expect(project.runners_token).not_to eq('')
538
539
    end

ubudzisz's avatar
ubudzisz committed
540
    it 'does not set an random token if one provided' do
541
      project = FactoryBot.create(:project, runners_token: 'my-token')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
542
      expect(project.runners_token).to eq('my-token')
543
544
    end
  end
gitlabhq's avatar
gitlabhq committed
545

546
  describe 'Respond to' do
547
548
549
550
    it { is_expected.to respond_to(:url_to_repo) }
    it { is_expected.to respond_to(:execute_hooks) }
    it { is_expected.to respond_to(:owner) }
    it { is_expected.to respond_to(:path_with_namespace) }
551
    it { is_expected.to respond_to(:full_path) }
gitlabhq's avatar
gitlabhq committed
552
553
  end

554
  describe 'delegation' do
555
    [:add_guest, :add_reporter, :add_developer, :add_maintainer, :add_user, :add_users].each do |method|
556
557
558
559
560
      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) }
561
    it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
562
    it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
563
564
  end

565
566
567
568
569
570
571
  describe 'reference methods' do
    let_it_be(:owner)     { create(:user, name: 'Gitlab') }
    let_it_be(:namespace) { create(:namespace, name: 'Sample namespace', path: 'sample-namespace', owner: owner) }
    let_it_be(:project)   { create(:project, name: 'Sample project', path: 'sample-project', namespace: namespace) }
    let_it_be(:group)     { create(:group, name: 'Group', path: 'sample-group') }
    let_it_be(:another_project) { create(:project, namespace: namespace) }
    let_it_be(:another_namespace_project) { create(:project, name: 'another-project') }
572

573
574
575
576
    describe '#to_reference' do
      it 'returns the path with reference_postfix' do
        expect(project.to_reference).to eq("#{project.full_path}>")
      end
577

578
579
      it 'returns the path with reference_postfix when arg is self' do
        expect(project.to_reference(project)).to eq("#{project.full_path}>")
580
581
      end

582
583
      it 'returns the full_path with reference_postfix when full' do
        expect(project.to_reference(full: true)).to eq("#{project.full_path}>")
584
585
      end

586
587
      it 'returns the full_path with reference_postfix when cross-project' do
        expect(project.to_reference(build_stubbed(:project))).to eq("#{project.full_path}>")
588
589
590
      end
    end

591
592
593
594
595
    describe '#to_reference_base' do
      context 'when nil argument' do
        it 'returns nil' do
          expect(project.to_reference_base).to be_nil
        end
596
597
      end

598
599
600
      context 'when full is true' do
        it 'returns complete path to the project', :aggregate_failures do
          be_full_path = eq('sample-namespace/sample-project')
601

602
603
604
605
          expect(project.to_reference_base(full: true)).to be_full_path
          expect(project.to_reference_base(project, full: true)).to be_full_path
          expect(project.to_reference_base(group, full: true)).to be_full_path
        end
606
      end
607

608
609
610
611
612
      context 'when same project argument' do
        it 'returns nil' do
          expect(project.to_reference_base(project)).to be_nil
        end
      end
613

614
615
616
617
      context 'when cross namespace project argument' do
        it 'returns complete path to the project' do
          expect(project.to_reference_base(another_namespace_project)).to eq 'sample-namespace/sample-project'
        end
618
619
      end

620
      context 'when same namespace / cross-project argument' do
621
        it 'returns path to the project' do
622
          expect(project.to_reference_base(another_project)).to eq 'sample-project'
623
624
625
        end
      end

626
627
628
629
      context 'when different namespace / cross-project argument with same owner' do
        let(:another_namespace_same_owner) { create(:namespace, path: 'another-namespace', owner: owner) }
        let(:another_project_same_owner)   { create(:project, path: 'another-project', namespace: another_namespace_same_owner) }

630
        it 'returns full path to the project' do
631
          expect(project.to_reference_base(another_project_same_owner)).to eq 'sample-namespace/sample-project'
632
        end
633
      end
634

635
636
637
638
639
640
      context 'when argument is a namespace' do
        context 'with same project path' do
          it 'returns path to the project' do
            expect(project.to_reference_base(namespace)).to eq 'sample-project'
          end
        end
641

642
643
644
645
646
        context 'with different project path' do
          it 'returns full path to the project' do
            expect(project.to_reference_base(group)).to eq 'sample-namespace/sample-project'
          end
        end
647
648
649
      end
    end

650
651
652
653
654
    describe '#to_human_reference' do
      context 'when nil argument' do
        it 'returns nil' do
          expect(project.to_human_reference).to be_nil
        end
655
656
      end

657
658
659
660
      context 'when same project argument' do
        it 'returns nil' do
          expect(project.to_human_reference(project)).to be_nil
        end
661
662
      end

663
664
665
666
667
      context 'when cross namespace project argument' do
        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
668

669
670
671
672
      context 'when same namespace / cross-project argument' do
        it 'returns name of the project' do
          expect(project.to_human_reference(another_project)).to eq 'Sample project'
        end
673
      end
674
675
676
    end
  end

677
  describe '#merge_method' do
678
679
680
681
682
683
684
    using RSpec::Parameterized::TableSyntax

    where(:ff, :rebase, :method) do
      true  | true  | :ff
      true  | false | :ff
      false | true  | :rebase_merge
      false | false | :merge
685
686
    end

687
688
689
690
691
692
    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) }
693
694
695
    end
  end

696
  it 'returns valid url to repo' do
697
    project = described_class.new(path: 'somewhere')
698
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
699
700
  end

701
702
  describe "#readme_url" do
    context 'with a non-existing repository' do
703
      let(:project) { create(:project) }
704

705
      it 'returns nil' do
706
707
708
709
710
711
        expect(project.readme_url).to be_nil
      end
    end

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

714
        it 'returns nil' do
715
716
717
718
719
          expect(project.readme_url).to be_nil
        end
      end

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

722
        it 'returns the README' do
723
          expect(project.readme_url).to eq("#{project.web_url}/-/blob/master/README.md")
724
725
726
727
728
        end
      end
    end
  end

729
  describe "#new_issuable_address" do
730
    let(:project) { create(:project, path: "somewhere") }
731
732
    let(:user) { create(:user) }

733
734
735
736
737
738
    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
739
        address = "p+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-issue@gl.ab"
740

741
742
743
744
        expect(project.new_issuable_address(user, 'issue')).to eq(address)
      end

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

        expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
748
      end
749
750
751
752

      it 'returns nil with invalid address type' do
        expect(project.new_issuable_address(user, 'invalid_param')).to be_nil
      end
753
754
755
756
757
758
    end

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

760
      it 'returns nil' do
761
762
763
764
765
        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
766
      end
767
768
769
    end
  end

770
  describe 'last_activity methods' do
771
772
    let(:timestamp) { 2.hours.ago }
    # last_activity_at gets set to created_at upon creation
773
    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
774

775
    describe 'last_activity' do
776
      it 'alias last_activity to last_event' do
777
        last_event = create(:event, :closed, project: project)
778

779
        expect(project.last_activity).to eq(last_event)
780
      end
gitlabhq's avatar
gitlabhq committed
781
782
    end

783
784
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
785
        new_event = create(:event, :closed, project: project, created_at: Time.now)
786

787
        project.reload
788
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
789
      end
790

791
      it 'returns the project\'s last update date if it has no events' do
792
        expect(project.last_activity_date).to eq(project.updated_at)
793
      end
794
795

      it 'returns the most recent timestamp' do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
796
797
798
        project.update(updated_at: nil,
                       last_activity_at: timestamp,
                       last_repository_updated_at: timestamp - 1.hour)
799

800
        expect(project.last_activity_date).to be_like_time(timestamp)
801

Lin Jen-Shin's avatar
Lin Jen-Shin committed
802
803
804
        project.update(updated_at: timestamp,
                       last_activity_at: timestamp - 1.hour,
                       last_repository_updated_at: nil)
805

806
        expect(project.last_activity_date).to be_like_time(timestamp)
807
      end
808
809
    end
  end
810

811
  describe '#get_issue' do
812
    let(:project) { create(:project) }
Stan Hu's avatar
Stan Hu committed
813
    let!(:issue)  { create(:issue, project: project) }
814
815
816
    let(:user)    { create(:user) }

    before do
817
      project.add_developer(user)
818
    end
819
820
821

    context 'with default issues tracker' do
      it 'returns an issue' do
822
        expect(project.get_issue(issue.iid, user)).to eq issue
823
824
      end

Stan Hu's avatar
Stan Hu committed
825
826
827
828
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      end

829
      it 'returns nil when no issue found' do
830
831
832
833
834
835
        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
836
837
838
839
      end
    end

    context 'with external issues tracker' do
840
      let!(:internal_issue) { create(:issue, project: project) }
841

842
      before do
843
        allow(project).to receive(:external_issue_tracker).and_return(true)
844
845
      end

846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
      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
883
884
885
886
887
      end
    end
  end

  describe '#issue_exists?' do
888
    let(:project) { create(:project) }
889
890
891
892
893
894
895
896
897
898
899
900

    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

901
  describe '#to_param' do
902
903
    context 'with namespace' do
      before do
904
        @group = create(:group, name: 'gitlab')
905
        @project = create(:project, name: 'gitlabhq', namespace: @group)
906
907
      end

Vinnie Okada's avatar
Vinnie Okada committed
908
      it { expect(@project.to_param).to eq('gitlabhq') }
909
    end
910
911
912

    context 'with invalid path' do
      it 'returns previous path to keep project suitable for use in URLs when persisted' do
913
        project = create(:project, path: 'gitlab')
914
915
916
917
918
919
920
        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
921
        project = build(:project, path: 'gitlab')
922
923
924
925
926
927
        project.path = 'foo&bar'

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

930
  describe '#default_issues_tracker?' do
931
    it "is true if used internal tracker" do
932
      project = build(:project)
933

934
      expect(project.default_issues_tracker?).to be_truthy
935
936
    end

937
    it "is false if used other tracker" do
938
939
940
941
      # NOTE: The current nature of this factory requires persistence
      project = create(:redmine_project)

      expect(project.default_issues_tracker?).to be_falsey
942
943
944
    end
  end

945
  describe '#external_issue_tracker' do
946
    let(:project) { create(:project) }
947
948
949
    let(:ext_project) { create(:redmine_project) }

    context 'on existing projects with no value for has_external_issue_tracker' do
950
      before do
951
952
953
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
        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

980
  describe '#cache_has_external_issue_tracker' do
981
    let(:project) { create(:project, has_external_issue_tracker: nil) }
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999

    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
Toon Claes's avatar
Toon Claes committed
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028