Read about our upcoming Code of Conduct on this issue

This instance will be upgraded to Heptapod 0.28.1 on 2022-01-26 at 16:00 UTC+1 (a few minutes of down time)

project_spec.rb 147 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

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

106
107
    context 'after initialized' do
      it "has a project_feature" do
108
        expect(described_class.new.project_feature).to be_present
109
110
111
      end
    end

112
113
114
115
116
117
118
119
120
    context 'when creating a new project' do
      it 'automatically creates a CI/CD settings row' do
        project = create(:project)

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

121
122
123
124
    context 'updating cd_cd_settings' do
      it 'does not raise an error' do
        project = create(:project)

James Lopez's avatar
James Lopez committed
125
        expect { project.update(ci_cd_settings: nil) }.not_to raise_exception
126
127
128
      end
    end

129
    describe '#members & #requesters' do
130
      let(:project) { create(:project, :public, :access_requestable) }
131
132
133
134
      let(:requester) { create(:user) }
      let(:developer) { create(:user) }
      before do
        project.request_access(requester)
135
        project.add_developer(developer)
136
137
      end

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

143
    describe 'ci_pipelines association' do
144
145
      it 'returns only pipelines from ci_sources' do
        expect(Ci::Pipeline).to receive(:ci_sources).and_call_original
146

147
        subject.ci_pipelines
148
149
      end
    end
gitlabhq's avatar
gitlabhq committed
150
151
  end

152
153
154
155
156
157
158
159
  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) }
160
161
  end

162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
  describe '.missing_kubernetes_namespace' do
    let!(:project) { create(:project) }
    let!(:cluster) { create(:cluster, :provided_by_user, :group) }
    let(:kubernetes_namespaces) { project.kubernetes_namespaces }

    subject { described_class.missing_kubernetes_namespace(kubernetes_namespaces) }

    it { is_expected.to contain_exactly(project) }

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

      it { is_expected.to be_empty }
    end
  end

180
  describe 'validation' do
181
    let!(:project) { create(:project) }
182

183
184
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
185
    it { is_expected.to validate_length_of(:name).is_at_most(255) }
186
    it { is_expected.to validate_presence_of(:path) }
187
188
    it { is_expected.to validate_length_of(:path).is_at_most(255) }
    it { is_expected.to validate_length_of(:description).is_at_most(2000) }
189
190
191
    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) }
192
    it { is_expected.not_to allow_value('/test/foo').for(:ci_config_path) }
193
194
    it { is_expected.to validate_presence_of(:creator) }
    it { is_expected.to validate_presence_of(:namespace) }
195
    it { is_expected.to validate_presence_of(:repository_storage) }
196

197
198
199
200
201
202
203
204
    it 'validates build timeout constraints' do
      is_expected.to validate_numericality_of(:build_timeout)
        .only_integer
        .is_greater_than_or_equal_to(10.minutes)
        .is_less_than(1.month)
        .with_message('needs to be beetween 10 minutes and 1 month')
    end

205
    it 'does not allow new projects beyond user limits' do
206
      project2 = build(:project)
207
208
209
210
211
212
213

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

214
      expect(project2).not_to be_valid
215
    end
216
217
218

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

          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
228
        it 'has an error on the name attribute' do
229
230
          project_with_wiki_suffix = create(:project, path: 'foo.wiki')
          new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
231
232
233
234
235
236

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

238
    context 'repository storages inclusion' do
239
      let(:project2) { build(:project, repository_storage: 'missing') }
240
241

      before do
242
        storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
243
244
245
        allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
      end

246
      it "does not allow repository storages that don't match a label in the configuration" do
247
248
249
250
        expect(project2).not_to be_valid
        expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
      end
    end
251

252
253
254
    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
255

256
257
        expect(project).not_to be_valid
      end
258

259
260
261
      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'
262

263
264
        expect(project).to be_valid
      end
265

266
267
      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')
268

269
270
        expect(project).not_to be_valid
      end
James Lopez's avatar
James Lopez committed
271

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

275
276
        expect(project).to be_valid
      end
277

278
279
      it 'allows an empty URI' do
        project = build(:project, import_url: '')
280

281
282
        expect(project).to be_valid
      end
283

284
285
      it 'does not produce import data on an empty URI' do
        project = build(:project, import_url: '')
286

287
288
        expect(project.import_data).to be_nil
      end
289

290
291
      it 'does not produce import data on an invalid URI' do
        project = build(:project, import_url: 'test://')
292

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

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

299
300
301
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Requests to localhost are not allowed')
      end
302

303
304
305
306
307
308
309
      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

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

313
314
315
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Only allowed ports are 80, 443')
      end
316

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

321
322
323
        expect(project).to be_invalid
        expect(project.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
      end
324

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

328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
        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
346
347
    end

348
349
    describe 'project pending deletion' do
      let!(:project_pending_deletion) do
350
        create(:project,
351
352
353
               pending_delete: true)
      end
      let(:new_project) do
354
        build(:project,
355
356
357
358
359
360
361
362
363
364
365
366
              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
367
368
369

    describe 'path validation' do
      it 'allows paths reserved on the root namespace' do
370
        project = build(:project, path: 'api')
371
372
373
374
375

        expect(project).to be_valid
      end

      it 'rejects paths reserved on another level' do
376
        project = build(:project, path: 'tree')
377
378
379

        expect(project).not_to be_valid
      end
380
381
382

      it 'rejects nested paths' do
        parent = create(:group, :nested, path: 'environments')
383
        project = build(:project, path: 'folders', namespace: parent)
384
385
386

        expect(project).not_to be_valid
      end
387
388
389

      it 'allows a reserved group name' do
        parent = create(:group)
390
        project = build(:project, path: 'avatar', namespace: parent)
391
392
393

        expect(project).to be_valid
      end
394
395
396
397
398
399

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

        expect(project).to be_valid
      end
400
    end
gitlabhq's avatar
gitlabhq committed
401
  end
402

403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
  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

420
      it 'returns .external pipelines' do
421
422
423
424
425
426
        expect(project.all_pipelines).to all(have_attributes(source: 'external'))
        expect(project.all_pipelines.size).to eq(1)
      end
    end
  end

427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
  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

444
      it 'returns .external pipelines' do
445
446
447
448
449
450
        expect(project.ci_pipelines).to all(have_attributes(source: 'external'))
        expect(project.ci_pipelines.size).to eq(1)
      end
    end
  end

451
  describe 'project token' do
452
    it 'sets an random token if none provided' do
453
      project = FactoryBot.create(:project, runners_token: '')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
454
      expect(project.runners_token).not_to eq('')
455
456
    end

ubudzisz's avatar
ubudzisz committed
457
    it 'does not set an random token if one provided' do
458
      project = FactoryBot.create(:project, runners_token: 'my-token')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
459
      expect(project.runners_token).to eq('my-token')
460
461
    end
  end
gitlabhq's avatar
gitlabhq committed
462

463
  describe 'Respond to' do
464
465
466
467
468
    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) }
469
    it { is_expected.to respond_to(:full_path) }
gitlabhq's avatar
gitlabhq committed
470
471
  end

472
  describe 'delegation' do
473
    [:add_guest, :add_reporter, :add_developer, :add_maintainer, :add_user, :add_users].each do |method|
474
475
476
477
478
      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) }
479
480
    it { is_expected.to delegate_method(:group_clusters_enabled?).to(:group).with_arguments(allow_nil: true) }
    it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
481
    it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
482
483
  end

484
485
486
487
488
489
490
491
492
  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

493
  describe '#to_reference' do
494
    let(:owner)     { create(:user, name: 'Gitlab') }
495
    let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
496
    let(:project)   { create(:project, path: 'sample-project', namespace: namespace) }
497
    let(:group)     { create(:group, name: 'Group', path: 'sample-group') }
498

499
    context 'when nil argument' do
500
501
502
503
504
      it 'returns nil' do
        expect(project.to_reference).to be_nil
      end
    end

505
    context 'when full is true' do
506
      it 'returns complete path to the project' do
507
508
509
        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'
510
511
512
513
514
515
516
517
518
519
      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
520
      let(:another_namespace_project) { create(:project, name: 'another-project') }
521
522
523
524
525
526
527

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

530
      it 'returns path to the project' do
531
532
533
        expect(project.to_reference(another_project)).to eq 'sample-project'
      end
    end
534

535
536
    context 'when different namespace / cross-project argument' do
      let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
537
      let(:another_project)   { create(:project, path: 'another-project', namespace: another_namespace) }
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554

      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
555
556
      end
    end
557
558
559
560
561
  end

  describe '#to_human_reference' do
    let(:owner) { create(:user, name: 'Gitlab') }
    let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
562
    let(:project) { create(:project, name: 'Sample project', namespace: namespace) }
563
564
565
566
567
568
569
570
571
572
573
574
575
576

    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
577
      let(:another_namespace_project) { create(:project, name: 'another-project') }
578
579
580
581
582
583
584

      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
585
      let(:another_project) { create(:project, namespace: namespace) }
586
587
588
589

      it 'returns name of the project' do
        expect(project.to_human_reference(another_project)).to eq 'Sample project'
      end
590
591
592
    end
  end

593
  describe '#merge_method' do
594
595
596
597
598
599
600
    using RSpec::Parameterized::TableSyntax

    where(:ff, :rebase, :method) do
      true  | true  | :ff
      true  | false | :ff
      false | true  | :rebase_merge
      false | false | :merge
601
602
    end

603
604
605
606
607
608
    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) }
609
610
611
    end
  end

612
  it 'returns valid url to repo' do
613
    project = described_class.new(path: 'somewhere')
614
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
615
616
  end

Douwe Maan's avatar
Douwe Maan committed
617
  describe "#web_url" do
618
    let(:project) { create(:project, path: "somewhere") }
Douwe Maan's avatar
Douwe Maan committed
619
620

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

Imre Farkas's avatar
Imre Farkas committed
625
626
  describe "#readme_url" do
    context 'with a non-existing repository' do
627
      let(:project) { create(:project) }
Imre Farkas's avatar
Imre Farkas committed
628

629
      it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
630
631
632
633
634
635
        expect(project.readme_url).to be_nil
      end
    end

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

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

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

Imre Farkas's avatar
Imre Farkas committed
646
        it 'returns the README' do
647
          expect(project.readme_url).to eq("#{project.web_url}/blob/master/README.md")
Imre Farkas's avatar
Imre Farkas committed
648
649
650
651
652
        end
      end
    end
  end

653
  describe "#new_issuable_address" do
654
    let(:project) { create(:project, path: "somewhere") }
655
656
    let(:user) { create(:user) }

657
658
659
660
661
662
    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
663
        address = "p+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-issue@gl.ab"
664

665
666
667
668
        expect(project.new_issuable_address(user, 'issue')).to eq(address)
      end

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

        expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
672
      end
673
674
675
676

      it 'returns nil with invalid address type' do
        expect(project.new_issuable_address(user, 'invalid_param')).to be_nil
      end
677
678
679
680
681
682
    end

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

684
      it 'returns nil' do
685
686
687
688
689
        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
690
      end
691
692
693
    end
  end

694
  describe 'last_activity methods' do
695
696
    let(:timestamp) { 2.hours.ago }
    # last_activity_at gets set to created_at upon creation
697
    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
698

699
    describe 'last_activity' do
700
      it 'alias last_activity to last_event' do
701
        last_event = create(:event, :closed, project: project)
702

703
        expect(project.last_activity).to eq(last_event)
704
      end
gitlabhq's avatar
gitlabhq committed
705
706
    end

707
708
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
709
        new_event = create(:event, :closed, project: project, created_at: Time.now)
710

711
        project.reload
712
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
713
      end
714

715
      it 'returns the project\'s last update date if it has no events' do
716
        expect(project.last_activity_date).to eq(project.updated_at)
717
      end
718
719

      it 'returns the most recent timestamp' do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
720
721
722
        project.update(updated_at: nil,
                       last_activity_at: timestamp,
                       last_repository_updated_at: timestamp - 1.hour)
723

724
        expect(project.last_activity_date).to be_like_time(timestamp)
725

Lin Jen-Shin's avatar
Lin Jen-Shin committed
726
727
728
        project.update(updated_at: timestamp,
                       last_activity_at: timestamp - 1.hour,
                       last_repository_updated_at: nil)
729

730
        expect(project.last_activity_date).to be_like_time(timestamp)
731
      end
732
733
    end
  end
734

735
  describe '#get_issue' do
736
    let(:project) { create(:project) }
Stan Hu's avatar
Stan Hu committed
737
    let!(:issue)  { create(:issue, project: project) }
738
739
740
    let(:user)    { create(:user) }

    before do
741
      project.add_developer(user)
742
    end
743
744
745

    context 'with default issues tracker' do
      it 'returns an issue' do
746
        expect(project.get_issue(issue.iid, user)).to eq issue
747
748
      end

Stan Hu's avatar
Stan Hu committed
749
750
751
752
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      end

753
      it 'returns nil when no issue found' do
754
755
756
757
758
759
        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
760
761
762
763
      end
    end

    context 'with external issues tracker' do
764
      let!(:internal_issue) { create(:issue, project: project) }
765
      before do
766
        allow(project).to receive(:external_issue_tracker).and_return(true)
767
768
      end

769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
      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
806
807
808
809
810
      end
    end
  end

  describe '#issue_exists?' do
811
    let(:project) { create(:project) }
812
813
814
815
816
817
818
819
820
821
822
823

    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

824
  describe '#to_param' do
825
826
    context 'with namespace' do
      before do
827
        @group = create(:group, name: 'gitlab')
828
        @project = create(:project, name: 'gitlabhq', namespace: @group)
829
830
      end

Vinnie Okada's avatar
Vinnie Okada committed
831
      it { expect(@project.to_param).to eq('gitlabhq') }
832
    end
833
834
835

    context 'with invalid path' do
      it 'returns previous path to keep project suitable for use in URLs when persisted' do
836
        project = create(:project, path: 'gitlab')
837
838
839
840
841
842
843
        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
844
        project = build(:project, path: 'gitlab')
845
846
847
848
849
850
        project.path = 'foo&bar'

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

853
  describe '#repository' do
854
    let(:project) { create(:project, :repository) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
855

856
    it 'returns valid repo' do
857
      expect(project.repository).to be_kind_of(Repository)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
858
859
    end
  end
860

861
  describe '#default_issues_tracker?' do
862
    it "is true if used internal tracker" do
863
      project = build(:project)
864

865
      expect(project.default_issues_tracker?).to be_truthy
866
867
    end

868
    it "is false if used other tracker" do
869
870
871
872
      # NOTE: The current nature of this factory requires persistence
      project = create(:redmine_project)

      expect(project.default_issues_tracker?).to be_falsey
873
874
875
    end
  end

876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
  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

894
  describe '#external_issue_tracker' do
895
    let(:project) { create(:project) }
896
897
898
    let(:ext_project) { create(:redmine_project) }

    context 'on existing projects with no value for has_external_issue_tracker' do
899
      before do
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
        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

929
  describe '#cache_has_external_issue_tracker' do
930
    let(:project) { create(:project, has_external_issue_tracker: nil) }
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948

    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
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
978
979
980
981
982
983
984
985
986

    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
987
988
  end

989
  describe '#has_wiki?' do
990
991
992
    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) }
993
994
995
996
997
998
999
1000

    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

1001
  describe '#external_wiki' do
1002
    let(:project) { create(:project) }
1003

1004
1005
1006
1007
1008
    context 'with an active external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: true)
        project.external_wiki
      end
1009

1010
1011
1012
      it 'sets :has_external_wiki as true' do
        expect(project.has_external_wiki).to be(true)
      end
1013

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

1017
1018
1019
1020
        project.services.external_wikis.first.destroy

        expect(project.has_external_wiki).to be(false)
      end
1021
1022
    end

1023
1024
1025
1026
    context 'with an inactive external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: false)
      end
1027

1028
1029
1030
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end
1031
1032
    end

1033
1034
1035
1036
    context 'with no external wiki' do
      before do
        project.external_wiki
      end
1037

1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
      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
1049
1050
1051
    end
  end

1052
1053
  describe '#star_count' do
    it 'counts stars from multiple users' do
1054
1055
      user1 = create(:user)
      user2 = create(:user)
1056
      project = create(:project, :public)
Ciro Santilli's avatar
Ciro Santilli committed
1057
1058

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

Ciro Santilli's avatar
Ciro Santilli committed
1060
      user1.toggle_star(project)
1061
1062
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
1063
      user2.toggle_star(project)
1064
1065
1066
      project.reload
      expect(project.reload.star_count).to eq(2)

Ciro Santilli's avatar
Ciro Santilli committed
1067
      user1.toggle_star(project)
1068
1069
1070
      project.reload
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
1071
      user2.toggle_star(project)