Refactor project membership and private repo functionality into generic protected...
[gitorious:mainline.git] / test / unit / repository_test.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2012 Gitorious AS
4 #   Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies)
5 #
6 #   This program is free software: you can redistribute it and/or modify
7 #   it under the terms of the GNU Affero General Public License as published by
8 #   the Free Software Foundation, either version 3 of the License, or
9 #   (at your option) any later version.
10 #
11 #   This program is distributed in the hope that it will be useful,
12 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
13 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 #   GNU Affero General Public License for more details.
15 #
16 #   You should have received a copy of the GNU Affero General Public License
17 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #++
19
20 require File.dirname(__FILE__) + "/../test_helper"
21 require "ostruct"
22
23 class RepositoryTest < ActiveSupport::TestCase
24   def new_repos(opts={})
25     Repository.new({
26       :name => "foo",
27       :project => projects(:johans),
28       :user => users(:johan),
29       :owner => users(:johan)
30     }.merge(opts))
31   end
32
33   def setup
34     @repository = new_repos
35     FileUtils.mkdir_p(@repository.full_repository_path, :mode => 0755)
36   end
37
38   def teardown
39     clear_message_queue
40   end
41
42   should_validate_presence_of :user_id, :name, :owner_id
43   should_validate_uniqueness_of :hashed_path
44   should_validate_uniqueness_of :name, :scoped_to => :project_id, :case_sensitive => false
45
46   should_have_many :hooks, :dependent => :destroy
47
48   should " only accept names with alphanum characters in it" do
49     @repository.name = "foo bar"
50     assert !@repository.valid?, "valid? should be false"
51
52     @repository.name = "foo!bar"
53     assert !@repository.valid?, "valid? should be false"
54
55     @repository.name = "foobar"
56     assert @repository.valid?
57
58     @repository.name = "foo42"
59     assert @repository.valid?
60   end
61
62   should "has a unique name within a project" do
63     @repository.save
64     repos = new_repos(:name => "FOO")
65     assert !repos.valid?, "valid? should be false"
66     assert_not_nil repos.errors.on(:name)
67
68     assert new_repos(:project => projects(:moes)).valid?
69   end
70
71   should "not have a reserved name" do
72     repo = new_repos(:name => Gitorious::Reservations.repository_names.first.dup)
73     repo.valid?
74     assert_not_nil repo.errors.on(:name)
75     RepositoriesController.action_methods.each do |action|
76       repo.name = action.dup
77       repo.valid?
78       assert_not_nil repo.errors.on(:name), "fail on #{action}"
79     end
80   end
81
82   context "git urls" do
83     setup do
84       @host_with_user = "#{GitoriousConfig['gitorious_user']}@#{GitoriousConfig['gitorious_host']}"
85       @host = "#{GitoriousConfig['gitorious_host']}"
86     end
87
88     should "has a gitdir name" do
89       assert_equal "#{@repository.project.slug}/foo.git", @repository.gitdir
90     end
91
92     should "has a push url" do
93       assert_equal "#{@host_with_user}:#{@repository.project.slug}/foo.git", @repository.push_url
94     end
95
96     should "has a clone url" do
97       assert_equal "git://#{@host}/#{@repository.project.slug}/foo.git", @repository.clone_url
98     end
99
100     should "has a http url" do
101       assert_equal "#{GitoriousConfig['scheme']}://git.#{@host}/#{@repository.project.slug}/foo.git", @repository.http_clone_url
102     end
103
104     should "use the real http cloning URL" do
105       old_value = Site::HTTP_CLONING_SUBDOMAIN
106       silence_warnings do
107         Site::HTTP_CLONING_SUBDOMAIN = "whatever"
108       end
109       assert_equal "#{GitoriousConfig['scheme']}://whatever.#{@host}/#{@repository.project.slug}/foo.git", @repository.http_clone_url
110       silence_warnings {Site::HTTP_CLONING_SUBDOMAIN = old_value}
111     end
112
113     should "has a clone url with the project name, if it is a mainline" do
114       @repository.owner = groups(:team_thunderbird)
115       @repository.kind = Repository::KIND_PROJECT_REPO
116       assert_equal "git://#{@host}/#{@repository.project.slug}/foo.git", @repository.clone_url
117     end
118
119     should "have a clone url with the team/user, if it is not a mainline" do
120       @repository.kind = Repository::KIND_TEAM_REPO
121       url = "git://#{@host}/#{@repository.owner.to_param_with_prefix}/#{@repository.project.slug}/foo.git"
122       assert_equal url, @repository.clone_url
123
124       @repository.kind = Repository::KIND_USER_REPO
125       @repository.owner = users(:johan)
126       url = "git://#{@host}/#{users(:johan).to_param_with_prefix}/#{@repository.project.slug}/foo.git"
127       assert_equal url, @repository.clone_url
128     end
129
130     should "has a push url with the project name, if it is a mainline" do
131       @repository.owner = groups(:team_thunderbird)
132       @repository.kind = Repository::KIND_PROJECT_REPO
133       assert_equal "#{@host_with_user}:#{@repository.project.slug}/foo.git", @repository.push_url
134     end
135
136     should "have a push url with the team/user, if it is not a mainline" do
137       @repository.owner = groups(:team_thunderbird)
138       @repository.kind = Repository::KIND_TEAM_REPO
139       url = "#{@host_with_user}:#{groups(:team_thunderbird).to_param_with_prefix}/#{@repository.project.slug}/foo.git"
140       assert_equal url, @repository.push_url
141
142       @repository.kind = Repository::KIND_USER_REPO
143       @repository.owner = users(:johan)
144       url = "#{@host_with_user}:#{users(:johan).to_param_with_prefix}/#{@repository.project.slug}/foo.git"
145       assert_equal url, @repository.push_url
146     end
147
148     should "has a http clone url with the project name, if it is a mainline" do
149       @repository.owner = groups(:team_thunderbird)
150       @repository.kind = Repository::KIND_PROJECT_REPO
151       assert_equal "#{GitoriousConfig['scheme']}://git.#{@host}/#{@repository.project.slug}/foo.git", @repository.http_clone_url
152     end
153
154     should "have a http clone url with the team/user, if it is not a mainline" do
155       @repository.owner = groups(:team_thunderbird)
156       @repository.kind = Repository::KIND_TEAM_REPO
157       url = "#{GitoriousConfig['scheme']}://git.#{@host}/#{groups(:team_thunderbird).to_param_with_prefix}/#{@repository.project.slug}/foo.git"
158       assert_equal url, @repository.http_clone_url
159
160       @repository.owner = users(:johan)
161       @repository.kind = Repository::KIND_USER_REPO
162       url = "#{GitoriousConfig['scheme']}://git.#{@host}/#{users(:johan).to_param_with_prefix}/#{@repository.project.slug}/foo.git"
163       assert_equal url, @repository.http_clone_url
164     end
165
166     should "has a full repository_path" do
167       expected_dir = File.expand_path(File.join(GitoriousConfig["repository_base_path"],
168         "#{@repository.full_hashed_path}.git"))
169       assert_equal expected_dir, @repository.full_repository_path
170     end
171
172     should "always display SSH URLs when so instructed" do
173       old_value = GitoriousConfig["always_display_ssh_url"]
174       GitoriousConfig["always_display_ssh_url"] = true
175       assert @repository.display_ssh_url?(users(:moe))
176       GitoriousConfig["always_display_ssh_url"] = old_value
177     end
178   end
179
180   should "inits the git repository" do
181     path = @repository.full_repository_path
182     Repository.git_backend.expects(:create).with(path).returns(true)
183     Repository.create_git_repository(@repository.real_gitdir)
184
185     assert File.exist?(path), "File.exist?(path) should be true"
186
187     Dir.chdir(path) do
188       hooks = File.join(path, "hooks")
189       assert File.exist?(hooks), "File.exist?(hooks) should be true"
190       assert File.symlink?(hooks), "File.symlink?(hooks) should be true"
191       assert File.symlink?(File.expand_path(File.readlink(hooks))), "File.symlink?(File.expand_path(File.readlink(hooks))) should be true"
192     end
193   end
194
195   should "clones a git repository" do
196     source = repositories(:johans)
197     target = @repository
198     target_path = @repository.full_repository_path
199
200     git_backend = mock("Git backend")
201     Repository.expects(:git_backend).returns(git_backend)
202     git_backend.expects(:clone).with(target.full_repository_path,
203       source.full_repository_path).returns(true)
204     Repository.expects(:create_hooks).returns(true)
205
206     assert Repository.clone_git_repository(target.real_gitdir, source.real_gitdir)
207   end
208
209   should "not create hooks if the :skip_hooks option is set to true" do
210     source = repositories(:johans)
211     target = @repository
212     target_path = @repository.full_repository_path
213
214     git_backend = mock("Git backend")
215     Repository.expects(:git_backend).returns(git_backend)
216     git_backend.expects(:clone).with(target.full_repository_path,
217       source.full_repository_path).returns(true)
218     Repository.expects(:create_hooks).never
219
220     Repository.clone_git_repository(target.real_gitdir, source.real_gitdir, :skip_hooks => true)
221   end
222
223   should " create the hooks" do
224     hooks = "/path/to/hooks"
225     path = "/path/to/repository"
226     base_path = "#{RAILS_ROOT}/data/hooks"
227
228     File.expects(:join).in_sequence.with(GitoriousConfig["repository_base_path"], ".hooks").returns(hooks)
229
230     Dir.expects(:chdir).in_sequence.with(path).yields(nil)
231
232     File.expects(:symlink?).in_sequence.with(hooks).returns(false)
233     File.expects(:exist?).in_sequence.with(hooks).returns(false)
234     FileUtils.expects(:ln_s).in_sequence.with(base_path, hooks)
235
236     local_hooks = "/path/to/local/hooks"
237     File.expects(:join).in_sequence.with(path, "hooks").returns(local_hooks)
238
239     File.expects(:exist?).in_sequence.with(local_hooks).returns(true)
240
241     File.expects(:join).with(path, "description").in_sequence
242
243     File.expects(:open).in_sequence.returns(true)
244
245     assert Repository.create_hooks(path)
246   end
247
248   should "deletes a repository" do
249     Repository.git_backend.expects(:delete!).with(@repository.full_repository_path).returns(true)
250     Repository.delete_git_repository(@repository.real_gitdir)
251   end
252
253   should "knows if has commits" do
254     @repository.stubs(:new_record?).returns(false)
255     @repository.stubs(:ready?).returns(true)
256     git_mock = mock("Grit::Git")
257     @repository.stubs(:git).returns(git_mock)
258     head = mock("head")
259     head.stubs(:name).returns("master")
260     @repository.git.expects(:heads).returns([head])
261     assert @repository.has_commits?, "@repository.has_commits? should be true"
262   end
263
264   should "knows if has commits, unless its a new record" do
265     @repository.stubs(:new_record?).returns(false)
266     assert !@repository.has_commits?, "@repository.has_commits? should be false"
267   end
268
269   should "knows if has commits, unless its not ready" do
270     @repository.stubs(:ready?).returns(false)
271     assert !@repository.has_commits?, "@repository.has_commits? should be false"
272   end
273
274   should " build a new repository by cloning another one" do
275     repos = Repository.new_by_cloning(@repository)
276     assert_equal @repository, repos.parent
277     assert_equal @repository.project, repos.project
278     assert repos.new_record?, "new_record? should be true"
279   end
280
281   should "inherit merge request inclusion from its parent" do
282     @repository.update_attribute(:merge_requests_enabled, true)
283     clone = Repository.new_by_cloning(@repository)
284     assert clone.merge_requests_enabled?
285     @repository.update_attribute(:merge_requests_enabled, false)
286     clone = Repository.new_by_cloning(@repository)
287     assert !clone.merge_requests_enabled?
288   end
289
290   should "suggests a decent name for a cloned repository bsed on username" do
291     repos = Repository.new_by_cloning(@repository, username="johan")
292     assert_equal "johans-foo", repos.name
293     repos = Repository.new_by_cloning(@repository, username=nil)
294     assert_equal nil, repos.name
295   end
296
297   should "has it is name as its to_param value" do
298     @repository.save
299     assert_equal @repository.name, @repository.to_param
300   end
301
302   should "finds a repository by name or raises" do
303     assert_equal repositories(:johans), Repository.find_by_name!(repositories(:johans).name)
304     assert_raises(ActiveRecord::RecordNotFound) do
305       Repository.find_by_name!("asdasdasd")
306     end
307   end
308
309   context "find_by_path" do
310     should "finds a repository by its path" do
311       repo = repositories(:johans)
312       path = File.join(GitoriousConfig["repository_base_path"],
313                         projects(:johans).slug, "#{repo.name}.git")
314       assert_equal repo, Repository.find_by_path(path)
315     end
316
317     should_eventually "finds a repository by its path, regardless of repository kind" do
318       repo = projects(:johans).wiki_repository
319       path = File.join(GitoriousConfig["repository_base_path"].chomp("/"),
320                         projects(:johans).slug, "#{repo.name}.git")
321       assert_equal repo, Repository.find_by_path(path)
322     end
323
324     should "finds a group repository by its path" do
325       repo = repositories(:johans)
326       repo.owner = groups(:team_thunderbird)
327       repo.kind = Repository::KIND_TEAM_REPO
328       repo.save!
329       path = File.join(GitoriousConfig["repository_base_path"], repo.gitdir)
330       assert_equal repo, Repository.find_by_path(path)
331     end
332
333     should "finds a user repository by its path" do
334       repo = repositories(:johans)
335       repo.owner = users(:johan)
336       repo.kind = Repository::KIND_USER_REPO
337       repo.save!
338       path = File.join(GitoriousConfig["repository_base_path"], repo.gitdir)
339       assert_equal repo, Repository.find_by_path(path)
340     end
341
342     should "scope the find by project id" do
343       repo = repositories(:johans)
344       repo.owner = groups(:team_thunderbird)
345       repo.kind = Repository::KIND_TEAM_REPO
346       repo.save!
347       same_name_repo = new_repos(:name => repo.name)
348       same_name_repo
349       same_name_repo.project = projects(:moes)
350       same_name_repo.owner = groups(:team_thunderbird)
351       same_name_repo.kind = Repository::KIND_TEAM_REPO
352       same_name_repo.save!
353       path = File.join(GitoriousConfig["repository_base_path"], same_name_repo.gitdir)
354       assert_equal same_name_repo, Repository.find_by_path(path)
355     end
356   end
357
358   context "#to_xml" do
359     should "xmlilizes git paths as well" do
360       assert @repository.to_xml.include?("<clone-url>")
361       assert @repository.to_xml.include?("<push-url>")
362     end
363
364     should "include a description of the kind" do
365       assert_match(/<kind>mainline<\/kind>/, @repository.to_xml)
366       @repository.kind = Repository::KIND_TEAM_REPO
367       assert_match(/<kind>team<\/kind>/, @repository.to_xml)
368     end
369
370     should "include the project name" do
371       assert_match(/<project>#{@repository.project.slug}<\/project>/, @repository.to_xml)
372     end
373
374     should "include the owner" do
375       assert_match(/<owner kind="User">johan<\/owner>/, @repository.to_xml)
376     end
377   end
378
379   context "can_push?" do
380     should "knows if a user can write to self" do
381       @repository.owner = users(:johan)
382       @repository.save!
383       @repository.reload
384       assert can_push?(users(:johan), @repository)
385       assert !can_push?(users(:mike), @repository)
386
387       @repository.change_owner_to!(groups(:team_thunderbird))
388       @repository.save!
389       assert !can_push?(users(:johan), @repository)
390
391       @repository.owner.add_member(users(:moe), Role.member)
392       @repository.committerships.reload
393       assert can_push?(users(:moe), @repository)
394     end
395
396     context "a wiki repository" do
397       setup do
398         @repository.kind = Repository::KIND_WIKI
399       end
400
401       should "be writable by everyone" do
402         @repository.wiki_permissions = Repository::WIKI_WRITABLE_EVERYONE
403         [:johan, :mike, :moe].each do |login|
404           assert can_push?(users(login), @repository), "not writable_by #{login}"
405         end
406       end
407
408       should "only be writable by project members" do
409         @repository.wiki_permissions = Repository::WIKI_WRITABLE_PROJECT_MEMBERS
410         assert @repository.project.member?(users(:johan))
411         assert can_push?(users(:johan), @repository)
412
413         assert !@repository.project.member?(users(:moe))
414         assert !can_push?(users(:moe), @repository)
415       end
416     end
417   end
418
419   should "publishe a message on create and update" do
420     @repository.save!
421
422     assert_published("/queue/GitoriousRepositoryCreation", {
423                        "command" => "create_git_repository",
424                        "target_id" => @repository.id,
425                        "arguments" => [@repository.real_gitdir]
426                      })
427   end
428
429   should "publishe a message on clone" do
430     @repository.parent = repositories(:johans)
431     @repository.save!
432
433     assert_published("/queue/GitoriousRepositoryCreation", {
434                        "command" => "clone_git_repository",
435                        "target_id" => @repository.id,
436                        "arguments" => [@repository.real_gitdir,
437                                        @repository.parent.real_gitdir]
438                      })
439   end
440
441   should "create a notification on destroy" do
442     @repository.save!
443     @repository.destroy
444
445     assert_published("/queue/GitoriousRepositoryDeletion", {
446                        "command" => "delete_git_repository",
447                        "arguments" => [@repository.real_gitdir]
448                      })
449   end
450
451   should "has one recent commit" do
452     @repository.save!
453     repos_mock = mock("Git mock")
454     commit_mock = stub_everything("Git::Commit mock")
455     repos_mock.expects(:commits).with("master", 1).returns([commit_mock])
456     @repository.stubs(:git).returns(repos_mock)
457     @repository.stubs(:has_commits?).returns(true)
458     heads_stub = mock("head")
459     heads_stub.stubs(:name).returns("master")
460     @repository.stubs(:head_candidate).returns(heads_stub)
461     assert_equal commit_mock, @repository.last_commit
462   end
463
464   should "has one recent commit within a given ref" do
465     @repository.save!
466     repos_mock = mock("Git mock")
467     commit_mock = stub_everything("Git::Commit mock")
468     repos_mock.expects(:commits).with("foo", 1).returns([commit_mock])
469     @repository.stubs(:git).returns(repos_mock)
470     @repository.stubs(:has_commits?).returns(true)
471     @repository.expects(:head_candidate).never
472     assert_equal commit_mock, @repository.last_commit("foo")
473   end
474
475   context "deletion" do
476     setup do
477       @repository.kind = Repository::KIND_PROJECT_REPO
478       @repository.project.repositories << new_repos(:name => "another")
479       @repository.save!
480       @repository.committerships.create!({
481         :committer => users(:moe),
482         :permissions => (Committership::CAN_REVIEW | Committership::CAN_COMMIT)
483       })
484     end
485
486     should "be deletable by admins" do
487       assert admin?(users(:johan), @repository)
488       assert !admin?(users(:moe), @repository)
489
490       assert can_delete?(users(:johan), @repository)
491       assert !can_delete?(users(:moe), @repository)
492     end
493
494     should "always be deletable if admin and non-project repo" do
495       @repository.kind = Repository::KIND_TEAM_REPO
496       assert can_delete?(users(:johan), @repository)
497       assert !can_delete?(users(:moe), @repository)
498     end
499
500     should "also be deletable by users with admin privs" do
501       @repository.kind = Repository::KIND_TEAM_REPO
502       cs = @repository.committerships.create_with_permissions!({
503           :committer => users(:mike)
504         }, Committership::CAN_ADMIN)
505       assert can_delete?(users(:johan), @repository)
506       assert can_delete?(users(:mike), @repository)
507     end
508   end
509
510   should "have a git method that accesses the repository" do
511     # FIXME: meh for stubbing internals, need to refactor that part in Grit
512     File.expects(:exist?).at_least(1).with("#{@repository.full_repository_path}/.git").returns(false)
513     File.expects(:exist?).at_least(1).with(@repository.full_repository_path).returns(true)
514     assert_instance_of Grit::Repo, @repository.git
515     assert_equal @repository.full_repository_path, @repository.git.path
516   end
517
518   should "have a head_candidate" do
519     head_stub = mock("head")
520     head_stub.stubs(:name).returns("master")
521     git = mock("git backend")
522     @repository.stubs(:git).returns(git)
523     git.expects(:head).returns(head_stub)
524     @repository.expects(:has_commits?).returns(true)
525
526     assert_equal head_stub, @repository.head_candidate
527   end
528
529   should "have a head_candidate, unless it does not have commits" do
530     @repository.expects(:has_commits?).returns(false)
531     assert_equal nil, @repository.head_candidate
532   end
533
534   should "has paginated_commits" do
535     git = mock("git")
536     commits = [mock("commit"), mock("commit")]
537     @repository.expects(:git).times(2).returns(git)
538     git.expects(:commit_count).returns(120)
539     git.expects(:commits).with("foo", 30, 30).returns(commits)
540     commits = @repository.paginated_commits("foo", 2, 30)
541     assert_instance_of WillPaginate::Collection, commits
542   end
543
544   should "has a count_commits_from_last_week_by_user of 0 if no commits" do
545     @repository.expects(:has_commits?).returns(false)
546     assert_equal 0, @repository.count_commits_from_last_week_by_user(users(:johan))
547   end
548
549   should "returns a set of users from a list of commits" do
550     commits = []
551     users(:johan, :moe).map do |u|
552       committer = OpenStruct.new(:email => u.email)
553       commits << OpenStruct.new(:committer => committer, :author => committer)
554     end
555     users = Repository.users_by_commits(commits)
556     assert_equal users(:johan, :moe).map(&:email).sort, users.keys.sort
557     assert_equal users(:johan, :moe).map(&:login).sort, users.values.map(&:login).sort
558   end
559
560   should "know if it is a normal project repository" do
561     assert @repository.project_repo?, "@repository.project_repo? should be true"
562   end
563
564   should "know if it is a wiki repo" do
565     @repository.kind = Repository::KIND_WIKI
566     assert @repository.wiki?, "@repository.wiki? should be true"
567   end
568
569   should "has a parent, which is the owner" do
570     @repository.kind = Repository::KIND_TEAM_REPO
571     @repository.owner = groups(:team_thunderbird)
572     assert_equal groups(:team_thunderbird), @repository.breadcrumb_parent
573
574     @repository.kind = Repository::KIND_USER_REPO
575     @repository.owner = users(:johan)
576     assert_equal users(:johan), @repository.breadcrumb_parent
577   end
578
579   should "has a parent, which is the project for mainlines" do
580     @repository.kind = Repository::KIND_PROJECT_REPO
581     @repository.owner = groups(:team_thunderbird)
582     assert_equal projects(:johans), @repository.breadcrumb_parent
583
584     @repository.owner = users(:johan)
585     assert_equal projects(:johans), @repository.breadcrumb_parent
586   end
587
588   should " return its name as title" do
589     assert_equal @repository.title, @repository.name
590   end
591
592   should "return the project title as owner_title if it is a mainline" do
593     @repository.kind = Repository::KIND_PROJECT_REPO
594     assert_equal @repository.project.title, @repository.owner_title
595   end
596
597   should "return the owner title as owner_title if it is not a mainline" do
598     @repository.kind = Repository::KIND_TEAM_REPO
599     assert_equal @repository.owner.title, @repository.owner_title
600   end
601
602   should "returns a list of committers depending on owner type" do
603     repo = repositories(:johans2)
604     repo.committerships.each(&:delete)
605     repo.reload
606     assert !committers(repo).include?(users(:moe))
607
608     repo.committerships.create_with_permissions!({
609         :committer => users(:johan)
610       }, Committership::CAN_COMMIT)
611     assert_equal [users(:johan).login], committers(repo).map(&:login)
612
613     repo.committerships.create_with_permissions!({
614         :committer => groups(:team_thunderbird)
615       }, Committership::CAN_COMMIT)
616     exp_users = groups(:team_thunderbird).members.unshift(users(:johan))
617     assert_equal exp_users.map(&:login), committers(repo).map(&:login)
618
619     groups(:team_thunderbird).add_member(users(:moe), Role.admin)
620     repo.reload
621     assert committers(repo).include?(users(:moe))
622   end
623
624
625   should "know you can request merges from it"  do
626     repo = repositories(:johans2)
627     assert !repo.mainline?
628     assert committer?(users(:mike), repo)
629     assert can_request_merge?(users(:mike), repo)
630
631     repo.kind = Repository::KIND_PROJECT_REPO
632     assert repo.mainline?
633     assert !can_request_merge?(users(:mike), repo), "mainlines should not request merges"
634   end
635
636   should "sets a hash on create" do
637     assert @repository.new_record?, "@repository.new_record? should be true"
638     @repository.save!
639     assert_not_nil @repository.hashed_path
640     assert_equal 3, @repository.hashed_path.split("/").length
641     assert_match(/[a-z0-9\/]{42}/, @repository.hashed_path)
642   end
643
644   should "create the initial committership on create for owner" do
645     group_repo = new_repos(:owner => groups(:team_thunderbird))
646     assert_difference("Committership.count") do
647       group_repo.save!
648       assert_equal 1, group_repo.committerships.count
649       assert_equal groups(:team_thunderbird), group_repo.committerships.first.committer
650     end
651
652     user_repo = new_repos(:owner => users(:johan), :name => "foo2")
653     assert_difference("Committership.count") do
654       user_repo.save!
655       assert_equal 1, user_repo.committerships.count
656       cs = user_repo.committerships.first
657       assert_equal users(:johan), cs.committer
658       [:reviewer?, :committer?, :admin?].each do |m|
659         assert cs.send(m), "should have #{m} permissions"
660       end
661     end
662   end
663
664   should "know the full hashed path" do
665     assert_equal @repository.hashed_path, @repository.full_hashed_path
666   end
667
668   should "allow changing ownership from a user to a group" do
669     repo = repositories(:johans)
670     repo.change_owner_to!(groups(:team_thunderbird))
671     assert_equal groups(:team_thunderbird), repo.owner
672     repo.change_owner_to!(users(:johan))
673     assert_equal groups(:team_thunderbird), repo.owner
674   end
675
676   should "not change kind when it is a project repo and changing owner" do
677     repo = repositories(:johans)
678     repo.change_owner_to!(groups(:team_thunderbird))
679     assert_equal groups(:team_thunderbird), repo.owner
680     assert_equal Repository::KIND_PROJECT_REPO, repo.kind
681   end
682
683   should "change kind when changing owner" do
684     repo = repositories(:johans)
685     repo.update_attribute(:kind, Repository::KIND_USER_REPO)
686     assert repo.user_repo?
687     repo.change_owner_to!(groups(:team_thunderbird))
688     assert_equal groups(:team_thunderbird), repo.owner
689     assert repo.team_repo?
690   end
691
692   should "changing ownership adds the new owner to the committerships" do
693     repo = repositories(:johans)
694     old_committer = repo.owner
695     repo.change_owner_to!(groups(:team_thunderbird))
696     assert !committers(repo).include?(old_committer)
697     assert committers(repo).include?(groups(:team_thunderbird).members.first)
698     [:reviewer?, :committer?, :admin?].each do |m|
699       assert repo.committerships.last.send(m), "cannot #{m}"
700     end
701   end
702
703   should "downcases the name before validation" do
704     repo = new_repos(:name => "FOOBAR")
705     repo.save!
706     assert_equal "foobar", repo.reload.name
707   end
708
709   should "have a project_or_owner" do
710     repo = repositories(:johans)
711     assert repo.project_repo?
712     assert_equal repo.project, repo.project_or_owner
713
714     repo.kind = Repository::KIND_TEAM_REPO
715     repo.owner = groups(:team_thunderbird)
716     assert_equal repo.owner, repo.project_or_owner
717
718     repo.kind = Repository::KIND_TEAM_REPO
719     repo.owner = groups(:team_thunderbird)
720     assert_equal repo.owner, repo.project_or_owner
721   end
722
723   context "participant groups" do
724     setup do
725       @repo = repositories(:moes)
726     end
727
728     should "includes the groups' members in #committers" do
729       assert committers(@repo).include?(groups(:team_thunderbird).members.first)
730     end
731
732     should "only include unique users in #committers" do
733       groups(:team_thunderbird).add_member(users(:moe), Role.member)
734       assert_equal 1, committers(@repo).select{|u| u == users(:moe)}.size
735     end
736
737     should "not include committerships without a commit permission bit" do
738       assert_equal 1, @repo.committerships.count
739       cs = @repo.committerships.first
740       cs.build_permissions(:review)
741       cs.save!
742       assert_equal [], committers(@repo).map(&:login)
743     end
744
745     should "return a list of reviewers" do
746       assert !reviewers(@repo).map(&:login).include?(users(:moe).login)
747       @repo.committerships.create_with_permissions!({
748           :committer => users(:moe)
749         }, Committership::CAN_REVIEW)
750       assert reviewers(@repo).map(&:login).include?(users(:moe).login)
751     end
752
753     context "permission helpers" do
754       setup do
755         @cs = @repo.committerships.first
756         @cs.permissions = 0
757         @cs.save!
758       end
759
760       should "know if a user is a committer" do
761         assert !committer?(@cs.committer, @repo)
762         @cs.build_permissions(:commit); @cs.save
763         assert !committer?(:false, @repo)
764         assert !committer?(@cs.committer, @repo)
765       end
766
767       should "know if a user is a reviewer" do
768         assert !reviewer?(@cs.committer, @repo)
769         @cs.build_permissions(:review); @cs.save
770         assert !reviewer?(:false, @repo)
771         assert !reviewer?(@cs.committer, @repo)
772       end
773
774       should "know if a user is a admin" do
775         assert !admin?(@cs.committer, @repo)
776         @cs.build_permissions(:commit, :admin); @cs.save
777         assert !admin?(:false, @repo)
778         assert !admin?(@cs.committer, @repo)
779       end
780     end
781   end
782
783   context "owners as User or Group" do
784     setup do
785       @repo = repositories(:moes)
786     end
787
788     should "return a list of the users who are admin for the repository if owned_by_group?" do
789       @repo.change_owner_to!(groups(:a_team))
790       assert_equal([users(:johan)], @repo.owners)
791     end
792
793     should "not throw an error if transferring ownership to a group if the group is already a committer" do
794       @repo.change_owner_to!(groups(:team_thunderbird))
795       assert_equal([users(:mike)], @repo.owners)
796     end
797
798     should "return the owner if owned by user" do
799       @repo.change_owner_to!(users(:moe))
800       assert_equal([users(:moe)], @repo.owners)
801     end
802   end
803
804   should "create an event on create if it is a project repo" do
805     repo = new_repos
806     repo.kind = Repository::KIND_PROJECT_REPO
807     assert_difference("repo.project.events.count") do
808       repo.save!
809     end
810     assert_equal repo, Event.last.target
811     assert_equal Action::ADD_PROJECT_REPOSITORY, Event.last.action
812   end
813
814   context "find_by_name_in_project" do
815     should "find with a project" do
816       Repository.expects(:find_by_name_and_project_id!).with(repositories(:johans).name, projects(:johans).id).once
817       Repository.find_by_name_in_project!(repositories(:johans).name, projects(:johans))
818     end
819
820     should "find without a project" do
821       Repository.expects(:find_by_name!).with(repositories(:johans).name).once
822       Repository.find_by_name_in_project!(repositories(:johans).name)
823     end
824   end
825
826   context "Signoff of merge requests" do
827     setup do
828       @project = projects(:johans)
829       @mainline_repository = repositories(:johans)
830       @other_repository = repositories(:johans2)
831     end
832
833     should "know that the mainline repository requires signoff of merge requests" do
834       assert @mainline_repository.mainline?
835       assert @mainline_repository.requires_signoff_on_merge_requests?
836     end
837
838     should "not require signoff of merge requests in other repositories" do
839       assert !@other_repository.mainline?
840       assert !@other_repository.requires_signoff_on_merge_requests?
841     end
842   end
843
844   context "Merge request status tags" do
845     setup {@repo = repositories(:johans)}
846
847     should "have a list of used status tags" do
848       @repo.merge_requests.last.update_attribute(:status_tag, "worksforme")
849       assert_equal %w[open worksforme], @repo.merge_request_status_tags
850     end
851   end
852
853   context "Thottling" do
854     setup{ Repository.destroy_all }
855
856     should "throttle on create" do
857       assert_nothing_raised do
858         5.times{|i| new_repos(:name => "wifebeater#{i}").save! }
859       end
860
861       assert_no_difference("Repository.count") do
862         assert_raises(RecordThrottling::LimitReachedError) do
863           new_repos(:name => "wtf-are-you-doing-bro").save!
864         end
865       end
866     end
867   end
868
869   context "Logging updates" do
870     setup {@repository = repositories(:johans)}
871
872     should "generate events for each value that is changed" do
873       assert_incremented_by(@repository.events, :size, 1) do
874         @repository.log_changes_with_user(users(:johan)) do
875           @repository.replace_value(:name, "new_name")
876         end
877         assert @repository.save
878       end
879       assert_equal "new_name", @repository.reload.name
880     end
881
882     should "not generate events when blank values are provided" do
883       assert_incremented_by(@repository.events, :size, 0) do
884         @repository.log_changes_with_user(users(:johan)) do
885           @repository.replace_value(:name, "")
886         end
887       end
888     end
889
890     should "allow blank updates if we say it is ok" do
891       @repository.update_attribute(:description, "asdf")
892       @repository.log_changes_with_user(users(:johan)) do
893         @repository.replace_value(:description, "", true)
894       end
895       @repository.save!
896       assert @repository.reload.description.blank?, "desc: #{@repository.description.inspect}"
897     end
898
899     should "not generate events when invalid values are provided" do
900       assert_incremented_by(@repository.events, :size, 0) do
901         @repository.log_changes_with_user(users(:johan)) do
902           @repository.replace_value(:name, "Some illegal value")
903         end
904       end
905     end
906
907     should "not generate events when a value is unchanged" do
908       assert_incremented_by(@repository.events, :size, 0) do
909         @repository.log_changes_with_user(users(:johan)) do
910           @repository.replace_value(:name, @repository.name)
911         end
912       end
913     end
914   end
915
916   context "changing the HEAD" do
917     setup do
918       @grit = Grit::Repo.new(grit_test_repo("dot_git"), :is_bare => true)
919       @repository.stubs(:git).returns(@grit)
920     end
921
922     should "change the head" do
923       assert the_head = @grit.get_head("test/master")
924       @grit.expects(:update_head).with(the_head).returns(true)
925       @repository.head = the_head.name
926     end
927
928     should "not change the head if given a nonexistant ref" do
929       @grit.expects(:update_head).never
930       @repository.head = "non-existant"
931       @repository.head = nil
932       @repository.head = ""
933     end
934
935     should "change the head, even if the current head is nil" do
936       assert the_head = @grit.get_head("test/master")
937       @grit.expects(:head).returns(nil)
938       @grit.expects(:update_head).with(the_head).returns(true)
939       @repository.head = the_head.name
940     end
941   end
942
943   context "Merge request repositories" do
944     setup do
945       @project = Factory.create(:user_project)
946       @main_repo = Factory.create(:repository, :project => @project, :owner => @project.owner, :user => @project.user)
947     end
948
949     should "initially not have a merge request repository" do
950       assert !@main_repo.has_tracking_repository?
951     end
952
953     should "generate a tracking repository" do
954       @merge_repo = @main_repo.create_tracking_repository
955       assert @main_repo.project_repo?
956       assert @merge_repo.tracking_repo?
957       assert_equal @main_repo, @merge_repo.parent
958       assert_equal @main_repo.owner, @merge_repo.owner
959       assert_equal @main_repo.user, @merge_repo.user
960       assert @main_repo.has_tracking_repository?
961       assert_equal @merge_repo, @main_repo.tracking_repository
962     end
963
964     should "not post a repository creation message for merge request repositories" do
965       @merge_repo = @main_repo.build_tracking_repository
966       @merge_repo.expects(:publish).never
967       assert @merge_repo.save
968     end
969   end
970
971   context "Merge requests enabling" do
972     setup do
973       @repository = Repository.new
974     end
975
976     should "by default allow merge requests" do
977       assert @repository.merge_requests_enabled?
978     end
979   end
980
981   context "garbage collection" do
982     setup do
983       @repository = repositories(:johans)
984       @now = Time.now
985       Time.stubs(:now).returns(@now)
986       @repository.stubs(:git).returns(stub())
987       @repository.git.expects(:gc_auto).returns(true)
988     end
989
990     should "have a gc! method that updates last_gc_at" do
991       assert_nil @repository.last_gc_at
992       assert @repository.gc!
993       assert_not_nil @repository.last_gc_at
994       assert_equal @now, @repository.last_gc_at
995     end
996
997     should "set push_count_since_gc to 0 when doing gc" do
998       @repository.push_count_since_gc = 10
999       @repository.gc!
1000       assert_equal 0, @repository.push_count_since_gc
1001     end
1002   end
1003
1004   context "Fresh repositories" do
1005     setup do
1006       Repository.destroy_all
1007       @me = Factory.create(:user, :login => "johnnie")
1008       @project = Factory.create(:project, :user => @me,
1009         :owner => @me)
1010       @repo = Factory.create(:repository, :project => @project,
1011         :owner => @project, :user => @me)
1012       @users = %w(bill steve jack nellie).map { | login |
1013         Factory.create(:user, :login => login)
1014       }
1015       @user_repos = @users.map do |u|
1016         new_repo = Repository.new_by_cloning(@repo)
1017         new_repo.name = "#{u.login}s-clone"
1018         new_repo.user = u
1019         new_repo.owner = u
1020         new_repo.kind = Repository::KIND_USER_REPO
1021         new_repo.last_pushed_at = 1.hour.ago
1022         assert new_repo.save
1023         new_repo
1024       end
1025     end
1026
1027     should "include repositories recently pushed to" do
1028       assert @project.repositories.reload.by_users.fresh(2).include?(@user_repos.first)
1029     end
1030
1031     should "not include repositories last pushed to in the middle ages" do
1032       older_repo = @user_repos.pop
1033       older_repo.last_pushed_at = 500.years.ago
1034       older_repo.save
1035       assert !@project.repositories.by_users.fresh(2).include?(older_repo)
1036     end
1037
1038   end
1039
1040   context "Searching clones" do
1041     setup do
1042       @repo = repositories(:johans)
1043       @clone = repositories(:johans2)
1044     end
1045
1046     should "find clones matching an owning group's name" do
1047       assert @repo.clones.include?(@clone)
1048       assert @repo.search_clones("sproject").include?(@clone)
1049     end
1050
1051     context "by user name" do
1052       setup do
1053         @repo = repositories(:moes)
1054         @clone = repositories(:johans_moe_clone)
1055         users(:johan).update_attribute(:login, "rohan")
1056         @clone.update_attribute(:name, "rohans-clone-of-moes")
1057       end
1058
1059       should "match users with a matching name" do
1060         assert_includes(@repo.search_clones("rohan"), @clone)
1061       end
1062
1063       should "not match user with diverging name" do
1064         assert_not_includes(@repo.search_clones("johan"), @clone)
1065       end
1066     end
1067
1068     context "by group name" do
1069       setup do
1070         @repo = repositories(:johans)
1071         @clone = repositories(:johans2)
1072       end
1073
1074       should "match groups with a matching name" do
1075         assert_includes(@repo.search_clones("thunderbird"), @clone)
1076       end
1077
1078       should "not match groups with diverging name" do
1079         assert_not_includes(@repo.search_clones("A-team"), @clone)
1080       end
1081     end
1082
1083     context "by repo name and description" do
1084       setup do
1085         @repo = repositories(:johans)
1086         @clone = repositories(:johans2)
1087       end
1088
1089       should "match repos with a matching name" do
1090         assert_includes(@repo.search_clones("projectrepos"), @clone)
1091       end
1092
1093       should "not match repos with a different parent" do
1094         assert_not_includes(@repo.search_clones("projectrepos"), repositories(:moes))
1095       end
1096     end
1097
1098   end
1099
1100   context "Sequences" do
1101     setup do
1102       @repository = repositories(:johans)
1103     end
1104
1105     should "calculate the highest existing sequence number" do
1106       assert_equal(@repository.merge_requests.max_by(&:sequence_number).sequence_number,
1107         @repository.calculate_highest_merge_request_sequence_number)
1108     end
1109
1110     should "calculate the number of merge requests" do
1111       assert_equal(3, @repository.merge_request_count)
1112     end
1113
1114     should "be the number of merge requests for a given repo" do
1115       assert_equal 3, @repository.merge_requests.size
1116       assert_equal 4, @repository.next_merge_request_sequence_number
1117     end
1118
1119     # 3 merge requests, update one to have seq 4
1120     should "handle taken sequence numbers gracefully" do
1121       last_merge_request = @repository.merge_requests.last
1122       last_merge_request.update_attribute(:sequence_number, 4)
1123       @repository.expects(:calculate_highest_merge_request_sequence_number).returns(99)
1124       assert_equal(100,
1125         @repository.next_merge_request_sequence_number)
1126     end
1127   end
1128
1129   context "default favoriting" do
1130     should "add the owner as a watcher when creating a clone" do
1131       user = users(:mike)
1132       repo = Repository.new_by_cloning(repositories(:johans), "mike")
1133       repo.user = repo.owner = user
1134       assert_difference("user.favorites.reload.count") do
1135         repo.save!
1136       end
1137       assert repo.reload.watched_by?(user)
1138     end
1139
1140     should "not add as watcher if it is an internal repository" do
1141       repo = new_repos(:user => users(:moe))
1142       repo.kind = Repository::KIND_TRACKING_REPO
1143       assert_no_difference("users(:moe).favorites.count") do
1144         repo.save!
1145       end
1146     end
1147   end
1148
1149   context "Calculation of disk usage" do
1150     setup do
1151       @repository = repositories(:johans)
1152       @bytes = 90129
1153     end
1154
1155     should "save the bytes used" do
1156       @repository.expects(:calculate_disk_usage).returns(@bytes)
1157       @repository.update_disk_usage
1158       assert_equal @bytes, @repository.disk_usage
1159     end
1160   end
1161
1162   context "Pushing" do
1163     setup do
1164       @repository = repositories(:johans)
1165     end
1166
1167     should "update last_pushed_at" do
1168       @repository.last_pushed_at = 1.hour.ago.utc
1169       @repository.stubs(:update_disk_usage)
1170       @repository.register_push
1171       assert @repository.last_pushed_at > 1.hour.ago.utc
1172     end
1173
1174     should "increment the number of pushes" do
1175       @repository.push_count_since_gc = 2
1176       @repository.stubs(:update_disk_usage)
1177       @repository.register_push
1178       assert_equal 3, @repository.push_count_since_gc
1179     end
1180
1181     should "call update_disk_usage when registering a push" do
1182       @repository.expects(:update_disk_usage)
1183       @repository.register_push
1184     end
1185   end
1186
1187   context "Database authorization" do
1188     context "with private repositories enabled" do
1189       setup do
1190         GitoriousConfig["enable_private_repositories"] = true
1191         @repository = repositories(:johans)
1192       end
1193
1194       should "allow anonymous user to view public repository" do
1195         repository = Repository.new(:name => "My repository")
1196         assert can_read?(nil, repository)
1197       end
1198
1199       should "allow owner to view private repository" do
1200         @repository.owner = users(:johan)
1201         @repository.add_member(users(:johan))
1202         assert can_read?(users(:johan), @repository)
1203       end
1204
1205       should "disallow anonymous user to view private repository" do
1206         @repository.add_member(users(:johan))
1207         assert !can_read?(nil, @repository)
1208       end
1209
1210       should "disallow repository member if not also project member" do
1211         @repository.add_member(users(:mike))
1212         @repository.project.make_private
1213
1214         assert !can_read?(nil, @repository)
1215       end
1216
1217       should "allow member to view private repository" do
1218         @repository.owner = users(:johan)
1219         @repository.add_member(users(:mike))
1220         assert can_read?(users(:mike), @repository)
1221       end
1222
1223       should "allow member to view private repository via group membership" do
1224         @repository.owner = users(:johan)
1225         @repository.add_member(groups(:team_thunderbird))
1226         assert can_read?(users(:mike), @repository)
1227       end
1228     end
1229
1230     context "with private repositories disabled" do
1231       setup do
1232         GitoriousConfig["enable_private_repositories"] = false
1233         @repository = repositories(:johans)
1234       end
1235
1236       should "allow anonymous user to view 'private' repository" do
1237         @repository.add_member(users(:johan))
1238         assert can_read?(nil, @repository)
1239       end
1240     end
1241
1242     context "making repositories private" do
1243       setup do
1244         @user = users(:johan)
1245         @repository = repositories(:johans)
1246         GitoriousConfig["enable_private_repositories"] = true
1247       end
1248
1249       should "add owner as member" do
1250         @repository.make_private
1251         assert !can_read?(users(:mike), @repository)
1252       end
1253     end
1254   end
1255
1256   context "repository memberships" do
1257     should "silently ignore duplicates" do
1258       repository = repositories(:johans)
1259       repository.add_member(users(:mike))
1260       repository.add_member(users(:mike))
1261
1262       assert repository.member?(users(:mike))
1263       assert is_member?(users(:mike), repository)
1264       assert_equal 1, repository.content_memberships.count
1265     end
1266   end
1267 end