project_spec.rb 55.3 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
24
25
26
    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) }
    it { is_expected.to have_one(:slack_service).dependent(:destroy) }
    it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
    it { is_expected.to have_one(:asana_service).dependent(:destroy) }
Douglas Barbosa Alexandre's avatar
Douglas Barbosa Alexandre committed
27
    it { is_expected.to have_one(:board).dependent(:destroy) }
ubudzisz's avatar
ubudzisz committed
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
    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) }
    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) }
    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
52
    it { is_expected.to have_many(:commit_statuses) }
53
    it { is_expected.to have_many(:pipelines) }
54
55
56
57
58
    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) }
ubudzisz's avatar
ubudzisz committed
59
60
    it { is_expected.to have_many(:labels).dependent(:destroy) }
    it { is_expected.to have_many(:users_star_projects).dependent(:destroy) }
61
62
    it { is_expected.to have_many(:environments).dependent(:destroy) }
    it { is_expected.to have_many(:deployments).dependent(:destroy) }
63
    it { is_expected.to have_many(:todos).dependent(:destroy) }
ubudzisz's avatar
ubudzisz committed
64
65
66
67
68
    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) }
69
70

    describe '#members & #requesters' do
71
      let(:project) { create(:project, :public) }
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
      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
gitlabhq's avatar
gitlabhq committed
97
98
  end

99
100
101
102
103
104
  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) }
105
    it { is_expected.to include_module(Gitlab::CurrentSettings) }
106
107
    it { is_expected.to include_module(Referable) }
    it { is_expected.to include_module(Sortable) }
108
109
  end

110
  describe 'validation' do
111
112
    let!(:project) { create(:project) }

113
114
    it { is_expected.to validate_presence_of(:name) }
    it { is_expected.to validate_uniqueness_of(:name).scoped_to(:namespace_id) }
115
    it { is_expected.to validate_length_of(:name).is_within(0..255) }
116

117
118
    it { is_expected.to validate_presence_of(:path) }
    it { is_expected.to validate_uniqueness_of(:path).scoped_to(:namespace_id) }
119
120
    it { is_expected.to validate_length_of(:path).is_within(0..255) }
    it { is_expected.to validate_length_of(:description).is_within(0..2000) }
121
122
    it { is_expected.to validate_presence_of(:creator) }
    it { is_expected.to validate_presence_of(:namespace) }
123
    it { is_expected.to validate_presence_of(:repository_storage) }
124

125
    it 'does not allow new projects beyond user limits' do
126
      project2 = build(:project)
127
128
      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
129
      expect(project2.errors[:limit_reached].first).to match(/Personal project creation is not allowed/)
130
    end
131
132
133

    describe 'wiki path conflict' do
      context "when the new path has been used by the wiki of other Project" do
134
        it 'has an error on the name attribute' do
135
136
137
138
139
140
141
142
          new_project = build_stubbed(:project, namespace_id: project.namespace_id, path: "#{project.path}.wiki")

          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
143
        it 'has an error on the name attribute' do
144
145
146
147
148
149
150
151
          project_with_wiki_suffix = create(:project, path: 'foo.wiki')
          new_project = build_stubbed(:project, namespace_id: project_with_wiki_suffix.namespace_id, path: 'foo')

          expect(new_project).not_to be_valid
          expect(new_project.errors[:name].first).to eq('has already been taken')
        end
      end
    end
152
153
154
155
156
157
158
159
160

    context 'repository storages inclussion' do
      let(:project2) { build(:project, repository_storage: 'missing') }

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

161
      it "does not allow repository storages that don't match a label in the configuration" do
162
163
164
165
        expect(project2).not_to be_valid
        expect(project2.errors[:repository_storage].first).to match(/is not included in the list/)
      end
    end
166

167
    it 'does not allow an invalid URI as import_url' do
James Lopez's avatar
James Lopez committed
168
169
170
171
172
      project2 = build(:project, import_url: 'invalid://')

      expect(project2).not_to be_valid
    end

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

176
177
      expect(project2).to be_valid
    end
178

179
    it 'allows an empty URI' do
180
181
      project2 = build(:project, import_url: '')

182
      expect(project2).to be_valid
183
184
185
186
187
188
189
190
191
192
193
194
195
    end

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

      expect(project2.import_data).to be_nil
    end

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

      expect(project2.import_data).to be_nil
    end
gitlabhq's avatar
gitlabhq committed
196
  end
197

198
199
200
201
202
203
204
205
206
  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

207
  describe 'project token' do
208
    it 'sets an random token if none provided' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
209
210
      project = FactoryGirl.create :empty_project, runners_token: ''
      expect(project.runners_token).not_to eq('')
211
212
    end

ubudzisz's avatar
ubudzisz committed
213
    it 'does not set an random token if one provided' do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
214
215
      project = FactoryGirl.create :empty_project, runners_token: 'my-token'
      expect(project.runners_token).to eq('my-token')
216
217
    end
  end
gitlabhq's avatar
gitlabhq committed
218

219
  describe 'Respond to' do
220
221
222
223
224
225
    it { is_expected.to respond_to(:url_to_repo) }
    it { is_expected.to respond_to(:repo_exists?) }
    it { is_expected.to respond_to(:update_merge_requests) }
    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
226
227
  end

228
229
230
231
232
233
234
  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

235
236
237
238
239
240
241
242
  describe '#to_reference' do
    let(:project) { create(:empty_project) }

    it 'returns a String reference to the object' do
      expect(project.to_reference).to eq project.path_with_namespace
    end
  end

243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
  describe '#repository_storage_path' do
    let(:project) { create(:project, repository_storage: 'custom') }

    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

261
  it 'returns valid url to repo' do
262
    project = Project.new(path: 'somewhere')
263
    expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git')
gitlabhq's avatar
gitlabhq committed
264
265
  end

Douwe Maan's avatar
Douwe Maan committed
266
267
268
269
270
271
  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
272
273
  end

Douwe Maan's avatar
Douwe Maan committed
274
275
276
277
278
279
  describe "#web_url_without_protocol" do
    let(:project) { create(:empty_project, path: "somewhere") }

    it 'returns the web URL without the protocol for this repo' do
      expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/#{project.namespace.path}/somewhere")
    end
280
281
  end

282
  xdescribe "#new_issue_address" do
283
284
285
    let(:project) { create(:empty_project, path: "somewhere") }
    let(:user) { create(:user) }

286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
    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
        token = user.authentication_token
        address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab"

        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
303

304
305
306
      it 'returns nil' do
        expect(project.new_issue_address(user)).to be_nil
      end
307
308
309
    end
  end

310
  describe 'last_activity methods' do
311
312
    let(:timestamp) { Time.now - 2.hours }
    let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
gitlabhq's avatar
gitlabhq committed
313

314
    describe 'last_activity' do
315
      it 'alias last_activity to last_event' do
316
317
        last_event = create(:event, project: project)

318
        expect(project.last_activity).to eq(last_event)
319
      end
gitlabhq's avatar
gitlabhq committed
320
321
    end

322
323
    describe 'last_activity_date' do
      it 'returns the creation date of the project\'s last event if present' do
324
325
326
327
        expect_any_instance_of(Event).to receive(:try_obtain_lease).and_return(true)
        new_event = create(:event, project: project, created_at: Time.now)

        expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
328
      end
329

330
      it 'returns the project\'s last update date if it has no events' do
331
        expect(project.last_activity_date).to eq(project.updated_at)
332
      end
333
334
    end
  end
335

336
337
  describe '#get_issue' do
    let(:project) { create(:empty_project) }
Stan Hu's avatar
Stan Hu committed
338
    let!(:issue)  { create(:issue, project: project) }
339
340
341
342
343
344

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

Stan Hu's avatar
Stan Hu committed
345
346
347
348
      it 'returns count of open issues' do
        expect(project.open_issues_count).to eq(1)
      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
      it 'returns nil when no issue found' do
        expect(project.get_issue(999)).to be_nil
      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
        issue = project.get_issue('FOO-1234')
        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

382
  describe '#update_merge_requests' do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
383
    let(:project) { create(:project) }
384
385
386
387
    let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
    let(:key) { create(:key, user_id: project.owner.id) }
    let(:prev_commit_id) { merge_request.commits.last.id }
    let(:commit_id) { merge_request.commits.first.id }
388

389
    it 'closes merge request if last commit from source branch was pushed to target branch' do
390
391
      project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.target_branch}", key.user)
      merge_request.reload
392
      expect(merge_request.merged?).to be_truthy
393
394
    end

395
    it 'updates merge request commits with new one if pushed to source branch' do
396
397
      project.update_merge_requests(prev_commit_id, commit_id, "refs/heads/#{merge_request.source_branch}", key.user)
      merge_request.reload
398
      expect(merge_request.diff_head_sha).to eq(commit_id)
399
400
    end
  end
401

402
  describe '.find_with_namespace' do
403
404
405
    context 'with namespace' do
      before do
        @group = create :group, name: 'gitlab'
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
406
        @project = create(:project, name: 'gitlabhq', namespace: @group)
407
408
      end

409
      it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) }
410
      it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
411
      it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
412
    end
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428

    context 'when multiple projects using a similar name exist' do
      let(:group) { create(:group, name: 'gitlab') }

      let!(:project1) do
        create(:empty_project, name: 'gitlab1', path: 'gitlab', namespace: group)
      end

      let!(:project2) do
        create(:empty_project, name: 'gitlab2', path: 'GITLAB', namespace: group)
      end

      it 'returns the row where the path matches literally' do
        expect(Project.find_with_namespace('gitlab/GITLAB')).to eq(project2)
      end
    end
429
430
  end

431
  describe '#to_param' do
432
433
434
    context 'with namespace' do
      before do
        @group = create :group, name: 'gitlab'
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
435
        @project = create(:project, name: 'gitlabhq', namespace: @group)
436
437
      end

Vinnie Okada's avatar
Vinnie Okada committed
438
      it { expect(@project.to_param).to eq('gitlabhq') }
439
    end
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457

    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
458
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
459

460
  describe '#repository' do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
461
462
    let(:project) { create(:project) }

463
    it 'returns valid repo' do
464
      expect(project.repository).to be_kind_of(Repository)
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
465
466
    end
  end
467

468
  describe '#default_issues_tracker?' do
469
470
471
    let(:project) { create(:project) }
    let(:ext_project) { create(:redmine_project) }

472
    it "is true if used internal tracker" do
473
      expect(project.default_issues_tracker?).to be_truthy
474
475
    end

476
    it "is false if used other tracker" do
477
      expect(ext_project.default_issues_tracker?).to be_falsey
478
479
480
    end
  end

481
  describe '#external_issue_tracker' do
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
    let(:project) { create(:project) }
    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
      project.build_missing_services
      project.reload

      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
      ext_project.build_missing_services
      ext_project.reload

      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

522
  describe '#cache_has_external_issue_tracker' do
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
    let(:project) { create(:project) }

    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

544
545
  describe '#has_wiki?' do
    let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) }
Felipe Artur's avatar
Felipe Artur committed
546
    let(:wiki_enabled_project) { build(:project) }
547
548
549
550
551
552
553
554
555
    let(:external_wiki_project) { build(:project, has_external_wiki: true) }

    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

556
  describe '#external_wiki' do
557
558
    let(:project) { create(:project) }

559
560
561
562
563
    context 'with an active external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: true)
        project.external_wiki
      end
564

565
566
567
      it 'sets :has_external_wiki as true' do
        expect(project.has_external_wiki).to be(true)
      end
568

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

572
573
574
575
        project.services.external_wikis.first.destroy

        expect(project.has_external_wiki).to be(false)
      end
576
577
    end

578
579
580
581
    context 'with an inactive external wiki' do
      before do
        create(:service, project: project, type: 'ExternalWikiService', active: false)
      end
582

583
584
585
      it 'sets :has_external_wiki as false' do
        expect(project.has_external_wiki).to be(false)
      end
586
587
    end

588
589
590
591
    context 'with no external wiki' do
      before do
        project.external_wiki
      end
592

593
594
595
596
597
598
599
600
601
602
603
      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
604
605
606
    end
  end

607
  describe '#open_branches' do
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
608
    let(:project) { create(:project) }
609
610
611
612
613

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

614
615
    it { expect(project.open_branches.map(&:name)).to include('feature') }
    it { expect(project.open_branches.map(&:name)).not_to include('master') }
616

617
    it "includes branches matching a protected branch wildcard" do
618
619
620
621
      expect(project.open_branches.map(&:name)).to include('feature')

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

622
      expect(Project.find(project.id).open_branches.map(&:name)).to include('feature')
623
    end
624
  end
Ciro Santilli's avatar
Ciro Santilli committed
625

626
627
  describe '#star_count' do
    it 'counts stars from multiple users' do
Ciro Santilli's avatar
Ciro Santilli committed
628
629
630
631
632
      user1 = create :user
      user2 = create :user
      project = create :project, :public

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

Ciro Santilli's avatar
Ciro Santilli committed
634
      user1.toggle_star(project)
635
636
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
637
      user2.toggle_star(project)
638
639
640
      project.reload
      expect(project.reload.star_count).to eq(2)

Ciro Santilli's avatar
Ciro Santilli committed
641
      user1.toggle_star(project)
642
643
644
      project.reload
      expect(project.reload.star_count).to eq(1)

Ciro Santilli's avatar
Ciro Santilli committed
645
      user2.toggle_star(project)
646
647
648
649
      project.reload
      expect(project.reload.star_count).to eq(0)
    end

650
    it 'counts stars on the right project' do
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
      user = create :user
      project1 = create :project, :public
      project2 = create :project, :public

      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
681
682
    end
  end
683

684
  describe '#avatar_type' do
685
686
    let(:project) { create(:project) }

687
    it 'is true if avatar is image' do
688
      project.update_attribute(:avatar, 'uploads/avatar.png')
689
      expect(project.avatar_type).to be_truthy
690
691
    end

692
    it 'is false if avatar is html page' do
693
      project.update_attribute(:avatar, 'uploads/avatar.html')
694
      expect(project.avatar_type).to eq(['only images allowed'])
695
696
    end
  end
sue445's avatar
sue445 committed
697

698
  describe '#avatar_url' do
sue445's avatar
sue445 committed
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
    subject { project.avatar_url }

    let(:project) { create(:project) }

    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

      it { should eq "http://localhost#{avatar_path}" }
    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

      it { should eq "http://localhost#{avatar_path}" }
    end
727
728
729
730
731
732

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

      it { should eq nil }
    end
sue445's avatar
sue445 committed
733
  end
734

735
736
737
  describe '#pipeline_for' do
    let(:project) { create(:project) }
    let!(:pipeline) { create_pipeline }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
738

739
740
    shared_examples 'giving the correct pipeline' do
      it { is_expected.to eq(pipeline) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
741

742
743
      context 'return latest' do
        let!(:pipeline2) { create_pipeline }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
744

745
        it { is_expected.to eq(pipeline2) }
Kamil Trzcinski's avatar
Kamil Trzcinski committed
746
      end
747
748
749
750
751
752
753
754
755
756
757
758
759
    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
760

761
762
763
764
765
    def create_pipeline
      create(:ci_pipeline,
             project: project,
             ref: 'master',
             sha: project.commit('master').sha)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
766
    end
767
  end
768

769
  describe '#builds_enabled' do
770
771
    let(:project) { create :project }

772
773
774
    subject { project.builds_enabled }

    it { expect(project.builds_enabled?).to be_truthy }
775
  end
776

777
778
779
780
781
782
783
784
785
786
787
788
789
790
  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

791
  describe '.trending' do
Felipe Artur's avatar
Felipe Artur committed
792
    let(:group)    { create(:group, :public) }
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
    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)
    end

    describe 'without an explicit start date' do
      subject { described_class.trending.to_a }

      it 'sorts Projects by the amount of notes in descending order' do
        expect(subject).to eq([project1, project2])
      end
    end

    describe 'with an explicit start date' do
      let(:date) { 2.months.ago }

      subject { described_class.trending(date).to_a }

      before do
        2.times do
819
820
821
          # Little fix for special issue related to Fractional Seconds support for MySQL.
          # See: https://github.com/rails/rails/pull/14359/files
          create(:note_on_commit, project: project2, created_at: date + 1)
822
823
824
825
826
827
828
        end
      end

      it 'sorts Projects by the amount of notes in descending order' do
        expect(subject).to eq([project2, project1])
      end
    end
829
830
831
832
833
834
835
836

    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
837
  end
Yorick Peterse's avatar
Yorick Peterse committed
838
839
840
841
842
843
844
845
846

  describe '.visible_to_user' do
    let!(:project) { create(:project, :private) }
    let!(:user)    { create(:user) }

    subject { described_class.visible_to_user(user) }

    describe 'when a user has access to a project' do
      before do
847
        project.add_user(user, Gitlab::Access::MASTER)
Yorick Peterse's avatar
Yorick Peterse committed
848
849
850
851
852
853
854
855
856
      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
857

858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
  context 'repository storage by default' do
    let(:project) { create(:empty_project) }

    subject { project.repository_storage }

    before do
      storages = { 'alternative_storage' => '/some/path' }
      allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
      stub_application_setting(repository_storage: 'alternative_storage')
      allow_any_instance_of(Project).to receive(:ensure_dir_exist).and_return(true)
    end

    it { is_expected.to eq('alternative_storage') }
  end

873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
  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

891
  describe '#any_runners' do
892
    let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
893
894
    let(:specific_runner) { create(:ci_runner) }
    let(:shared_runner) { create(:ci_runner, :shared) }
895
896
897

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

899
      it 'has no runners available' do
900
901
        expect(project.any_runners?).to be_falsey
      end
902

903
      it 'has a specific runner' do
904
        project.runners << specific_runner
905
906
        expect(project.any_runners?).to be_truthy
      end
907

908
      it 'has a shared runner, but they are prohibited to use' do
909
910
911
        shared_runner
        expect(project.any_runners?).to be_falsey
      end
912

913
      it 'checks the presence of specific runner' do
914
        project.runners << specific_runner
915
916
917
        expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
      end
    end
918

919
920
    context 'for shared runners enabled' do
      let(:shared_runners_enabled) { true }
921

922
      it 'has a shared runner' do
923
924
925
926
927
928
929
930
931
932
        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
933
934

  describe '#visibility_level_allowed?' do
935
    let(:project) { create(:project, :internal) }
936
937
938
939
940
941
942
943

    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
944
945
      let(:project)        { create(:project, :internal) }
      let(:forked_project) { create(:project, forked_from_project: project) }
946
947
948
949
950

      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
951
  end
952

953
954
  describe '.search' do
    let(:project) { create(:project, description: 'kitten mittens') }
955

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

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

964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
    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
1003
1004
1005
1006
1007

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

      expect(relation.search(project.namespace.name)).to eq([project])
1008
    end
1009
  end
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020

  describe '#rename_repo' do
    let(:project) { create(:project) }
    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
1021
    end
1022

Kamil Trzcinski's avatar
Kamil Trzcinski committed
1023
    it 'renames a repository' do
1024
1025
1026
1027
      ns = project.namespace_dir

      expect(gitlab_shell).to receive(:mv_repository).
        ordered.
1028
        with(project.repository_storage_path, "#{ns}/foo", "#{ns}/#{project.path}").
1029
1030
1031
1032
        and_return(true)

      expect(gitlab_shell).to receive(:mv_repository).
        ordered.
1033
        with(project.repository_storage_path, "#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki").
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
        and_return(true)

      expect_any_instance_of(SystemHooksService).
        to receive(:execute_hooks_for).
        with(project, :rename)

      expect_any_instance_of(Gitlab::UploadsTransfer).
        to receive(:rename_project).
        with('foo', project.path, ns)

      expect(project).to receive(:expire_caches_before_rename)

      project.rename_repo
    end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058

    context 'container registry with tags' do
      before do
        stub_container_registry_config(enabled: true)
        stub_container_registry_tags('tag')
      end

      subject { project.rename_repo }

      it { expect{subject}.to raise_error(Exception) }
    end
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
  end

  describe '#expire_caches_before_rename' do
    let(:project) { create(:project) }
    let(:repo)    { double(:repo, exists?: true) }
    let(:wiki)    { double(:wiki, exists?: true) }

    it 'expires the caches of the repository and wiki' do
      allow(Repository).to receive(:new).
        with('foo', project).
        and_return(repo)

      allow(Repository).to receive(:new).
        with('foo.wiki', project).
        and_return(wiki)

1075
1076
      expect(repo).to receive(:before_delete)
      expect(wiki).to receive(:before_delete)
1077
1078
1079
1080

      project.expire_caches_before_rename('foo')
    end
  end
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096

  describe '.search_by_title' do
    let(:project) { create(:project, name: 'kittens') }

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

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

    it 'returns projects with a matching name regardless of the casing' do
      expect(described_class.search_by_title('KITTENS')).to eq([project])
    end
  end
1097
1098
1099
1100
1101

  context 'when checking projects from groups' do
    let(:private_group)    { create(:group, visibility_level: 0)  }
    let(:internal_group)   { create(:group, visibility_level: 10) }

Douwe Maan's avatar
Douwe Maan committed
1102
1103
    let(:private_project)  { create :project, :private, group: private_group }
    let(:internal_project) { create :project, :internal, group: internal_group }
1104
1105
1106
1107
1108
1109
1110
1111
1112

    context 'when group is private project can not be internal' do
      it { expect(private_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_falsey }
    end

    context 'when group is internal project can not be public' do
      it { expect(internal_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
    end
  end
1113

1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
  describe '#create_repository' do
    let(:project) { create(:project) }
    let(:shell) { Gitlab::Shell.new }

    before do
      allow(project).to receive(:gitlab_shell).and_return(shell)
    end

    context 'using a regular repository' do
      it 'creates the repository' do
        expect(shell).to receive(:add_repository).
1125
          with(project.repository_storage_path, project.path_with_namespace).
1126
1127
1128
1129
1130
1131
1132
1133
1134
          and_return(true)

        expect(project.repository).to receive(:after_create)

        expect(project.create_repository).to eq(true)
      end

      it 'adds an error if the repository could not be created' do
        expect(shell).to receive(:add_repository).
1135
          with(project.repository_storage_path, project.path_with_namespace).
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
          and_return(false)

        expect(project.repository).not_to receive(:after_create)

        expect(project.create_repository).to eq(false)
        expect(project.errors).not_to be_empty
      end
    end

    context 'using a forked repository' do
      it 'does nothing' do
        expect(project).to receive(:forked?).and_return(true)
        expect(shell).not_to receive(:add_repository)

        project.create_repository
      end
    end
  end
1154
1155

  describe '#protected_branch?' do
1156
1157
1158
1159
    context 'existing project' do
      let(:project) { create(:project) }

      it 'returns true when the branch matches a protected branch via direct match' do
1160
        create(:protected_branch, project: project, name: "foo")
1161
1162
1163
1164
1165

        expect(project.protected_branch?('foo')).to eq(true)
      end

      it 'returns true when the branch matches a protected branch via wildcard match' do
1166
        create(:protected_branch, project: project, name: "production/*")
1167
1168
1169
1170
1171
1172
1173
1174
1175

        expect(project.protected_branch?('production/some-branch')).to eq(true)
      end

      it 'returns false when the branch does not match a protected branch via direct match' do
        expect(project.protected_branch?('foo')).to eq(false)
      end

      it 'returns false when the branch does not match a protected branch via wildcard match' do
1176
        create(:protected_branch, project: project, name: "production/*")
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210

        expect(project.protected_branch?('staging/some-branch')).to eq(false)
      end
    end

    context "new project" do
      let(:project) { create(:empty_project) }

      it 'returns false when default_protected_branch is unprotected' do
        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)

        expect(project.protected_branch?('master')).to be false
      end

      it 'returns false when default_protected_branch lets developers push' do
        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)

        expect(project.protected_branch?('master')).to be false
      end

      it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)

        expect(project.protected_branch?('master')).to be true
      end

      it 'returns true when default_branch_protection is in full protection' do
        stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)

        expect(project.protected_branch?('master')).to be true
      end
    end
  end

1211
  describe '#user_can_push_to_empty_repo?' do
1212
    let(:project) { create(:empty_project) }
1213
    let(:user)    { create(:user) }
1214

1215
1216
1217
    it 'returns false when default_branch_protection is in full protection and user is developer' do
      project.team << [user, :developer]
      stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
1218

1219
      expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
1220
1221
    end

1222
1223
1224
    it 'returns false when default_branch_protection only lets devs merge and user is dev' do
      project.team << [user, :developer]
      stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
1225

1226
      expect(project.user_can_push_to_empty_repo?(user)).to be_falsey
1227
1228
    end

1229
1230
1231
    it 'returns true when default_branch_protection lets devs push and user is developer' do
      project.team << [user, :developer]
      stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
1232

1233
1234
      expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
    end
1235

1236
1237
1238
    it 'returns true when default_branch_protection is unprotected and user is developer' do
      project.team << [user, :developer]
      stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
1239

1240
      expect(project.user_can_push_to_empty_repo?(user)).to be_truthy
Yorick Peterse's avatar