Read about our upcoming Code of Conduct on this issue

project_spec.rb 157 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) }
Matt Coleman's avatar
Matt Coleman committed
36
    it { is_expected.to have_one(:packagist_service) }
37
38
39
40
    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
41
    it { is_expected.to have_one(:discord_service) }
42
43
44
45
46
    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) }
47
    it { is_expected.to have_one(:hipchat_service) }
48
49
50
51
52
53
54
55
56
    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) }
57
    it { is_expected.to have_one(:youtrack_service) }
58
59
60
61
62
    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) }
63
    it { is_expected.to have_one(:project_repository) }
64
65
    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
66
    it { is_expected.to have_one(:last_event).class_name('Event') }
67
    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
68
    it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
69
    it { is_expected.to have_one(:error_tracking_setting).class_name('ErrorTracking::ProjectErrorTrackingSetting') }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
70
    it { is_expected.to have_many(:commit_statuses) }
71
    it { is_expected.to have_many(:ci_pipelines) }
72
    it { is_expected.to have_many(:builds) }
73
    it { is_expected.to have_many(:build_trace_section_names)}
74
75
76
77
    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
78
    it { is_expected.to have_many(:pages_domains) }
79
80
    it { is_expected.to have_many(:labels).class_name('ProjectLabel') }
    it { is_expected.to have_many(:users_star_projects) }
81
    it { is_expected.to have_many(:repository_languages) }
82
83
84
85
86
87
88
    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) }
89
90
    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
91
    it { is_expected.to have_many(:uploads) }
92
    it { is_expected.to have_many(:pipeline_schedules) }
93
    it { is_expected.to have_many(:members_and_requesters) }
94
    it { is_expected.to have_many(:clusters) }
95
    it { is_expected.to have_many(:kubernetes_namespaces) }
96
    it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
97
    it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
98
    it { is_expected.to have_many(:lfs_file_locks) }
Mayra Cabrera's avatar
Mayra Cabrera committed
99
100
    it { is_expected.to have_many(:project_deploy_tokens) }
    it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
101
    it { is_expected.to have_many(:cycle_analytics_stages) }
102
    it { is_expected.to have_many(:external_pull_requests) }
103

104
105
106
107
    it 'has an inverse relationship with merge requests' do
      expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project)
    end

108
109
110
111
112
113
114
115
116
117
118
119
120
121
    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

122
123
    context 'after initialized' do
      it "has a project_feature" do
124
        expect(described_class.new.project_feature).to be_present
125
126
127
      end
    end

128
129
130
131
132
133
134
    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
135
136
137
138
139
140
141

      it 'automatically creates a Pages metadata row' do
        project = create(:project)

        expect(project.pages_metadatum).to be_an_instance_of(ProjectPagesMetadatum)
        expect(project.pages_metadatum).to be_persisted
      end
142
143
    end

144
145
146
147
    context 'updating cd_cd_settings' do
      it 'does not raise an error' do
        project = create(:project)

James Lopez's avatar
James Lopez committed
148
        expect { project.update(ci_cd_settings: nil) }.not_to raise_exception
149
150
151
      end
    end

152
    describe '#members & #requesters' do
153
      let(:project) { create(:project, :public, :access_requestable) }
154
155
156
157
      let(:requester) { create(:user) }
      let(:developer) { create(:user) }
      before do
        project.request_access(requester)
158
        project.add_developer(developer)
159
160
      end

161
162
      it_behaves_like 'members and requesters associations' do
        let(:namespace) { project }
163
164
      end
    end
165

166
    describe 'ci_pipelines association' do
167
168
      it 'returns only pipelines from ci_sources' do
        expect(Ci::Pipeline).to receive(:ci_sources).and_call_original
169

170
        subject.ci_pipelines
171
172
      end
    end
gitlabhq's avatar
gitlabhq committed
173
174
  end

175
176
177
178
179
180
181
182
  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) }
183
184
  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
    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)
207
        .with_message('needs to be between 10 minutes and 1 month')
208
209
    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
224
225
226
227
228
    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

229
230
    describe 'wiki path conflict' do
      context "when the new path has been used by the wiki of other Project" do
231
        it 'has an error on the name attribute' do
232
          new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
233
234
235
236
237
238
239

          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
240
        it 'has an error on the name attribute' do
241
242
          project_with_wiki_suffix = create(:project, path: 'foo.wiki')
          new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
243
244
245
246
247
248

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

250
    context 'repository storages inclusion' do
251
      let(:project2) { build(:project, repository_storage: 'missing') }
252
253

      before do
254
        storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
255
256
257
        allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
      end

258
      it "does not allow repository storages that don't match a label in the configuration" do
259
260
261
262
        expect(project2).not_to be_valid
        expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
      end
    end
263

264
265
266
    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
267

268
269
        expect(project).not_to be_valid
      end
270

271
272
273
      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'
274

275
276
        expect(project).to be_valid
      end
277

278
279
      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')
280

281
282
        expect(project).not_to be_valid
      end
James Lopez's avatar
James Lopez committed
283

284
285
      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
286

287
288
        expect(project).to be_valid
      end
289

290
291
      it 'allows an empty URI' do
        project = build(:project, import_url: '')
292

293
294
        expect(project).to be_valid
      end
295

296
297
      it 'does not produce import data on an empty URI' do
        project = build(:project, import_url: '')
298

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

302
303
      it 'does not produce import data on an invalid URI' do
        project = build(:project, import_url: 'test://')
304

305
306
        expect(project.import_data).to be_nil
      end
307

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

311
312
313
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed')
      end
314

315
316
317
318
319
320
321
      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

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

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

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

333
334
335
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
      end
336

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

340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
        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
358
359
    end

360
361
    describe 'project pending deletion' do
      let!(:project_pending_deletion) do
362
        create(:project,
363
364
365
               pending_delete: true)
      end
      let(:new_project) do
366
        build(:project,
367
368
369
370
371
372
373
374
375
376
377
378
              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
379
380
381

    describe 'path validation' do
      it 'allows paths reserved on the root namespace' do
382
        project = build(:project, path: 'api')
383
384
385
386
387

        expect(project).to be_valid
      end

      it 'rejects paths reserved on another level' do
388
        project = build(:project, path: 'tree')
389
390
391

        expect(project).not_to be_valid
      end
392
393
394

      it 'rejects nested paths' do
        parent = create(:group, :nested, path: 'environments')
395
        project = build(:project, path: 'folders', namespace: parent)
396
397
398

        expect(project).not_to be_valid
      end
399
400
401

      it 'allows a reserved group name' do
        parent = create(:group)
402
        project = build(:project, path: 'avatar', namespace: parent)
403
404
405

        expect(project).to be_valid
      end
406
407
408
409
410
411

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

        expect(project).to be_valid
      end
412
    end
gitlabhq's avatar
gitlabhq committed
413
  end
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

432
      it 'returns .external pipelines' do
433
434
435
436
437
438
        expect(project.all_pipelines).to all(have_attributes(source: 'external'))
        expect(project.all_pipelines.size).to eq(1)
      end
    end
  end

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

456
      it 'returns .external pipelines' do
457
458
459
460
461
462
        expect(project.ci_pipelines).to all(have_attributes(source: 'external'))
        expect(project.ci_pipelines.size).to eq(1)
      end
    end
  end

463
  describe 'project token' do
464
    it 'sets an random token if none provided' do
465
      project = FactoryBot.create(:project, runners_token: '')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
466
      expect(project.runners_token).not_to eq('')
467
468
    end

ubudzisz's avatar
ubudzisz committed
469
    it 'does not set an random token if one provided' do
470
      project = FactoryBot.create(:project, runners_token: 'my-token')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
471
      expect(project.runners_token).to eq('my-token')
472
473
    end
  end
gitlabhq's avatar
gitlabhq committed
474

475
  describe 'Respond to' do
476
477
478
479
480
    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) }
481
    it { is_expected.to respond_to(:full_path) }
gitlabhq's avatar
gitlabhq committed
482
483
  end

484
  describe 'delegation' do
485
    [:add_guest, :add_reporter, :add_developer, :add_maintainer, :add_user, :add_users].each do |method|
486
487
488
489
490
      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) }
491
    it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
492
    it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
493
494
  end

495
496
497
498
499
500
501
502
503
  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

504
  describe '#to_reference' do
505
    let(:owner)     { create(:user, name: 'Gitlab') }
506
    let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
507
    let(:project)   { create(:project, path: 'sample-project', namespace: namespace) }
508
    let(:group)     { create(:group, name: 'Group', path: 'sample-group') }
509

510
    context 'when nil argument' do
511
512
513
514
515
      it 'returns nil' do
        expect(project.to_reference).to be_nil
      end
    end

516
    context 'when full is true' do
517
      it 'returns complete path to the project' do
518
519
520
        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'
521
522
523
524
525
526
527
528
529
530
      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
531
      let(:another_namespace_project) { create(:project, name: 'another-project') }
532
533
534
535
536
537
538

      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
539
      let(:another_project) { create(:project, namespace: namespace) }
540

541
      it 'returns path to the project' do
542
543
544
        expect(project.to_reference(another_project)).to eq 'sample-project'
      end
    end
545

546
547
    context 'when different namespace / cross-project argument' do
      let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
548
      let(:another_project)   { create(:project, path: 'another-project', namespace: another_namespace) }
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565

      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
566
567
      end
    end
568
569
570
571
572
  end

  describe '#to_human_reference' do
    let(:owner) { create(:user, name: 'Gitlab') }
    let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
573
    let(:project) { create(:project, name: 'Sample project', namespace: namespace) }
574
575
576
577
578
579
580
581
582
583
584
585
586
587

    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
588
      let(:another_namespace_project) { create(:project, name: 'another-project') }
589
590
591
592
593
594
595

      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
596
      let(:another_project) { create(:project, namespace: namespace) }
597
598
599
600

      it 'returns name of the project' do
        expect(project.to_human_reference(another_project)).to eq 'Sample project'
      end
601
602
603
    end
  end

604
  describe '#merge_method' do
605
606
607
608
609
610
611
    using RSpec::Parameterized::TableSyntax

    where(:ff, :rebase, :method) do
      true  | true  | :ff
      true  | false | :ff
      false | true  | :rebase_merge
      false | false | :merge
612
613
    end

614
615
616
617
618
619
    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) }
620
621
622
    end
  end

623
  it 'returns valid url to repo' do
624
    project = described_class.new(path: 'somewhere')
625
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
626
627
  end

Douwe Maan's avatar
Douwe Maan committed
628
  describe "#web_url" do
629
    let(:project) { create(:project, path: "somewhere") }
Douwe Maan's avatar
Douwe Maan committed
630
631

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

Imre Farkas's avatar
Imre Farkas committed
636
637
  describe "#readme_url" do
    context 'with a non-existing repository' do
638
      let(:project) { create(:project) }
Imre Farkas's avatar
Imre Farkas committed
639

640
      it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
641
642
643
644
645
646
        expect(project.readme_url).to be_nil
      end
    end

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

649
        it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
650
651
652
653
654
          expect(project.readme_url).to be_nil
        end
      end

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

Imre Farkas's avatar
Imre Farkas committed
657
        it 'returns the README' do
658
          expect(project.readme_url).to eq("#{project.web_url}/blob/master/README.md")
Imre Farkas's avatar
Imre Farkas committed
659
660
661
662
663
        end
      end
    end
  end

664
  describe "#new_issuable_address" do
665
    let(:project) { create(:project, path: "somewhere") }
666
667
    let(:user) { create(:user) }

668
669
670
671
672
673
    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
674
        address = "p+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-issue@gl.ab"
675

676
677
678
679
        expect(project.new_issuable_address(user, 'issue')).to eq(address)
      end

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

        expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
683
      end
684
685
686
687

      it 'returns nil with invalid address type' do
        expect(project.new_issuable_address(user, 'invalid_param')).to be_nil
      end
688
689
690
691
692
693
    end

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

695
      it 'returns nil' do
696
697
698
699
700
        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
701
      end
702
703
704
    end
  end

705
  describe 'last_activity methods' do
706
707
    let(:timestamp) { 2.hours.ago }
    # last_activity_at gets set to created_at upon creation
708
    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
709

710
    describe 'last_activity' do
711
      it 'alias last_activity to last_event' do
712
        last_event = create(:event, :closed, project: project)
713

714
        expect(project.last_activity).to eq(last_event)
715
      end
gitlabhq's avatar
gitlabhq committed
716
717
    end

718
719
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
720
        new_event = create(:event, :closed, project: project, created_at: Time.now)
721

722
        project.reload
723
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
724
      end
725

726
      it 'returns the project\'s last update date if it has no events' do
727
        expect(project.last_activity_date).to eq(project.updated_at)
728
      end
729
730

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

735
        expect(project.last_activity_date).to be_like_time(timestamp)
736

Lin Jen-Shin's avatar
Lin Jen-Shin committed
737
738
739
        project.update(updated_at: timestamp,
                       last_activity_at: timestamp - 1.hour,
                       last_repository_updated_at: nil)
740

741
        expect(project.last_activity_date).to be_like_time(timestamp)
742
      end
743
744
    end
  end
745

746
  describe '#get_issue' do
747
    let(:project) { create(:project) }
Stan Hu's avatar
Stan Hu committed
748
    let!(:issue)  { create(:issue, project: project) }
749
750
751
    let(:user)    { create(:user) }

    before do
752
      project.add_developer(user)
753
    end
754
755
756

    context 'with default issues tracker' do
      it 'returns an issue' do
757
        expect(project.get_issue(issue.iid, user)).to eq issue
758
759
      end

Stan Hu's avatar
Stan Hu committed
760
761
762
763
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      end

764
      it 'returns nil when no issue found' do
765
766
767
768
769
770
        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
771
772
773
774
      end
    end

    context 'with external issues tracker' do
775
      let!(:internal_issue) { create(:issue, project: project) }
776
      before do
777
        allow(project).to receive(:external_issue_tracker).and_return(true)
778
779
      end

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
811
812
813
814
815
816
      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
817
818
819
820
821
      end
    end
  end

  describe '#issue_exists?' do
822
    let(:project) { create(:project) }
823
824
825
826
827
828
829
830
831
832
833
834

    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

835
  describe '#to_param' do
836
837
    context 'with namespace' do
      before do
838
        @group = create(:group, name: 'gitlab')
839
        @project = create(:project, name: 'gitlabhq', namespace: @group)
840
841
      end

Vinnie Okada's avatar
Vinnie Okada committed
842
      it { expect(@project.to_param).to eq('gitlabhq') }
843
    end
844
845
846

    context 'with invalid path' do
      it 'returns previous path to keep project suitable for use in URLs when persisted' do
847
        project = create(:project, path: 'gitlab')
848
849
850
851
852
853
854
        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
855
        project = build(:project, path: 'gitlab')
856
857
858
859
860
861
        project.path = 'foo&bar'

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

864
  describe '#repository' do
865
    let(:project) { create(:project, :repository) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
866

867
    it 'returns valid repo' do
868
      expect(project.repository).to be_kind_of(Repository)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
869
870
    end
  end
871

872
  describe '#default_issues_tracker?' do
873
    it "is true if used internal tracker" do
874
      project = build(:project)
875

876
      expect(project.default_issues_tracker?).to be_truthy
877
878
    end

879
    it "is false if used other tracker" do
880
881
882
883
      # NOTE: The current nature of this factory requires persistence
      project = create(:redmine_project)

      expect(project.default_issues_tracker?).to be_falsey
884
885
886
    end
  end

887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
  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

905
  describe '#external_issue_tracker' do
906
    let(:project) { create(:project) }
907
908
909
    let(:ext_project) { create(:redmine_project) }

    context 'on existing projects with no value for has_external_issue_tracker' do
910
      before do
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
        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

940
  describe '#cache_has_external_issue_tracker' do
941
    let(:project) { create(:project, has_external_issue_tracker: nil) }
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959

    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
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
992
993
994
995
996
997

    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
998
999
  end

1000
  describe '#has_wiki?' do
1001
1002
1003
    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) }
1004
1005
1006
1007
1008
1009
1010
1011

    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

1012
  describe '#external_wiki' do
1013
    let(:project) { create(:project) }
1014

1015
1016
1017
1018
1019
    context 'with an active external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: true)
        project.external_wiki
      end
1020

1021
1022
1023
      it 'sets :has_external_wiki as true' do
        expect(project.has_external_wiki).to be(true)
      end
1024

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

1028
1029
1030
1031
        project.services.external_wikis.first.destroy

        expect(project.has_external_wiki).to be(false)
      end
1032
1033
    end

1034
1035
1036
1037
    context 'with an inactive external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: false)
      end
1038

1039
1040
1041
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end
1042
1043
    end

1044
1045
1046
1047
    context 'with no external wiki' do
      before do
        project.external_wiki
      end
1048

1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
      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
1060
1061
1062
    end
  end

1063
1064
  describe '#star_count' do
    it 'counts stars from multiple users' do
1065
1066
      user1 = create(:user)
      user2 = create(:user)
1067
      project = create(:project, :public)
Ciro Santilli's avatar
Ciro Santilli committed
1068
1069

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