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

Douwe Maan's avatar
Douwe Maan committed
3
describe Project, models: true do
4
  describe 'associations' do
5
6
7
8
    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
9
    it { is_expected.to have_many(:services) }
10
11
12
13
14
    it { is_expected.to have_many(:events).dependent(:destroy) }
    it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
    it { is_expected.to have_many(:issues).dependent(:destroy) }
    it { is_expected.to have_many(:milestones).dependent(:destroy) }
    it { is_expected.to have_many(:project_members).dependent(:destroy) }
15
16
    it { is_expected.to have_many(:users).through(:project_members) }
    it { is_expected.to have_many(:requesters).dependent(:destroy) }
17
18
19
20
21
22
23
    it { is_expected.to have_many(:notes).dependent(:destroy) }
    it { is_expected.to have_many(:snippets).class_name('ProjectSnippet').dependent(:destroy) }
    it { is_expected.to have_many(:deploy_keys_projects).dependent(:destroy) }
    it { is_expected.to have_many(:deploy_keys) }
    it { is_expected.to have_many(:hooks).dependent(:destroy) }
    it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
    it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
24
    it { is_expected.to have_one(:slack_service).dependent(:destroy) }
25
    it { is_expected.to have_one(:mattermost_service).dependent(:destroy) }
26
27
    it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
    it { is_expected.to have_one(:asana_service).dependent(:destroy) }
28
    it { is_expected.to have_many(:boards).dependent(:destroy) }
ubudzisz's avatar
ubudzisz committed
29
30
31
32
33
34
35
36
37
38
    it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
    it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
    it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
    it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
    it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
    it { is_expected.to have_one(:irker_service).dependent(:destroy) }
    it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
    it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
    it { is_expected.to have_one(:flowdock_service).dependent(:destroy) }
    it { is_expected.to have_one(:assembla_service).dependent(:destroy) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
39
    it { is_expected.to have_one(:slack_slash_commands_service).dependent(:destroy) }
40
    it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) }
ubudzisz's avatar
ubudzisz committed
41
42
43
44
45
46
47
48
49
50
51
    it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) }
    it { is_expected.to have_one(:buildkite_service).dependent(:destroy) }
    it { is_expected.to have_one(:bamboo_service).dependent(:destroy) }
    it { is_expected.to have_one(:teamcity_service).dependent(:destroy) }
    it { is_expected.to have_one(:jira_service).dependent(:destroy) }
    it { is_expected.to have_one(:redmine_service).dependent(:destroy) }
    it { is_expected.to have_one(:custom_issue_tracker_service).dependent(:destroy) }
    it { is_expected.to have_one(:bugzilla_service).dependent(:destroy) }
    it { is_expected.to have_one(:gitlab_issue_tracker_service).dependent(:destroy) }
    it { is_expected.to have_one(:external_wiki_service).dependent(:destroy) }
    it { is_expected.to have_one(:project_feature).dependent(:destroy) }
Markus Koller's avatar
Markus Koller committed
52
    it { is_expected.to have_one(:statistics).class_name('ProjectStatistics').dependent(:delete) }
ubudzisz's avatar
ubudzisz committed
53
54
55
    it { is_expected.to have_one(:import_data).class_name('ProjectImportData').dependent(:destroy) }
    it { is_expected.to have_one(:last_event).class_name('Event') }
    it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
56
    it { is_expected.to have_many(:commit_statuses) }
57
    it { is_expected.to have_many(:pipelines) }
58
59
60
61
62
    it { is_expected.to have_many(:builds) }
    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
63
    it { is_expected.to have_many(:pages_domains) }
64
    it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) }
ubudzisz's avatar
ubudzisz committed
65
    it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
66
67
    it { is_expected.to have_many(:environments).dependent(:destroy) }
    it { is_expected.to have_many(:deployments).dependent(:destroy) }
68
    it { is_expected.to have_many(:todos).dependent(:destroy) }
ubudzisz's avatar
ubudzisz committed
69
70
71
72
73
    it { is_expected.to have_many(:releases).dependent(:destroy) }
    it { is_expected.to have_many(:lfs_objects_projects).dependent(:destroy) }
    it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
    it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
    it { is_expected.to have_many(:forks).through(:forked_project_links) }
74

75
76
    context 'after initialized' do
      it "has a project_feature" do
77
        expect(Project.new.project_feature).to be_present
78
79
80
      end
    end

81
    describe '#members & #requesters' do
82
      let(:project) { create(:empty_project, :public, :access_requestable) }
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
      let(:requester) { create(:user) }
      let(:developer) { create(:user) }
      before do
        project.request_access(requester)
        project.team << [developer, :developer]
      end

      describe '#members' do
        it 'includes members and exclude requesters' do
          member_user_ids = project.members.pluck(:user_id)

          expect(member_user_ids).to include(developer.id)
          expect(member_user_ids).not_to include(requester.id)
        end
      end

      describe '#requesters' do
        it 'does not include requesters' do
          requester_user_ids = project.requesters.pluck(:user_id)

          expect(requester_user_ids).to include(requester.id)
          expect(requester_user_ids).not_to include(developer.id)
        end
      end
    end
108
109
110
111
112

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

113
        expect { subject.boards.build }.to raise_error(Project::BoardLimitExceeded, 'Number of permitted boards exceeded')
114
115
116
        expect(subject.boards.size).to eq 1
      end
    end
gitlabhq's avatar
gitlabhq committed
117
118
  end

119
120
121
122
123
124
  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) }
125
    it { is_expected.to include_module(Gitlab::CurrentSettings) }
126
127
    it { is_expected.to include_module(Referable) }
    it { is_expected.to include_module(Sortable) }
128
129
  end

130
  describe 'validation' do
131
    let!(:project) { create(:empty_project) }
132

133
134
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
135
    it { is_expected.to validate_length_of(:name).is_at_most(255) }
136

137
138
    it { is_expected.to validate_presence_of(:path) }
    it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) }
139
140
141
142
    it { is_expected.to validate_length_of(:path).is_at_most(255) }

    it { is_expected.to validate_length_of(:description).is_at_most(2000) }

143
    it { is_expected.to validate_presence_of(:creator) }
144

145
    it { is_expected.to validate_presence_of(:namespace) }
146

147
    it { is_expected.to validate_presence_of(:repository_storage) }
148

149
    it 'does not allow new projects beyond user limits' do
150
      project2 = build(:empty_project)
151
152
      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
153
      expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/)
154
    end
155
156
157

    describe 'wiki path conflict' do
      context "when the new path has been used by the wiki of other Project" do
158
        it 'has an error on the name attribute' do
159
          new_project = build_stubbed(:empty_project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")
160
161
162
163
164
165
166

          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
167
        it 'has an error on the name attribute' do
168
169
          project_with_wiki_suffix = create(:empty_project, path: 'foo.wiki')
          new_project = build_stubbed(:empty_project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')
170
171
172
173
174
175

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

    context 'repository storages inclussion' do
178
      let(:project2) { build(:empty_project, repository_storage: 'missing') }
179
180
181
182
183
184

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

185
      it "does not allow repository storages that don't match a label in the configuration" do
186
187
188
189
        expect(project2).not_to be_valid
        expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
      end
    end
190

191
    it 'does not allow an invalid URI as import_url' do
192
      project2 = build(:empty_project, import_url: 'invalid://')
James Lopez's avatar
James Lopez committed
193
194
195
196

      expect(project2).not_to be_valid
    end

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

200
201
      expect(project2).to be_valid
    end
202

203
    it 'allows an empty URI' do
204
      project2 = build(:empty_project, import_url: '')
205

206
      expect(project2).to be_valid
207
208
209
    end

    it 'does not produce import data on an empty URI' do
210
      project2 = build(:empty_project, import_url: '')
211
212
213
214
215

      expect(project2.import_data).to be_nil
    end

    it 'does not produce import data on an invalid URI' do
216
      project2 = build(:empty_project, import_url: 'test://')
217
218
219

      expect(project2.import_data).to be_nil
    end
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239

    describe 'project pending deletion' do
      let!(:project_pending_deletion) do
        create(:empty_project,
               pending_delete: true)
      end
      let(:new_project) do
        build(:empty_project,
              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
gitlabhq's avatar
gitlabhq committed
240
  end
241

242
243
244
245
246
247
248
249
250
  describe 'default_scope' do
    it 'excludes projects pending deletion from the results' do
      project = create(:empty_project)
      create(:empty_project, pending_delete: true)

      expect(Project.all).to eq [project]
    end
  end

251
  describe 'project token' do
252
    it 'sets an random token if none provided' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
253
254
      project = FactoryGirl.create :empty_project, runners_token: ''
      expect(project.runners_token).not_to eq('')
255
256
    end

ubudzisz's avatar
ubudzisz committed
257
    it 'does not set an random token if one provided' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
258
259
      project = FactoryGirl.create :empty_project, runners_token: 'my-token'
      expect(project.runners_token).to eq('my-token')
260
261
    end
  end
gitlabhq's avatar
gitlabhq committed
262

263
  describe 'Respond to' do
264
265
266
267
268
    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) }
gitlabhq's avatar
gitlabhq committed
269
270
  end

271
272
273
274
275
276
277
  describe 'delegation' do
    it { is_expected.to delegate_method(:add_guest).to(:team) }
    it { is_expected.to delegate_method(:add_reporter).to(:team) }
    it { is_expected.to delegate_method(:add_developer).to(:team) }
    it { is_expected.to delegate_method(:add_master).to(:team) }
  end

278
279
280
281
282
283
284
  describe '#name_with_namespace' do
    let(:project) { build_stubbed(:empty_project) }

    it { expect(project.name_with_namespace).to eq "#{project.namespace.human_name} / #{project.name}" }
    it { expect(project.human_name).to eq project.name_with_namespace }
  end

285
  describe '#to_reference' do
286
    let(:owner)     { create(:user, name: 'Gitlab') }
287
    let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
288
289
    let(:project)   { create(:empty_project, path: 'sample-project', namespace: namespace) }
    let(:group)     { create(:group, name: 'Group', path: 'sample-group', owner: owner) }
290

291
    context 'when nil argument' do
292
293
294
295
296
      it 'returns nil' do
        expect(project.to_reference).to be_nil
      end
    end

297
    context 'when full is true' do
298
      it 'returns complete path to the project' do
299
300
301
        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'
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
      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
      let(:another_namespace_project) { create(:empty_project, name: 'another-project') }

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

322
      it 'returns path to the project' do
323
324
325
        expect(project.to_reference(another_project)).to eq 'sample-project'
      end
    end
326

327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
    context 'when different namespace / cross-project argument' do
      let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) }
      let(:another_project)   { create(:empty_project, path: 'another-project', namespace: another_namespace) }

      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
347
348
      end
    end
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
  end

  describe '#to_human_reference' do
    let(:owner) { create(:user, name: 'Gitlab') }
    let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
    let(:project) { create(:empty_project, name: 'Sample project', namespace: namespace) }

    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
      let(:another_namespace_project) { create(:empty_project, name: 'another-project') }

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

      it 'returns name of the project' do
        expect(project.to_human_reference(another_project)).to eq 'Sample project'
      end
382
383
384
    end
  end

385
  describe '#repository_storage_path' do
386
    let(:project) { create(:empty_project, repository_storage: 'custom') }
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402

    before do
      FileUtils.mkdir('tmp/tests/custom_repositories')
      storages = { 'custom' => 'tmp/tests/custom_repositories' }
      allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
    end

    after do
      FileUtils.rm_rf('tmp/tests/custom_repositories')
    end

    it 'returns the repository storage path' do
      expect(project.repository_storage_path).to eq('tmp/tests/custom_repositories')
    end
  end

403
  it 'returns valid url to repo' do
404
    project = Project.new(path: 'somewhere')
405
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
406
407
  end

Douwe Maan's avatar
Douwe Maan committed
408
409
410
411
412
413
  describe "#web_url" do
    let(:project) { create(:empty_project, path: "somewhere") }

    it 'returns the full web URL for this repo' do
      expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere")
    end
414
415
  end

416
  describe "#new_issue_address" do
417
418
419
    let(:project) { create(:empty_project, path: "somewhere") }
    let(:user) { create(:user) }

420
421
422
423
424
425
    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
426
        address = "p+#{project.path_with_namespace}+#{user.incoming_email_token}@gl.ab"
427
428
429
430
431
432
433
434
435

        expect(project.new_issue_address(user)).to eq(address)
      end
    end

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

437
438
439
      it 'returns nil' do
        expect(project.new_issue_address(user)).to be_nil
      end
440
441
442
    end
  end

443
  describe 'last_activity methods' do
444
445
    let(:timestamp) { 2.hours.ago }
    # last_activity_at gets set to created_at upon creation
446
    let(:project) { create(:empty_project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
447

448
    describe 'last_activity' do
449
      it 'alias last_activity to last_event' do
450
451
        last_event = create(:event, project: project)

452
        expect(project.last_activity).to eq(last_event)
453
      end
gitlabhq's avatar
gitlabhq committed
454
455
    end

456
457
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
458
459
        new_event = create(:event, project: project, created_at: Time.now)

460
        project.reload
461
        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
462
      end
463

464
      it 'returns the project\'s last update date if it has no events' do
465
        expect(project.last_activity_date).to eq(project.updated_at)
466
      end
467
468
    end
  end
469

470
471
  describe '#get_issue' do
    let(:project) { create(:empty_project) }
Stan Hu's avatar
Stan Hu committed
472
    let!(:issue)  { create(:issue, project: project) }
473
474
475
476
477
    let(:user)    { create(:user) }

    before do
      project.team << [user, :developer]
    end
478
479
480

    context 'with default issues tracker' do
      it 'returns an issue' do
481
        expect(project.get_issue(issue.iid, user)).to eq issue
482
483
      end

Stan Hu's avatar
Stan Hu committed
484
485
486
487
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      end

488
      it 'returns nil when no issue found' do
489
490
491
492
493
494
        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
495
496
497
498
499
500
501
502
503
      end
    end

    context 'with external issues tracker' do
      before do
        allow(project).to receive(:default_issues_tracker?).and_return(false)
      end

      it 'returns an ExternalIssue' do
504
        issue = project.get_issue('FOO-1234', user)
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
        expect(issue).to be_kind_of(ExternalIssue)
        expect(issue.iid).to eq 'FOO-1234'
        expect(issue.project).to eq project
      end
    end
  end

  describe '#issue_exists?' do
    let(:project) { create(:empty_project) }

    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

526
  describe '#to_param' do
527
528
529
    context 'with namespace' do
      before do
        @group = create :group, name: 'gitlab'
530
        @project = create(:empty_project, name: 'gitlabhq', namespace: @group)
531
532
      end

Vinnie Okada's avatar
Vinnie Okada committed
533
      it { expect(@project.to_param).to eq('gitlabhq') }
534
    end
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552

    context 'with invalid path' do
      it 'returns previous path to keep project suitable for use in URLs when persisted' do
        project = create(:empty_project, path: 'gitlab')
        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
        project = build(:empty_project, path: 'gitlab')
        project.path = 'foo&bar'

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

555
  describe '#repository' do
556
    let(:project) { create(:project, :repository) }
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
557

558
    it 'returns valid repo' do
559
      expect(project.repository).to be_kind_of(Repository)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
560
561
    end
  end
562

563
  describe '#default_issues_tracker?' do
564
    it "is true if used internal tracker" do
565
566
      project = build(:empty_project)

567
      expect(project.default_issues_tracker?).to be_truthy
568
569
    end

570
    it "is false if used other tracker" do
571
572
573
574
      # NOTE: The current nature of this factory requires persistence
      project = create(:redmine_project)

      expect(project.default_issues_tracker?).to be_falsey
575
576
577
    end
  end

578
  describe '#external_issue_tracker' do
579
    let(:project) { create(:empty_project) }
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
    let(:ext_project) { create(:redmine_project) }

    context 'on existing projects with no value for has_external_issue_tracker' do
      before(:each) do
        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

613
  describe '#cache_has_external_issue_tracker' do
614
    let(:project) { create(:empty_project, has_external_issue_tracker: nil) }
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634

    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
  end

635
  describe '#has_wiki?' do
636
637
638
    let(:no_wiki_project)       { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) }
    let(:wiki_enabled_project)  { create(:empty_project) }
    let(:external_wiki_project) { create(:empty_project, has_external_wiki: true) }
639
640
641
642
643
644
645
646

    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

647
  describe '#external_wiki' do
648
    let(:project) { create(:empty_project) }
649

650
651
652
653
654
    context 'with an active external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: true)
        project.external_wiki
      end
655

656
657
658
      it 'sets :has_external_wiki as true' do
        expect(project.has_external_wiki).to be(true)
      end
659

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

663
664
665
666
        project.services.external_wikis.first.destroy

        expect(project.has_external_wiki).to be(false)
      end
667
668
    end

669
670
671
672
    context 'with an inactive external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: false)
      end
673

674
675
676
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end
677
678
    end

679
680
681
682
    context 'with no external wiki' do
      before do
        project.external_wiki
      end
683

684
685
686
687
688
689
690
691
692
693
694
      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
695
696
697
    end
  end

698
  describe '#open_branches' do
699
    let(:project) { create(:project, :repository) }
700
701
702
703
704

    before do
      project.protected_branches.create(name: 'master')
    end

705
706
    it { expect(project.open_branches.map(&:name)).to include('feature') }
    it { expect(project.open_branches.map(&:name)).not_to include('master') }
707

708
    it "includes branches matching a protected branch wildcard" do
709
710
711
712
      expect(project.open_branches.map(&:name)).to include('feature')

      create(:protected_branch, name: 'feat*', project: project)

713
      expect(Project.find(project.id).open_branches.map(&:name)).to include('feature')
714
    end
715
  end
Ciro Santilli's avatar
Ciro Santilli committed
716

717
718
  describe '#star_count' do
    it 'counts stars from multiple users' do
Ciro Santilli's avatar
Ciro Santilli committed
719
720
      user1 = create :user
      user2 = create :user
721
      project = create(:empty_project, :public)
Ciro Santilli's avatar
Ciro Santilli committed
722
723

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

Ciro Santilli's avatar
Ciro Santilli committed
725
      user1.toggle_star(project)
726
727
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
728
      user2.toggle_star(project)
729
730
731
      project.reload
      expect(project.reload.star_count).to eq(2)

Ciro Santilli's avatar
Ciro Santilli committed
732
      user1.toggle_star(project)
733
734
735
      project.reload
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
736
      user2.toggle_star(project)
737
738
739
740
      project.reload
      expect(project.reload.star_count).to eq(0)
    end

741
    it 'counts stars on the right project' do
742
      user = create :user
743
744
      project1 = create(:empty_project, :public)
      project2 = create(:empty_project, :public)
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771

      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
772
773
    end
  end
774

775
  describe '#avatar_type' do
776
    let(:project) { create(:empty_project) }
777

778
    it 'is true if avatar is image' do
779
      project.update_attribute(:avatar, 'uploads/avatar.png')
780
      expect(project.avatar_type).to be_truthy
781
782
    end

783
    it 'is false if avatar is html page' do
784
      project.update_attribute(:avatar, 'uploads/avatar.html')
785
      expect(project.avatar_type).to eq(['only images allowed'])
786
787
    end
  end
sue445's avatar
sue445 committed
788

789
  describe '#avatar_url' do
sue445's avatar
sue445 committed
790
791
    subject { project.avatar_url }

792
    let(:project) { create(:empty_project) }
sue445's avatar
sue445 committed
793
794
795
796
797
798
799
800
801
802
803

    context 'When avatar file is uploaded' do
      before do
        project.update_columns(avatar: 'uploads/avatar.png')
        allow(project.avatar).to receive(:present?) { true }
      end

      let(:avatar_path) do
        "/uploads/project/avatar/#{project.id}/uploads/avatar.png"
      end

804
      it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
sue445's avatar
sue445 committed
805
806
807
808
809
810
811
812
813
814
815
    end

    context 'When avatar file in git' do
      before do
        allow(project).to receive(:avatar_in_git) { true }
      end

      let(:avatar_path) do
        "/#{project.namespace.name}/#{project.path}/avatar"
      end

816
      it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
sue445's avatar
sue445 committed
817
    end
818
819
820
821
822
823

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

      it { should eq nil }
    end
sue445's avatar
sue445 committed
824
  end
825

826
  describe '#pipeline_for' do
827
    let(:project) { create(:project, :repository) }
828
    let!(:pipeline) { create_pipeline }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
829

830
831
    shared_examples 'giving the correct pipeline' do
      it { is_expected.to eq(pipeline) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
832

833
834
      context 'return latest' do
        let!(:pipeline2) { create_pipeline }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
835

836
        it { is_expected.to eq(pipeline2) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
837
      end
838
839
840
841
842
843
844
845
846
847
848
849
850
    end

    context 'with explicit sha' do
      subject { project.pipeline_for('master', pipeline.sha) }

      it_behaves_like 'giving the correct pipeline'
    end

    context 'with implicit sha' do
      subject { project.pipeline_for('master') }

      it_behaves_like 'giving the correct pipeline'
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
851

852
853
854
855
856
    def create_pipeline
      create(:ci_pipeline,
             project: project,
             ref: 'master',
             sha: project.commit('master').sha)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
857
    end
858
  end
859

860
  describe '#builds_enabled' do
861
    let(:project) { create(:empty_project) }
862

863
864
865
    subject { project.builds_enabled }

    it { expect(project.builds_enabled?).to be_truthy }
866
  end
867

868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
  describe '.with_shared_runners' do
    subject { Project.with_shared_runners }

    context 'when shared runners are enabled for project' do
      let!(:project) { create(:empty_project, shared_runners_enabled: true) }

      it "returns a project" do
        is_expected.to eq([project])
      end
    end

    context 'when shared runners are disabled for project' do
      let!(:project) { create(:empty_project, shared_runners_enabled: false) }

      it "returns an empty array" do
        is_expected.to be_empty
      end
    end
  end

888
889
890
891
892
893
894
895
896
897
898
899
900
901
  describe '.cached_count', caching: true do
    let(:group)     { create(:group, :public) }
    let!(:project1) { create(:empty_project, :public, group: group) }
    let!(:project2) { create(:empty_project, :public, group: group) }

    it 'returns total project count' do
      expect(Project).to receive(:count).once.and_call_original

      3.times do
        expect(Project.cached_count).to eq(2)
      end
    end
  end

902
  describe '.trending' do
Felipe Artur's avatar
Felipe Artur committed
903
    let(:group)    { create(:group, :public) }
904
905
906
907
908
909
910
911
912
913
    let(:project1) { create(:empty_project, :public, group: group) }
    let(:project2) { create(:empty_project, :public, group: group) }

    before do
      2.times do
        create(:note_on_commit, project: project1)
      end

      create(:note_on_commit, project: project2)

914
      TrendingProject.refresh!
915
916
    end

917
    subject { described_class.trending.to_a }
918

919
920
    it 'sorts projects by the amount of notes in descending order' do
      expect(subject).to eq([project1, project2])
921
    end
922
923
924
925
926
927
928
929

    it 'does not take system notes into account' do
      10.times do
        create(:note_on_commit, project: project2, system: true)
      end

      expect(described_class.trending.to_a).to eq([project1, project2])
    end
930
  end
Yorick Peterse's avatar
Yorick Peterse committed
931
932

  describe '.visible_to_user' do
933
    let!(:project) { create(:empty_project, :private) }
Yorick Peterse's avatar
Yorick Peterse committed
934
935
936
937
938
939
    let!(:user)    { create(:user) }

    subject { described_class.visible_to_user(user) }

    describe 'when a user has access to a project' do
      before do
940
        project.add_user(user, Gitlab::Access::MASTER)
Yorick Peterse's avatar
Yorick Peterse committed
941
942
943
944
945
946
947
948
949
      end

      it { is_expected.to eq([project]) }
    end

    describe 'when a user does not have access to any projects' do
      it { is_expected.to eq([]) }
    end
  end
950

951
952
953
954
  context 'repository storage by default' do
    let(:project) { create(:empty_project) }

    before do
955
956
957
958
      storages = {
        'default' => 'tmp/tests/repositories',
        'picked'  => 'tmp/tests/repositories',
      }
959
960
961
      allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
    end

962
963
964
965
966
    it 'picks storage from ApplicationSetting' do
      expect_any_instance_of(ApplicationSetting).to receive(:pick_repository_storage).and_return('picked')

      expect(project.repository_storage).to eq('picked')
    end
967
968
  end

969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
  context 'shared runners by default' do
    let(:project) { create(:empty_project) }

    subject { project.shared_runners_enabled }

    context 'are enabled' do
      before { stub_application_setting(shared_runners_enabled: true) }

      it { is_expected.to be_truthy }
    end

    context 'are disabled' do
      before { stub_application_setting(shared_runners_enabled: false) }

      it { is_expected.to be_falsey }
    end
  end

987
  describe '#any_runners' do
988
    let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
989
990
    let(:specific_runner) { create(:ci_runner) }
    let(:shared_runner) { create(:ci_runner, :shared) }
991
992
993

    context 'for shared runners disabled' do
      let(:shared_runners_enabled) { false }
994

995
      it 'has no runners available' do
996
997
        expect(project.any_runners?).to be_falsey
      end
998

999
      it 'has a specific runner' do
1000
        project.runners << specific_runner
1001
1002
        expect(project.any_runners?).to be_truthy
      end
1003

1004
      it 'has a shared runner, but they are prohibited to use' do
1005
1006
1007
        shared_runner
        expect(project.any_runners?).to be_falsey
      end
1008

1009
      it 'checks the presence of specific runner' do
1010
        project.runners << specific_runner
1011
1012
1013
        expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
      end
    end
1014

1015
1016
    context 'for shared runners enabled' do
      let(:shared_runners_enabled) { true }
1017

1018
      it 'has a shared runner' do
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
        shared_runner
        expect(project.any_runners?).to be_truthy
      end

      it 'checks the presence of shared runner' do
        shared_runner
        expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
      end
    end
  end
1029

1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
  describe '#shared_runners' do
    let!(:runner) { create(:ci_runner, :shared) }

    subject { project.shared_runners }

    context 'when shared runners are enabled for project' do
      let!(:project) { create(:empty_project, shared_runners_enabled: true) }

      it "returns a list of shared runners" do
        is_expected.to eq([runner])
      end
    end

    context 'when shared runners are disabled for project' do
      let!(:project) { create(:empty_project, shared_runners_enabled: false) }

      it "returns a empty list" do
        is_expected.to be_empty
      end
    end
  end

1052
  describe '#visibility_level_allowed?' do
1053
    let(:project) { create(:empty_project, :internal) }
1054
1055
1056
1057
1058
1059
1060
1061

    context 'when checking on non-forked project' do
      it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
      it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
      it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_truthy }
    end

    context 'when checking on forked project' do
1062
1063
      let(:project)        { create(:empty_project, :internal) }
      let(:forked_project) { create(:empty_project, forked_from_project: project) }
1064
1065
1066
1067
1068

      it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
      it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
      it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
    end
1069
  end
1070

1071
  describe '.search' do
1072
    let(:project) { create(:empty_project, description: 'kitten mittens') }
1073

1074
1075
1076
    it 'returns projects with a matching name' do
      expect(described_class.search(project.name)).to eq([project])
    end
1077

1078
1079
1080
    it 'returns projects with a partially matching name' do
      expect(described_class.search(project.name[0..2])).to eq([project])
    end
1081

1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
    it 'returns projects with a matching name regardless of the casing' do
      expect(described_class.search(project.name.upcase)).to eq([project])
    end

    it 'returns projects with a matching description' do
      expect(described_class.search(project.description)).to eq([project])
    end

    it 'returns projects with a partially matching description' do
      expect(described_class.search('kitten')).to eq([project])
    end

    it 'returns projects with a matching description regardless of the casing' do
      expect(described_class.search('KITTEN')).to eq([project])
    end

    it 'returns projects with a matching path' do
      expect(described_class.search(project.path)).to eq([project])
    end

    it 'returns projects with a partially matching path' do
      expect(described_class.search(project.path[0..2])).to eq([project])
    end

    it 'returns projects with a matching path regardless of the casing' do
      expect(described_class.search(project.path.upcase)).to eq([project])
    end

    it 'returns projects with a matching namespace name' do
      expect(described_class.search(project.namespace.name)).to eq([project])
    end

    it 'returns projects with a partially matching namespace name' do
      expect(described_class.search(project.namespace.name[0..2])).to eq([project])
    end

    it 'returns projects with a matching namespace name regardless of the casing' do
      expect(described_class.search(project.namespace.name.upcase)).to eq([project])
    end
1121
1122
1123
1124
1125

    it 'returns projects when eager loading namespaces' do
      relation = described_class.all.includes(:namespace)

      expect(relation.search(project.namespace.name)).to eq([project])
1126
    end
1127
  end
1128
1129

  describe '#rename_repo' do
1130
    let(:project) { create(:project, :repository) }
1131
1132
1133
1134
1135
1136
1137
1138
    let(:gitlab_shell) { Gitlab::Shell.new }

    before do
      # Project#gitlab_shell returns a new instance of Gitlab::Shell on every
      # call. This makes testing a bit easier.
      allow(project).to receive(:gitlab_shell).and_return(gitlab_shell)

      allow(project).to receive(:previous_changes).and_return('path' => ['foo'])
Kamil Trzcinski's avatar
Kamil Trzcinski committed