Read about our upcoming Code of Conduct on this issue

project_spec.rb 183 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
113
114
115
116
117
    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 }
118
      let(:expected_web_url_path) { "#{container.namespace.full_path}/somewhere" }
119
120
    end

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

125
126
127
128
129
130
131
132
133
134
135
136
137
138
    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

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

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

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

153
154
155
156
157
      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

158
159
160
161
162
163
164
165
166
167
      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

168
169
170
171
      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
172
173
174
175
176

      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
177
178
    end

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

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

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

192
193
      before do
        project.request_access(requester)
194
        project.add_developer(developer)
195
196
      end

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

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

206
        subject.ci_pipelines
207
208
      end
    end
gitlabhq's avatar
gitlabhq committed
209
210
  end

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

221
  describe 'validation' do
222
    let!(:project) { create(:project) }
223

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

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

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

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

256
      expect(project2).not_to be_valid
257
    end
258

259
260
261
262
263
264
265
    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

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

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

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

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

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

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

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

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

      expect(project).not_to be_valid
    end

307
308
309
    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
310

311
312
        expect(project).not_to be_valid
      end
313

314
315
316
      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'
317

318
319
        expect(project).to be_valid
      end
320

321
322
      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')
323

324
325
        expect(project).not_to be_valid
      end
James Lopez's avatar
James Lopez committed
326

327
328
      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
329

330
331
        expect(project).to be_valid
      end
332

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

336
337
        expect(project).to be_valid
      end
338

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

342
343
        expect(project.import_data).to be_nil
      end
344

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

348
349
        expect(project.import_data).to be_nil
      end
350

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

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

358
359
360
361
362
363
364
      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

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

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

372
373
374
      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'
375

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

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

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

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

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

        expect(project).to be_valid
      end

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

        expect(project).not_to be_valid
      end
435
436
437

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

        expect(project).not_to be_valid
      end
442
443
444

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

        expect(project).to be_valid
      end
449
450
451
452
453
454

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

        expect(project).to be_valid
      end
455
    end
gitlabhq's avatar
gitlabhq committed
456
  end
457

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

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

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

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

506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
  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

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

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

544
  describe 'Respond to' do
545
546
547
548
    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) }
549
    it { is_expected.to respond_to(:full_path) }
gitlabhq's avatar
gitlabhq committed
550
551
  end

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

563
564
565
566
567
568
569
  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') }
570

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

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

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

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

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

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

600
601
602
603
          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
604
      end
605

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

612
613
614
615
      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
616
617
      end

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

624
625
626
627
      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) }

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

633
634
635
636
637
638
      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
639

640
641
642
643
644
        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
645
646
647
      end
    end

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

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

661
662
663
664
665
      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
666

667
668
669
670
      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
671
      end
672
673
674
    end
  end

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

    where(:ff, :rebase, :method) do
      true  | true  | :ff
      true  | false | :ff
      false | true  | :rebase_merge
      false | false | :merge
683
684
    end

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

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

Imre Farkas's avatar
Imre Farkas committed
699
700
  describe "#readme_url" do
    context 'with a non-existing repository' do
701
      let(:project) { create(:project) }
Imre Farkas's avatar
Imre Farkas committed
702

703
      it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
704
705
706
707
708
709
        expect(project.readme_url).to be_nil
      end
    end

    context 'with an existing repository' do
      context 'when no README exists' do
710
        let(:project) { create(:project, :empty_repo) }
Imre Farkas's avatar
Imre Farkas committed
711

712
        it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
713
714
715
716
717
          expect(project.readme_url).to be_nil
        end
      end

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

Imre Farkas's avatar
Imre Farkas committed
720
        it 'returns the README' do
721
          expect(project.readme_url).to eq("#{project.web_url}/-/blob/master/README.md")
Imre Farkas's avatar
Imre Farkas committed
722
723
724
725
726
        end
      end
    end
  end

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

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

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

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

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

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

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

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

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

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

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

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

785
        project.reload
786
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
787
      end
788

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

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

798
        expect(project.last_activity_date).to be_like_time(timestamp)
799

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

804
        expect(project.last_activity_date).to be_like_time(timestamp)
805
      end
806
807
    end
  end
808

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

    before do
815
      project.add_developer(user)
816
    end
817
818
819

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

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

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

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

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

844
845
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
      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
881
882
883
884
885
      end
    end
  end

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

    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

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

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

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

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

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

932
      expect(project.default_issues_tracker?).to be_truthy
933
934
    end

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

      expect(project.default_issues_tracker?).to be_falsey
940
941
942
    end
  end

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

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

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

    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
998
999
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
1029
1030
1031
1032
1033
1034
1035

    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
1036
1037
  end

1038
  describe '#has_wiki?' do
1039
1040
1041
    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) }
1042