Read about our upcoming Code of Conduct on this issue

project_spec.rb 132 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
58
    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(:hipchat_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) }
59
    it { is_expected.to have_one(:project_repository) }
60
61
    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
62
    it { is_expected.to have_one(:last_event).class_name('Event') }
63
    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
64
    it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
65
    it { is_expected.to have_one(:error_tracking_setting).class_name('ErrorTracking::ProjectErrorTrackingSetting') }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
66
    it { is_expected.to have_many(:commit_statuses) }
67
    it { is_expected.to have_many(:ci_pipelines) }
68
    it { is_expected.to have_many(:builds) }
69
    it { is_expected.to have_many(:build_trace_section_names)}
70
71
72
73
    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
74
    it { is_expected.to have_many(:pages_domains) }
75
76
    it { is_expected.to have_many(:labels).class_name('ProjectLabel') }
    it { is_expected.to have_many(:users_star_projects) }
77
    it { is_expected.to have_many(:repository_languages) }
78
79
80
81
82
83
84
    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) }
85
86
    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
87
    it { is_expected.to have_many(:uploads) }
88
    it { is_expected.to have_many(:pipeline_schedules) }
89
    it { is_expected.to have_many(:members_and_requesters) }
90
    it { is_expected.to have_many(:clusters) }
91
    it { is_expected.to have_many(:kubernetes_namespaces) }
92
    it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
93
    it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
94
    it { is_expected.to have_many(:lfs_file_locks) }
Mayra Cabrera's avatar
Mayra Cabrera committed
95
96
    it { is_expected.to have_many(:project_deploy_tokens) }
    it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
97

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

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

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

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

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

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

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

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

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

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

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

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

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

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
207
208
209
    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

210
    it 'does not allow new projects beyond user limits' do
211
      project2 = build(:project)
212
213
      allow(project2).to receive(:creator).and_return(double(can_create_project?: false, projects_limit: 0).as_null_object)
      expect(project2).not_to be_valid
Phil Hughes's avatar
Phil Hughes committed
214
      expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/)
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
  describe 'project token' do
404
    it 'sets an random token if none provided' do
405
      project = FactoryBot.create(:project, runners_token: '')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
406
      expect(project.runners_token).not_to eq('')
407
408
    end

ubudzisz's avatar
ubudzisz committed
409
    it 'does not set an random token if one provided' do
410
      project = FactoryBot.create(:project, runners_token: 'my-token')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
411
      expect(project.runners_token).to eq('my-token')
412
413
    end
  end
gitlabhq's avatar
gitlabhq committed
414

415
  describe 'Respond to' do
416
417
418
419
420
    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) }
421
    it { is_expected.to respond_to(:full_path) }
gitlabhq's avatar
gitlabhq committed
422
423
  end

424
  describe 'delegation' do
425
    [:add_guest, :add_reporter, :add_developer, :add_maintainer, :add_user, :add_users].each do |method|
426
427
428
429
430
      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) }
431
432
    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) }
433
434
  end

435
436
437
438
439
440
441
442
443
  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

444
  describe '#to_reference' do
445
    let(:owner)     { create(:user, name: 'Gitlab') }
446
    let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
447
    let(:project)   { create(:project, path: 'sample-project', namespace: namespace) }
448
    let(:group)     { create(:group, name: 'Group', path: 'sample-group') }
449

450
    context 'when nil argument' do
451
452
453
454
455
      it 'returns nil' do
        expect(project.to_reference).to be_nil
      end
    end

456
    context 'when full is true' do
457
      it 'returns complete path to the project' do
458
459
460
        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'
461
462
463
464
465
466
467
468
469
470
      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
471
      let(:another_namespace_project) { create(:project, name: 'another-project') }
472
473
474
475
476
477
478

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

481
      it 'returns path to the project' do
482
483
484
        expect(project.to_reference(another_project)).to eq 'sample-project'
      end
    end
485

486
487
    context 'when different namespace / cross-project argument' do
      let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
488
      let(:another_project)   { create(:project, path: 'another-project', namespace: another_namespace) }
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505

      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
506
507
      end
    end
508
509
510
511
512
  end

  describe '#to_human_reference' do
    let(:owner) { create(:user, name: 'Gitlab') }
    let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
513
    let(:project) { create(:project, name: 'Sample project', namespace: namespace) }
514
515
516
517
518
519
520
521
522
523
524
525
526
527

    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
528
      let(:another_namespace_project) { create(:project, name: 'another-project') }
529
530
531
532
533
534
535

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

      it 'returns name of the project' do
        expect(project.to_human_reference(another_project)).to eq 'Sample project'
      end
541
542
543
    end
  end

544
  describe '#merge_method' do
545
546
547
548
549
550
551
    using RSpec::Parameterized::TableSyntax

    where(:ff, :rebase, :method) do
      true  | true  | :ff
      true  | false | :ff
      false | true  | :rebase_merge
      false | false | :merge
552
553
    end

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

563
  it 'returns valid url to repo' do
564
    project = described_class.new(path: 'somewhere')
565
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
566
567
  end

Douwe Maan's avatar
Douwe Maan committed
568
  describe "#web_url" do
569
    let(:project) { create(:project, path: "somewhere") }
Douwe Maan's avatar
Douwe Maan committed
570
571

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

Imre Farkas's avatar
Imre Farkas committed
576
577
  describe "#readme_url" do
    context 'with a non-existing repository' do
578
      let(:project) { create(:project) }
Imre Farkas's avatar
Imre Farkas committed
579

580
      it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
581
582
583
584
585
586
        expect(project.readme_url).to be_nil
      end
    end

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

589
        it 'returns nil' do
Imre Farkas's avatar
Imre Farkas committed
590
591
592
593
594
          expect(project.readme_url).to be_nil
        end
      end

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

Imre Farkas's avatar
Imre Farkas committed
597
        it 'returns the README' do
598
          expect(project.readme_url).to eq("#{project.web_url}/blob/master/README.md")
Imre Farkas's avatar
Imre Farkas committed
599
600
601
602
603
        end
      end
    end
  end

604
  describe "#new_issuable_address" do
605
    let(:project) { create(:project, path: "somewhere") }
606
607
    let(:user) { create(:user) }

608
609
610
611
612
613
    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
614
        address = "p+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-issue@gl.ab"
615

616
617
618
619
        expect(project.new_issuable_address(user, 'issue')).to eq(address)
      end

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

        expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
623
      end
624
625
626
627

      it 'returns nil with invalid address type' do
        expect(project.new_issuable_address(user, 'invalid_param')).to be_nil
      end
628
629
630
631
632
633
    end

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

635
      it 'returns nil' do
636
637
638
639
640
        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
641
      end
642
643
644
    end
  end

645
  describe 'last_activity methods' do
646
647
    let(:timestamp) { 2.hours.ago }
    # last_activity_at gets set to created_at upon creation
648
    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
649

650
    describe 'last_activity' do
651
      it 'alias last_activity to last_event' do
652
        last_event = create(:event, :closed, project: project)
653

654
        expect(project.last_activity).to eq(last_event)
655
      end
gitlabhq's avatar
gitlabhq committed
656
657
    end

658
659
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
660
        new_event = create(:event, :closed, project: project, created_at: Time.now)
661

662
        project.reload
663
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
664
      end
665

666
      it 'returns the project\'s last update date if it has no events' do
667
        expect(project.last_activity_date).to eq(project.updated_at)
668
      end
669
670

      it 'returns the most recent timestamp' do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
671
672
673
        project.update(updated_at: nil,
                       last_activity_at: timestamp,
                       last_repository_updated_at: timestamp - 1.hour)
674

675
        expect(project.last_activity_date).to be_like_time(timestamp)
676

Lin Jen-Shin's avatar
Lin Jen-Shin committed
677
678
679
        project.update(updated_at: timestamp,
                       last_activity_at: timestamp - 1.hour,
                       last_repository_updated_at: nil)
680

681
        expect(project.last_activity_date).to be_like_time(timestamp)
682
      end
683
684
    end
  end
685

686
  describe '#get_issue' do
687
    let(:project) { create(:project) }
Stan Hu's avatar
Stan Hu committed
688
    let!(:issue)  { create(:issue, project: project) }
689
690
691
    let(:user)    { create(:user) }

    before do
692
      project.add_developer(user)
693
    end
694
695
696

    context 'with default issues tracker' do
      it 'returns an issue' do
697
        expect(project.get_issue(issue.iid, user)).to eq issue
698
699
      end

Stan Hu's avatar
Stan Hu committed
700
701
702
703
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      end

704
      it 'returns nil when no issue found' do
705
706
707
708
709
710
        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
711
712
713
714
      end
    end

    context 'with external issues tracker' do
715
      let!(:internal_issue) { create(:issue, project: project) }
716
      before do
717
        allow(project).to receive(:external_issue_tracker).and_return(true)
718
719
      end

720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
      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
757
758
759
760
761
      end
    end
  end

  describe '#issue_exists?' do
762
    let(:project) { create(:project) }
763
764
765
766
767
768
769
770
771
772
773
774

    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

775
  describe '#to_param' do
776
777
    context 'with namespace' do
      before do
778
        @group = create(:group, name: 'gitlab')
779
        @project = create(:project, name: 'gitlabhq', namespace: @group)
780
781
      end

Vinnie Okada's avatar
Vinnie Okada committed
782
      it { expect(@project.to_param).to eq('gitlabhq') }
783
    end
784
785
786

    context 'with invalid path' do
      it 'returns previous path to keep project suitable for use in URLs when persisted' do
787
        project = create(:project, path: 'gitlab')
788
789
790
791
792
793
794
        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
795
        project = build(:project, path: 'gitlab')
796
797
798
799
800
801
        project.path = 'foo&bar'

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

804
  describe '#repository' do
805
    let(:project) { create(:project, :repository) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
806

807
    it 'returns valid repo' do
808
      expect(project.repository).to be_kind_of(Repository)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
809
810
    end
  end
811

812
  describe '#default_issues_tracker?' do
813
    it "is true if used internal tracker" do
814
      project = build(:project)
815

816
      expect(project.default_issues_tracker?).to be_truthy
817
818
    end

819
    it "is false if used other tracker" do
820
821
822
823
      # NOTE: The current nature of this factory requires persistence
      project = create(:redmine_project)

      expect(project.default_issues_tracker?).to be_falsey
824
825
826
    end
  end

827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
  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

845
  describe '#external_issue_tracker' do
846
    let(:project) { create(:project) }
847
848
849
    let(:ext_project) { create(:redmine_project) }

    context 'on existing projects with no value for has_external_issue_tracker' do
850
      before do
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
        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

880
  describe '#cache_has_external_issue_tracker' do
881
    let(:project) { create(:project, has_external_issue_tracker: nil) }
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899

    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
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
929
930
931
932
933
934
935
936
937

    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
938
939
  end

940
  describe '#has_wiki?' do
941
942
943
    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) }
944
945
946
947
948
949
950
951

    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

952
  describe '#external_wiki' do
953
    let(:project) { create(:project) }
954

955
956
957
958
959
    context 'with an active external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: true)
        project.external_wiki
      end
960

961
962
963
      it 'sets :has_external_wiki as true' do
        expect(project.has_external_wiki).to be(true)
      end
964

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

968
969
970
971
        project.services.external_wikis.first.destroy

        expect(project.has_external_wiki).to be(false)
      end
972
973
    end

974
975
976
977
    context 'with an inactive external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: false)
      end
978

979
980
981
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end
982
983
    end

984
985
986
987
    context 'with no external wiki' do
      before do
        project.external_wiki
      end
988

989
990
991
992
993
994
995
996
997
998
999
      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
1000
1001
1002
    end
  end

1003
1004
  describe '#star_count' do
    it 'counts stars from multiple users' do
1005
1006
      user1 = create(:user)
      user2 = create(:user)
1007
      project = create(:project, :public)
Ciro Santilli's avatar
Ciro Santilli committed
1008
1009

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

Ciro Santilli's avatar
Ciro Santilli committed
1011
      user1.toggle_star(project)
1012
1013
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
1014
      user2.toggle_star(project)
1015
1016
1017
      project.reload
      expect(project.reload.star_count).to eq(2)

Ciro Santilli's avatar
Ciro Santilli committed
1018
      user1.toggle_star(project)
1019
1020
1021
      project.reload
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
1022
      user2.toggle_star(project)
1023
1024
1025
1026
      project.reload
      expect(project.reload.star_count).to eq(0)
    end

1027
    it 'counts stars on the right project' do
1028
      user = create(:user)
1029
1030
      project1 = create(:project, :public)
      project2 = create(:project, :public)
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057

      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)
Ciro Santilli's avatar
Ciro Santilli committed
1058
1059
    end
  end
1060

1061
  describe '#avatar_type' do
1062
    let(:project) { create(:project) }
1063

1064
    it 'is true if avatar is image' do
1065
      project