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

3
describe Project do
4
5
  include ProjectForksHelper

6
  describe 'associations' do
7
8
9
10
    it { is_expected.to belong_to(:group) }
    it { is_expected.to belong_to(:namespace) }
    it { is_expected.to belong_to(:creator).class_name('User') }
    it { is_expected.to have_many(:users) }
ubudzisz's avatar
ubudzisz committed
11
    it { is_expected.to have_many(:services) }
12
13
14
15
16
    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) }
17
    it { is_expected.to have_many(:users).through(:project_members) }
18
19
20
21
    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) }
22
    it { is_expected.to have_many(:deploy_keys) }
23
24
25
26
27
28
    it { is_expected.to have_many(:hooks) }
    it { is_expected.to have_many(:protected_branches) }
    it { is_expected.to have_one(:forked_project_link) }
    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) }
29
    it { is_expected.to have_one(:hangouts_chat_service) }
Matt Coleman's avatar
Matt Coleman committed
30
    it { is_expected.to have_one(:packagist_service) }
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    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) }
    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) }
    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
57
58
    it { is_expected.to have_one(:last_event).class_name('Event') }
    it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
59
    it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
60
    it { is_expected.to have_many(:commit_statuses) }
61
    it { is_expected.to have_many(:pipelines) }
62
    it { is_expected.to have_many(:builds) }
63
    it { is_expected.to have_many(:build_trace_section_names)}
64
65
66
67
    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
68
    it { is_expected.to have_many(:pages_domains) }
69
70
    it { is_expected.to have_many(:labels).class_name('ProjectLabel') }
    it { is_expected.to have_many(:users_star_projects) }
71
    it { is_expected.to have_many(:repository_languages) }
72
73
74
75
76
77
78
    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) }
ubudzisz's avatar
ubudzisz committed
79
    it { is_expected.to have_many(:forks).through(:forked_project_links) }
Jan Provaznik's avatar
Jan Provaznik committed
80
    it { is_expected.to have_many(:uploads) }
81
    it { is_expected.to have_many(:pipeline_schedules) }
82
    it { is_expected.to have_many(:members_and_requesters) }
83
    it { is_expected.to have_many(:clusters) }
84
    it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
85
    it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
86
    it { is_expected.to have_many(:lfs_file_locks) }
Mayra Cabrera's avatar
Mayra Cabrera committed
87
88
    it { is_expected.to have_many(:project_deploy_tokens) }
    it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) }
89

90
91
    context 'after initialized' do
      it "has a project_feature" do
92
        expect(described_class.new.project_feature).to be_present
93
94
95
      end
    end

96
97
98
99
100
101
102
103
104
    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

105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
    context 'Site Statistics' do
      context 'when creating a new project' do
        it 'tracks project in SiteStatistic' do
          expect { create(:project) }.to change { SiteStatistic.fetch.repositories_count }.by(1)
        end
      end

      context 'when deleting a project' do
        it 'untracks project in SiteStatistic' do
          project = create(:project)

          expect { project.destroy }.to change { SiteStatistic.fetch.repositories_count }.by(-1)
        end
      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
144
145
146

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

147
        expect { subject.boards.build }.to raise_error(Project::BoardLimitExceeded, 'Number of permitted boards exceeded')
148
149
150
        expect(subject.boards.size).to eq 1
      end
    end
gitlabhq's avatar
gitlabhq committed
151
152
  end

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

163
  describe 'validation' do
164
    let!(:project) { create(:project) }
165

166
167
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
168
    it { is_expected.to validate_length_of(:name).is_at_most(255) }
169
    it { is_expected.to validate_presence_of(:path) }
170
171
    it { is_expected.to validate_length_of(:path).is_at_most(255) }
    it { is_expected.to validate_length_of(:description).is_at_most(2000) }
172
173
174
    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) }
175
    it { is_expected.not_to allow_value('/test/foo').for(:ci_config_path) }
176
177
    it { is_expected.to validate_presence_of(:creator) }
    it { is_expected.to validate_presence_of(:namespace) }
178
    it { is_expected.to validate_presence_of(:repository_storage) }
179

180
181
182
183
184
185
186
187
    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

188
    it 'does not allow new projects beyond user limits' do
189
      project2 = build(:project)
190
191
      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
192
      expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/)
193
    end
194
195
196

    describe 'wiki path conflict' do
      context "when the new path has been used by the wiki of other Project" do
197
        it 'has an error on the name attribute' do
198
          new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
199
200
201
202
203
204
205

          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
206
        it 'has an error on the name attribute' do
207
208
          project_with_wiki_suffix = create(:project, path: 'foo.wiki')
          new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
209
210
211
212
213
214

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

216
    context 'repository storages inclusion' do
217
      let(:project2) { build(:project, repository_storage: 'missing') }
218
219

      before do
220
        storages = { 'custom' => { 'path' => 'tmp/tests/custom_repositories' } }
221
222
223
        allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
      end

224
      it "does not allow repository storages that don't match a label in the configuration" do
225
226
227
228
        expect(project2).not_to be_valid
        expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
      end
    end
229

230
    it 'does not allow an invalid URI as import_url' do
231
      project2 = build(:project, import_url: 'invalid://')
James Lopez's avatar
James Lopez committed
232
233
234
235

      expect(project2).not_to be_valid
    end

236
    it 'does allow a valid URI as import_url' do
237
      project2 = build(:project, import_url: 'ssh://test@gitlab.com/project.git')
James Lopez's avatar
James Lopez committed
238

239
240
      expect(project2).to be_valid
    end
241

242
    it 'allows an empty URI' do
243
      project2 = build(:project, import_url: '')
244

245
      expect(project2).to be_valid
246
247
248
    end

    it 'does not produce import data on an empty URI' do
249
      project2 = build(:project, import_url: '')
250
251
252
253
254

      expect(project2.import_data).to be_nil
    end

    it 'does not produce import data on an invalid URI' do
255
      project2 = build(:project, import_url: 'test://')
256
257
258

      expect(project2.import_data).to be_nil
    end
259

260
    it "does not allow import_url pointing to localhost" do
261
      project2 = build(:project, import_url: 'http://localhost:9000/t.git')
262
263

      expect(project2).to be_invalid
264
      expect(project2.errors[:import_url].first).to include('Requests to localhost are not allowed')
265
266
    end

267
    it "does not allow import_url with invalid ports" do
268
      project2 = build(:project, import_url: 'http://github.com:25/t.git')
269
270

      expect(project2).to be_invalid
271
      expect(project2.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
272
273
    end

274
275
276
277
278
279
280
    it "does not allow import_url with invalid user" do
      project2 = build(:project, import_url: 'http://$user:password@github.com/t.git')

      expect(project2).to be_invalid
      expect(project2.errors[:import_url].first).to include('Username needs to start with an alphanumeric character')
    end

281
282
    describe 'project pending deletion' do
      let!(:project_pending_deletion) do
283
        create(:project,
284
285
286
               pending_delete: true)
      end
      let(:new_project) do
287
        build(:project,
288
289
290
291
292
293
294
295
296
297
298
299
              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
300
301
302

    describe 'path validation' do
      it 'allows paths reserved on the root namespace' do
303
        project = build(:project, path: 'api')
304
305
306
307
308

        expect(project).to be_valid
      end

      it 'rejects paths reserved on another level' do
309
        project = build(:project, path: 'tree')
310
311
312

        expect(project).not_to be_valid
      end
313
314
315

      it 'rejects nested paths' do
        parent = create(:group, :nested, path: 'environments')
316
        project = build(:project, path: 'folders', namespace: parent)
317
318
319

        expect(project).not_to be_valid
      end
320
321
322

      it 'allows a reserved group name' do
        parent = create(:group)
323
        project = build(:project, path: 'avatar', namespace: parent)
324
325
326

        expect(project).to be_valid
      end
327
328
329
330
331
332

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

        expect(project).to be_valid
      end
333
    end
gitlabhq's avatar
gitlabhq committed
334
  end
335

336
  describe 'project token' do
337
    it 'sets an random token if none provided' do
338
      project = FactoryBot.create(:project, runners_token: '')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
339
      expect(project.runners_token).not_to eq('')
340
341
    end

ubudzisz's avatar
ubudzisz committed
342
    it 'does not set an random token if one provided' do
343
      project = FactoryBot.create(:project, runners_token: 'my-token')
Kamil Trzcinski's avatar
Kamil Trzcinski committed
344
      expect(project.runners_token).to eq('my-token')
345
346
    end
  end
gitlabhq's avatar
gitlabhq committed
347

348
  describe 'Respond to' do
349
350
351
352
353
    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) }
354
    it { is_expected.to respond_to(:full_path) }
gitlabhq's avatar
gitlabhq committed
355
356
  end

357
  describe 'delegation' do
358
    [:add_guest, :add_reporter, :add_developer, :add_maintainer, :add_user, :add_users].each do |method|
359
360
361
362
363
      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) }
364
365
  end

366
367
368
369
370
371
372
373
374
  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

375
  describe '#to_reference' do
376
    let(:owner)     { create(:user, name: 'Gitlab') }
377
    let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
378
    let(:project)   { create(:project, path: 'sample-project', namespace: namespace) }
379
    let(:group)     { create(:group, name: 'Group', path: 'sample-group') }
380

381
    context 'when nil argument' do
382
383
384
385
386
      it 'returns nil' do
        expect(project.to_reference).to be_nil
      end
    end

387
    context 'when full is true' do
388
      it 'returns complete path to the project' do
389
390
391
        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'
392
393
394
395
396
397
398
399
400
401
      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
402
      let(:another_namespace_project) { create(:project, name: 'another-project') }
403
404
405
406
407
408
409

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

412
      it 'returns path to the project' do
413
414
415
        expect(project.to_reference(another_project)).to eq 'sample-project'
      end
    end
416

417
418
    context 'when different namespace / cross-project argument' do
      let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
419
      let(:another_project)   { create(:project, path: 'another-project', namespace: another_namespace) }
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436

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

  describe '#to_human_reference' do
    let(:owner) { create(:user, name: 'Gitlab') }
    let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
444
    let(:project) { create(:project, name: 'Sample project', namespace: namespace) }
445
446
447
448
449
450
451
452
453
454
455
456
457
458

    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
459
      let(:another_namespace_project) { create(:project, name: 'another-project') }
460
461
462
463
464
465
466

      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
467
      let(:another_project) { create(:project, namespace: namespace) }
468
469
470
471

      it 'returns name of the project' do
        expect(project.to_human_reference(another_project)).to eq 'Sample project'
      end
472
473
474
    end
  end

475
  describe '#merge_method' do
476
477
478
479
480
481
482
    using RSpec::Parameterized::TableSyntax

    where(:ff, :rebase, :method) do
      true  | true  | :ff
      true  | false | :ff
      false | true  | :rebase_merge
      false | false | :merge
483
484
    end

485
486
487
488
489
490
    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) }
491
492
493
    end
  end

494
  it 'returns valid url to repo' do
495
    project = described_class.new(path: 'somewhere')
496
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
497
498
  end

Douwe Maan's avatar
Douwe Maan committed
499
  describe "#web_url" do
500
    let(:project) { create(:project, path: "somewhere") }
Douwe Maan's avatar
Douwe Maan committed
501
502

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

Imre Farkas's avatar
Imre Farkas committed
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
  describe "#readme_url" do
    let(:project) { create(:project, :repository, path: "somewhere") }

    context 'with a non-existing repository' do
      it 'returns nil' do
        allow(project.repository).to receive(:tree).with(:head).and_return(nil)

        expect(project.readme_url).to be_nil
      end
    end

    context 'with an existing repository' do
      context 'when no README exists' do
        it 'returns nil' do
          allow_any_instance_of(Tree).to receive(:readme).and_return(nil)

          expect(project.readme_url).to be_nil
        end
      end

      context 'when a README exists' do
        it 'returns the README' do
          expect(project.readme_url).to eql("#{Gitlab.config.gitlab.url}/#{project.namespace.full_path}/somewhere/blob/master/README.md")
        end
      end
    end
  end

535
  describe "#new_issuable_address" do
536
    let(:project) { create(:project, path: "somewhere") }
537
538
    let(:user) { create(:user) }

539
540
541
542
543
544
    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
545
        address = "p+#{project.full_path}+#{user.incoming_email_token}@gl.ab"
546

547
548
549
550
551
552
553
        expect(project.new_issuable_address(user, 'issue')).to eq(address)
      end

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

        expect(project.new_issuable_address(user, 'merge_request')).to eq(address)
554
555
556
557
558
559
560
      end
    end

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

562
      it 'returns nil' do
563
564
565
566
567
        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
568
      end
569
570
571
    end
  end

572
  describe 'last_activity methods' do
573
574
    let(:timestamp) { 2.hours.ago }
    # last_activity_at gets set to created_at upon creation
575
    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
576

577
    describe 'last_activity' do
578
      it 'alias last_activity to last_event' do
579
        last_event = create(:event, :closed, project: project)
580

581
        expect(project.last_activity).to eq(last_event)
582
      end
gitlabhq's avatar
gitlabhq committed
583
584
    end

585
586
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
587
        new_event = create(:event, :closed, project: project, created_at: Time.now)
588

589
        project.reload
590
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
591
      end
592

593
      it 'returns the project\'s last update date if it has no events' do
594
        expect(project.last_activity_date).to eq(project.updated_at)
595
      end
596
597

      it 'returns the most recent timestamp' do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
598
599
600
        project.update(updated_at: nil,
                       last_activity_at: timestamp,
                       last_repository_updated_at: timestamp - 1.hour)
601

602
        expect(project.last_activity_date).to be_like_time(timestamp)
603

Lin Jen-Shin's avatar
Lin Jen-Shin committed
604
605
606
        project.update(updated_at: timestamp,
                       last_activity_at: timestamp - 1.hour,
                       last_repository_updated_at: nil)
607

608
        expect(project.last_activity_date).to be_like_time(timestamp)
609
      end
610
611
    end
  end
612

613
  describe '#get_issue' do
614
    let(:project) { create(:project) }
Stan Hu's avatar
Stan Hu committed
615
    let!(:issue)  { create(:issue, project: project) }
616
617
618
    let(:user)    { create(:user) }

    before do
619
      project.add_developer(user)
620
    end
621
622
623

    context 'with default issues tracker' do
      it 'returns an issue' do
624
        expect(project.get_issue(issue.iid, user)).to eq issue
625
626
      end

Stan Hu's avatar
Stan Hu committed
627
628
629
630
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      end

631
      it 'returns nil when no issue found' do
632
633
634
635
636
637
        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
638
639
640
641
      end
    end

    context 'with external issues tracker' do
642
      let!(:internal_issue) { create(:issue, project: project) }
643
      before do
644
        allow(project).to receive(:external_issue_tracker).and_return(true)
645
646
      end

647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
      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
684
685
686
687
688
      end
    end
  end

  describe '#issue_exists?' do
689
    let(:project) { create(:project) }
690
691
692
693
694
695
696
697
698
699
700
701

    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

702
  describe '#to_param' do
703
704
    context 'with namespace' do
      before do
705
        @group = create(:group, name: 'gitlab')
706
        @project = create(:project, name: 'gitlabhq', namespace: @group)
707
708
      end

Vinnie Okada's avatar
Vinnie Okada committed
709
      it { expect(@project.to_param).to eq('gitlabhq') }
710
    end
711
712
713

    context 'with invalid path' do
      it 'returns previous path to keep project suitable for use in URLs when persisted' do
714
        project = create(:project, path: 'gitlab')
715
716
717
718
719
720
721
        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
722
        project = build(:project, path: 'gitlab')
723
724
725
726
727
728
        project.path = 'foo&bar'

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

731
  describe '#repository' do
732
    let(:project) { create(:project, :repository) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
733

734
    it 'returns valid repo' do
735
      expect(project.repository).to be_kind_of(Repository)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
736
737
    end
  end
738

739
  describe '#default_issues_tracker?' do
740
    it "is true if used internal tracker" do
741
      project = build(:project)
742

743
      expect(project.default_issues_tracker?).to be_truthy
744
745
    end

746
    it "is false if used other tracker" do
747
748
749
750
      # NOTE: The current nature of this factory requires persistence
      project = create(:redmine_project)

      expect(project.default_issues_tracker?).to be_falsey
751
752
753
    end
  end

754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
  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

772
  describe '#external_issue_tracker' do
773
    let(:project) { create(:project) }
774
775
776
    let(:ext_project) { create(:redmine_project) }

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

807
  describe '#cache_has_external_issue_tracker' do
808
    let(:project) { create(:project, has_external_issue_tracker: nil) }
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826

    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
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864

    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
865
866
  end

867
  describe '#has_wiki?' do
868
869
870
    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) }
871
872
873
874
875
876
877
878

    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

879
  describe '#external_wiki' do
880
    let(:project) { create(:project) }
881

882
883
884
885
886
    context 'with an active external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: true)
        project.external_wiki
      end
887

888
889
890
      it 'sets :has_external_wiki as true' do
        expect(project.has_external_wiki).to be(true)
      end
891

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

895
896
897
898
        project.services.external_wikis.first.destroy

        expect(project.has_external_wiki).to be(false)
      end
899
900
    end

901
902
903
904
    context 'with an inactive external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: false)
      end
905

906
907
908
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end
909
910
    end

911
912
913
914
    context 'with no external wiki' do
      before do
        project.external_wiki
      end
915

916
917
918
919
920
921
922
923
924
925
926
      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
927
928
929
    end
  end

930
931
  describe '#star_count' do
    it 'counts stars from multiple users' do
932
933
      user1 = create(:user)
      user2 = create(:user)
934
      project = create(:project, :public)
Ciro Santilli's avatar
Ciro Santilli committed
935
936

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

Ciro Santilli's avatar
Ciro Santilli committed
938
      user1.toggle_star(project)
939
940
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
941
      user2.toggle_star(project)
942
943
944
      project.reload
      expect(project.reload.star_count).to eq(2)

Ciro Santilli's avatar
Ciro Santilli committed
945
      user1.toggle_star(project)
946
947
948
      project.reload
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
949
      user2.toggle_star(project)
950
951
952
953
      project.reload
      expect(project.reload.star_count).to eq(0)
    end

954
    it 'counts stars on the right project' do
955
      user = create(:user)
956
957
      project1 = create(:project, :public)
      project2 = create(:project, :public)
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

      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
985
986
    end
  end
987

988
  describe '#avatar_type' do
989
    let(:project) { create(:project) }
990

991
    it 'is true if avatar is image' do
992
      project.update_attribute(:avatar, 'uploads/avatar.png')
993
      expect(project.avatar_type).to be_truthy
994
995
    end

996
    it 'is false if avatar is html page' do
997
      project.update_attribute(:avatar, 'uploads/avatar.html')
998
      expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico'])
999
1000
    end
  end
sue445's avatar
sue445 committed
1001

1002
  describe '#avatar_url' do
sue445's avatar
sue445 committed
1003
1004
    subject { project.avatar_url }

1005
    let(:project) { create(:project) }
sue445's avatar
sue445 committed
1006

1007
    context 'when avatar file is uploaded' do
1008
      let(:project) { create(:project, :public, :with_avatar) }
sue445's avatar
sue445 committed
1009

1010
      it 'shows correct url' do
1011
1012
        expect(project.avatar_url).to eq(project.avatar.url)
        expect(project.avatar_url(only_path: false)).to eq([Gitlab.config.gitlab.url, project.avatar.url].join)
1013
      end
sue445's avatar
sue445 committed
1014
1015
    end

1016
    context 'when avatar file in git' do
sue445's avatar
sue445 committed
1017
1018
1019
1020
      before do
        allow(project).to receive(:avatar_in_git) { true }
      end

1021
      let(:avatar_path) { "/#{project.full_path}/avatar" }
sue445's avatar
sue445 committed
1022

1023
      it { is_expected.to eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
sue445's avatar
sue445 committed
1024
    end
1025
1026

    context 'when git repo is empty' do
1027
      let(:project) { create(:project) }
1028

1029
      it { is_expected.to eq nil }
1030
    end
sue445's avatar
sue445 committed
1031
  end
1032

1033
  describe '#pipeline_for' do
1034
    let(:project) { create(:project, :repository) }
1035
    let!(:pipeline) { create_pipeline }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1036

1037
1038
    shared_examples 'giving the correct pipeline' do
      it { is_expected.to eq(pipeline) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1039

1040
1041
      context 'return latest' do
        let!(:pipeline2) { create_pipeline }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1042

1043
        it { is_expected.to eq(pipeline2) }