Read about our upcoming Code of Conduct on this issue

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

3
describe Project do
4
  include ProjectForksHelper
5
  include GitHelpers
6

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

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

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

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

107
108
109
110
111
112
113
114
115
    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

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

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

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

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

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

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

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

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

156
157
158
159
160
161
162
163
  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) }
164
165
  end

166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
  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

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

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

201
202
203
204
205
206
207
208
    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

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

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

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

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

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

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

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

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

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

256
257
258
    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
259

260
261
        expect(project).not_to be_valid
      end
262

263
264
265
      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'
266

267
268
        expect(project).to be_valid
      end
269

270
271
      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')
272

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

276
277
      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
278

279
280
        expect(project).to be_valid
      end
281

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

285
286
        expect(project).to be_valid
      end
287

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

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

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

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

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

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

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

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

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

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

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

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

332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
        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
350
351
    end

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

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

        expect(project).to be_valid
      end

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

        expect(project).not_to be_valid
      end
384
385
386

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

        expect(project).not_to be_valid
      end
391
392
393

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

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

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

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

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

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

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

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

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

431
  describe 'project token' do
432
    it 'sets an random token if none provided' do
433
      project = FactoryBot.create(:project, runners_token: '')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
434
      expect(project.runners_token).not_to eq('')
435
436
    end

ubudzisz's avatar
ubudzisz committed
437
    it 'does not set an random token if one provided' do
438
      project = FactoryBot.create(:project, runners_token: 'my-token')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
439
      expect(project.runners_token).to eq('my-token')
440
441
    end
  end
gitlabhq's avatar
gitlabhq committed
442

443
  describe 'Respond to' do
444
445
446
447
448
    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) }
449
    it { is_expected.to respond_to(:full_path) }
gitlabhq's avatar
gitlabhq committed
450
451
  end

452
  describe 'delegation' do
453
    [:add_guest, :add_reporter, :add_developer, :add_maintainer, :add_user, :add_users].each do |method|
454
455
456
457
458
      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) }
459
460
    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) }
461
    it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
462
463
  end

464
465
466
467
468
469
470
471
472
  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

473
  describe '#to_reference' do
474
    let(:owner)     { create(:user, name: 'Gitlab') }
475
    let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
476
    let(:project)   { create(:project, path: 'sample-project', namespace: namespace) }
477
    let(:group)     { create(:group, name: 'Group', path: 'sample-group') }
478

479
    context 'when nil argument' do
480
481
482
483
484
      it 'returns nil' do
        expect(project.to_reference).to be_nil
      end
    end

485
    context 'when full is true' do
486
      it 'returns complete path to the project' do
487
488
489
        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'
490
491
492
493
494
495
496
497
498
499
      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
500
      let(:another_namespace_project) { create(:project, name: 'another-project') }
501
502
503
504
505
506
507

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

510
      it 'returns path to the project' do
511
512
513
        expect(project.to_reference(another_project)).to eq 'sample-project'
      end
    end
514

515
516
    context 'when different namespace / cross-project argument' do
      let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
517
      let(:another_project)   { create(:project, path: 'another-project', namespace: another_namespace) }
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534

      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
535
536
      end
    end
537
538
539
540
541
  end

  describe '#to_human_reference' do
    let(:owner) { create(:user, name: 'Gitlab') }
    let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
542
    let(:project) { create(:project, name: 'Sample project', namespace: namespace) }
543
544
545
546
547
548
549
550
551
552
553
554
555
556

    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
557
      let(:another_namespace_project) { create(:project, name: 'another-project') }
558
559
560
561
562
563
564

      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
565
      let(:another_project) { create(:project, namespace: namespace) }
566
567
568
569

      it 'returns name of the project' do
        expect(project.to_human_reference(another_project)).to eq 'Sample project'
      end
570
571
572
    end
  end

573
  describe '#merge_method' do
574
575
576
577
578
579
580
    using RSpec::Parameterized::TableSyntax

    where(:ff, :rebase, :method) do
      true  | true  | :ff
      true  | false | :ff
      false | true  | :rebase_merge
      false | false | :merge
581
582
    end

583
584
585
586
587
588
    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) }
589
590
591
    end
  end

592
  it 'returns valid url to repo' do
593
    project = described_class.new(path: 'somewhere')
594
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
595
596
  end

Douwe Maan's avatar
Douwe Maan committed
597
  describe "#web_url" do
598
    let(:project) { create(:project, path: "somewhere") }
Douwe Maan's avatar
Douwe Maan committed
599
600

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

Imre Farkas's avatar
Imre Farkas committed
605
606
  describe "#readme_url" do
    context 'with a non-existing repository' do
607
      let(:project) { create(:project) }
Imre Farkas's avatar
Imre Farkas committed
608

609
      it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
610
611
612
613
614
615
        expect(project.readme_url).to be_nil
      end
    end

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

618
        it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
619
620
621
622
623
          expect(project.readme_url).to be_nil
        end
      end

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

Imre Farkas's avatar
Imre Farkas committed
626
        it 'returns the README' do
627
          expect(project.readme_url).to eq("#{project.web_url}/blob/master/README.md")
Imre Farkas's avatar
Imre Farkas committed
628
629
630
631
632
        end
      end
    end
  end

633
  describe "#new_issuable_address" do
634
    let(:project) { create(:project, path: "somewhere") }
635
636
    let(:user) { create(:user) }

637
638
639
640
641
642
    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
643
        address = "p+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-issue@gl.ab"
644

645
646
647
648
        expect(project.new_issuable_address(user, 'issue')).to eq(address)
      end

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

        expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
652
      end
653
654
655
656

      it 'returns nil with invalid address type' do
        expect(project.new_issuable_address(user, 'invalid_param')).to be_nil
      end
657
658
659
660
661
662
    end

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

664
      it 'returns nil' do
665
666
667
668
669
        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
670
      end
671
672
673
    end
  end

674
  describe 'last_activity methods' do
675
676
    let(:timestamp) { 2.hours.ago }
    # last_activity_at gets set to created_at upon creation
677
    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
678

679
    describe 'last_activity' do
680
      it 'alias last_activity to last_event' do
681
        last_event = create(:event, :closed, project: project)
682

683
        expect(project.last_activity).to eq(last_event)
684
      end
gitlabhq's avatar
gitlabhq committed
685
686
    end

687
688
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
689
        new_event = create(:event, :closed, project: project, created_at: Time.now)
690

691
        project.reload
692
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
693
      end
694

695
      it 'returns the project\'s last update date if it has no events' do
696
        expect(project.last_activity_date).to eq(project.updated_at)
697
      end
698
699

      it 'returns the most recent timestamp' do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
700
701
702
        project.update(updated_at: nil,
                       last_activity_at: timestamp,
                       last_repository_updated_at: timestamp - 1.hour)
703

704
        expect(project.last_activity_date).to be_like_time(timestamp)
705

Lin Jen-Shin's avatar
Lin Jen-Shin committed
706
707
708
        project.update(updated_at: timestamp,
                       last_activity_at: timestamp - 1.hour,
                       last_repository_updated_at: nil)
709

710
        expect(project.last_activity_date).to be_like_time(timestamp)
711
      end
712
713
    end
  end
714

715
  describe '#get_issue' do
716
    let(:project) { create(:project) }
Stan Hu's avatar
Stan Hu committed
717
    let!(:issue)  { create(:issue, project: project) }
718
719
720
    let(:user)    { create(:user) }

    before do
721
      project.add_developer(user)
722
    end
723
724
725

    context 'with default issues tracker' do
      it 'returns an issue' do
726
        expect(project.get_issue(issue.iid, user)).to eq issue
727
728
      end

Stan Hu's avatar
Stan Hu committed
729
730
731
732
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      end

733
      it 'returns nil when no issue found' do
734
735
736
737
738
739
        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
740
741
742
743
      end
    end

    context 'with external issues tracker' do
744
      let!(:internal_issue) { create(:issue, project: project) }
745
      before do
746
        allow(project).to receive(:external_issue_tracker).and_return(true)
747
748
      end

749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
      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
786
787
788
789
790
      end
    end
  end

  describe '#issue_exists?' do
791
    let(:project) { create(:project) }
792
793
794
795
796
797
798
799
800
801
802
803

    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

804
  describe '#to_param' do
805
806
    context 'with namespace' do
      before do
807
        @group = create(:group, name: 'gitlab')
808
        @project = create(:project, name: 'gitlabhq', namespace: @group)
809
810
      end

Vinnie Okada's avatar
Vinnie Okada committed
811
      it { expect(@project.to_param).to eq('gitlabhq') }
812
    end
813
814
815

    context 'with invalid path' do
      it 'returns previous path to keep project suitable for use in URLs when persisted' do
816
        project = create(:project, path: 'gitlab')
817
818
819
820
821
822
823
        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
824
        project = build(:project, path: 'gitlab')
825
826
827
828
829
830
        project.path = 'foo&bar'

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

833
  describe '#repository' do
834
    let(:project) { create(:project, :repository) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
835

836
    it 'returns valid repo' do
837
      expect(project.repository).to be_kind_of(Repository)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
838
839
    end
  end
840

841
  describe '#default_issues_tracker?' do
842
    it "is true if used internal tracker" do
843
      project = build(:project)
844

845
      expect(project.default_issues_tracker?).to be_truthy
846
847
    end

848
    it "is false if used other tracker" do
849
850
851
852
      # NOTE: The current nature of this factory requires persistence
      project = create(:redmine_project)

      expect(project.default_issues_tracker?).to be_falsey
853
854
855
    end
  end

856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
  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

874
  describe '#external_issue_tracker' do
875
    let(:project) { create(:project) }
876
877
878
    let(:ext_project) { create(:redmine_project) }

    context 'on existing projects with no value for has_external_issue_tracker' do
879
      before do
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
        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

909
  describe '#cache_has_external_issue_tracker' do
910
    let(:project) { create(:project, has_external_issue_tracker: nil) }
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928

    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
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966

    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
967
968
  end

969
  describe '#has_wiki?' do
970
971
972
    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) }
973
974
975
976
977
978
979
980

    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

981
  describe '#external_wiki' do
982
    let(:project) { create(:project) }
983

984
985
986
987
988
    context 'with an active external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: true)
        project.external_wiki
      end
989

990
991
992
      it 'sets :has_external_wiki as true' do
        expect(project.has_external_wiki).to be(true)
      end
993

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

997
998
999
1000
        project.services.external_wikis.first.destroy

        expect(project.has_external_wiki).to be(false)
      end
1001
1002
    end

1003
1004
1005
1006
    context 'with an inactive external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: false)
      end
1007

1008
1009
1010
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end
1011
1012
    end

1013
1014
1015
1016
    context 'with no external wiki' do
      before do
        project.external_wiki
      end
1017

1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
      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
1029
1030
1031
    end
  end

1032
1033
  describe '#star_count' do
    it 'counts stars from multiple users' do
1034
1035
      user1 = create(:user)
      user2 = create(:user)
1036
      project = create(:project, :public)
Ciro Santilli's avatar
Ciro Santilli committed
1037
1038

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

Ciro Santilli's avatar
Ciro Santilli committed
1040
      user1.toggle_star(project)
1041
1042
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
1043
      user2.toggle_star(project)
1044
1045
1046
      project.reload
      expect(project.reload.star_count).to eq(2)

Ciro Santilli's avatar
Ciro Santilli committed
1047
      user1.toggle_star(project)
1048
1049
1050
      project.reload
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
1051
      user2.toggle_star(project)
1052
1053
1054
1055
      project.reload
      expect(project.reload.star_count).to eq(0)
    end

1056
    it 'counts stars on the right project' do
1057
      user = create(:user)
1058
1059
      project1 = create(:project, :public)
      project2 = create(:project, :public)
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086

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

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

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

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

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