Private projects should make its repos private no matter what.
[gitorious:thomas-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     File.expects(:exist?).in_sequence.with(local_hooks).returns(true)
239
240     Repository.create_hooks(path)
241   end
242
243   should "deletes a repository" do
244     Repository.git_backend.expects(:delete!).with(@repository.full_repository_path).returns(true)
245     Repository.delete_git_repository(@repository.real_gitdir)
246   end
247
248   should "knows if has commits" do
249     @repository.stubs(:new_record?).returns(false)
250     @repository.stubs(:ready?).returns(true)
251     git_mock = mock("Grit::Git")
252     @repository.stubs(:git).returns(git_mock)
253     head = mock("head")
254     head.stubs(:name).returns("master")
255     @repository.git.expects(:heads).returns([head])
256     assert @repository.has_commits?, "@repository.has_commits? should be true"
257   end
258
259   should "knows if has commits, unless its a new record" do
260     @repository.stubs(:new_record?).returns(false)
261     assert !@repository.has_commits?, "@repository.has_commits? should be false"
262   end
263
264   should "knows if has commits, unless its not ready" do
265     @repository.stubs(:ready?).returns(false)
266     assert !@repository.has_commits?, "@repository.has_commits? should be false"
267   end
268
269   should " build a new repository by cloning another one" do
270     repos = Repository.new_by_cloning(@repository)
271     assert_equal @repository, repos.parent
272     assert_equal @repository.project, repos.project
273     assert repos.new_record?, "new_record? should be true"
274   end
275
276   should "inherit merge request inclusion from its parent" do
277     @repository.update_attribute(:merge_requests_enabled, true)
278     clone = Repository.new_by_cloning(@repository)
279     assert clone.merge_requests_enabled?
280     @repository.update_attribute(:merge_requests_enabled, false)
281     clone = Repository.new_by_cloning(@repository)
282     assert !clone.merge_requests_enabled?
283   end
284
285   should "suggests a decent name for a cloned repository bsed on username" do
286     repos = Repository.new_by_cloning(@repository, username="johan")
287     assert_equal "johans-foo", repos.name
288     repos = Repository.new_by_cloning(@repository, username=nil)
289     assert_equal nil, repos.name
290   end
291
292   should "has it is name as its to_param value" do
293     @repository.save
294     assert_equal @repository.name, @repository.to_param
295   end
296
297   should "finds a repository by name or raises" do
298     assert_equal repositories(:johans), Repository.find_by_name!(repositories(:johans).name)
299     assert_raises(ActiveRecord::RecordNotFound) do
300       Repository.find_by_name!("asdasdasd")
301     end
302   end
303
304   context "find_by_path" do
305     should "finds a repository by its path" do
306       repo = repositories(:johans)
307       path = File.join(GitoriousConfig["repository_base_path"],
308                         projects(:johans).slug, "#{repo.name}.git")
309       assert_equal repo, Repository.find_by_path(path)
310     end
311
312     should_eventually "finds a repository by its path, regardless of repository kind" do
313       repo = projects(:johans).wiki_repository
314       path = File.join(GitoriousConfig["repository_base_path"].chomp("/"),
315                         projects(:johans).slug, "#{repo.name}.git")
316       assert_equal repo, Repository.find_by_path(path)
317     end
318
319     should "finds a group repository by its path" do
320       repo = repositories(:johans)
321       repo.owner = groups(:team_thunderbird)
322       repo.kind = Repository::KIND_TEAM_REPO
323       repo.save!
324       path = File.join(GitoriousConfig["repository_base_path"], repo.gitdir)
325       assert_equal repo, Repository.find_by_path(path)
326     end
327
328     should "finds a user repository by its path" do
329       repo = repositories(:johans)
330       repo.owner = users(:johan)
331       repo.kind = Repository::KIND_USER_REPO
332       repo.save!
333       path = File.join(GitoriousConfig["repository_base_path"], repo.gitdir)
334       assert_equal repo, Repository.find_by_path(path)
335     end
336
337     should "scope the find by project id" do
338       repo = repositories(:johans)
339       repo.owner = groups(:team_thunderbird)
340       repo.kind = Repository::KIND_TEAM_REPO
341       repo.save!
342       same_name_repo = new_repos(:name => repo.name)
343       same_name_repo
344       same_name_repo.project = projects(:moes)
345       same_name_repo.owner = groups(:team_thunderbird)
346       same_name_repo.kind = Repository::KIND_TEAM_REPO
347       same_name_repo.save!
348       path = File.join(GitoriousConfig["repository_base_path"], same_name_repo.gitdir)
349       assert_equal same_name_repo, Repository.find_by_path(path)
350     end
351   end
352
353   context "#to_xml" do
354     should "xmlilizes git paths as well" do
355       assert @repository.to_xml.include?("<clone-url>")
356       assert @repository.to_xml.include?("<push-url>")
357     end
358
359     should "include a description of the kind" do
360       assert_match(/<kind>mainline<\/kind>/, @repository.to_xml)
361       @repository.kind = Repository::KIND_TEAM_REPO
362       assert_match(/<kind>team<\/kind>/, @repository.to_xml)
363     end
364
365     should "include the project name" do
366       assert_match(/<project>#{@repository.project.slug}<\/project>/, @repository.to_xml)
367     end
368
369     should "include the owner" do
370       assert_match(/<owner kind="User">johan<\/owner>/, @repository.to_xml)
371     end
372   end
373
374   context "can_push?" do
375     should "knows if a user can write to self" do
376       @repository.owner = users(:johan)
377       @repository.save!
378       @repository.reload
379       assert can_push?(users(:johan), @repository)
380       assert !can_push?(users(:mike), @repository)
381
382       @repository.change_owner_to!(groups(:team_thunderbird))
383       @repository.save!
384       assert !can_push?(users(:johan), @repository)
385
386       @repository.owner.add_member(users(:moe), Role.member)
387       @repository.committerships.reload
388       assert can_push?(users(:moe), @repository)
389     end
390
391     context "a wiki repository" do
392       setup do
393         @repository.kind = Repository::KIND_WIKI
394       end
395
396       should "be writable by everyone" do
397         @repository.wiki_permissions = Repository::WIKI_WRITABLE_EVERYONE
398         [:johan, :mike, :moe].each do |login|
399           assert can_push?(users(login), @repository), "not writable_by #{login}"
400         end
401       end
402
403       should "only be writable by project members" do
404         @repository.wiki_permissions = Repository::WIKI_WRITABLE_PROJECT_MEMBERS
405         assert @repository.project.member?(users(:johan))
406         assert can_push?(users(:johan), @repository)
407
408         assert !@repository.project.member?(users(:moe))
409         assert !can_push?(users(:moe), @repository)
410       end
411     end
412   end
413
414   should "publishe a message on create and update" do
415     @repository.save!
416
417     assert_published("/queue/GitoriousRepositoryCreation", {
418                        "command" => "create_git_repository",
419                        "target_id" => @repository.id,
420                        "arguments" => [@repository.real_gitdir]
421                      })
422   end
423
424   should "publishe a message on clone" do
425     @repository.parent = repositories(:johans)
426     @repository.save!
427
428     assert_published("/queue/GitoriousRepositoryCreation", {
429                        "command" => "clone_git_repository",
430                        "target_id" => @repository.id,
431                        "arguments" => [@repository.real_gitdir,
432                                        @repository.parent.real_gitdir]
433                      })
434   end
435
436   should "create a notification on destroy" do
437     @repository.save!
438     @repository.destroy
439
440     assert_published("/queue/GitoriousRepositoryDeletion", {
441                        "command" => "delete_git_repository",
442                        "arguments" => [@repository.real_gitdir]
443                      })
444   end
445
446   should "has one recent commit" do
447     @repository.save!
448     repos_mock = mock("Git mock")
449     commit_mock = stub_everything("Git::Commit mock")
450     repos_mock.expects(:commits).with("master", 1).returns([commit_mock])
451     @repository.stubs(:git).returns(repos_mock)
452     @repository.stubs(:has_commits?).returns(true)
453     heads_stub = mock("head")
454     heads_stub.stubs(:name).returns("master")
455     @repository.stubs(:head_candidate).returns(heads_stub)
456     assert_equal commit_mock, @repository.last_commit
457   end
458
459   should "has one recent commit within a given ref" do
460     @repository.save!
461     repos_mock = mock("Git mock")
462     commit_mock = stub_everything("Git::Commit mock")
463     repos_mock.expects(:commits).with("foo", 1).returns([commit_mock])
464     @repository.stubs(:git).returns(repos_mock)
465     @repository.stubs(:has_commits?).returns(true)
466     @repository.expects(:head_candidate).never
467     assert_equal commit_mock, @repository.last_commit("foo")
468   end
469
470   context "deletion" do
471     setup do
472       @repository.kind = Repository::KIND_PROJECT_REPO
473       @repository.project.repositories << new_repos(:name => "another")
474       @repository.save!
475       @repository.committerships.create!({
476         :committer => users(:moe),
477         :permissions => (Committership::CAN_REVIEW | Committership::CAN_COMMIT)
478       })
479     end
480
481     should "be deletable by admins" do
482       assert admin?(users(:johan), @repository)
483       assert !admin?(users(:moe), @repository)
484
485       assert can_delete?(users(:johan), @repository)
486       assert !can_delete?(users(:moe), @repository)
487     end
488
489     should "always be deletable if admin and non-project repo" do
490       @repository.kind = Repository::KIND_TEAM_REPO
491       assert can_delete?(users(:johan), @repository)
492       assert !can_delete?(users(:moe), @repository)
493     end
494
495     should "also be deletable by users with admin privs" do
496       @repository.kind = Repository::KIND_TEAM_REPO
497       cs = @repository.committerships.create_with_permissions!({
498           :committer => users(:mike)
499         }, Committership::CAN_ADMIN)
500       assert can_delete?(users(:johan), @repository)
501       assert can_delete?(users(:mike), @repository)
502     end
503   end
504
505   should "have a git method that accesses the repository" do
506     # FIXME: meh for stubbing internals, need to refactor that part in Grit
507     File.expects(:exist?).at_least(1).with("#{@repository.full_repository_path}/.git").returns(false)
508     File.expects(:exist?).at_least(1).with(@repository.full_repository_path).returns(true)
509     assert_instance_of Grit::Repo, @repository.git
510     assert_equal @repository.full_repository_path, @repository.git.path
511   end
512
513   should "have a head_candidate" do
514     head_stub = mock("head")
515     head_stub.stubs(:name).returns("master")
516     git = mock("git backend")
517     @repository.stubs(:git).returns(git)
518     git.expects(:head).returns(head_stub)
519     @repository.expects(:has_commits?).returns(true)
520
521     assert_equal head_stub, @repository.head_candidate
522   end
523
524   should "have a head_candidate, unless it does not have commits" do
525     @repository.expects(:has_commits?).returns(false)
526     assert_equal nil, @repository.head_candidate
527   end
528
529   should "has paginated_commits" do
530     git = mock("git")
531     commits = [mock("commit"), mock("commit")]
532     @repository.expects(:git).times(2).returns(git)
533     git.expects(:commit_count).returns(120)
534     git.expects(:commits).with("foo", 30, 30).returns(commits)
535     commits = @repository.paginated_commits("foo", 2, 30)
536     assert_instance_of WillPaginate::Collection, commits
537   end
538
539   should "has a count_commits_from_last_week_by_user of 0 if no commits" do
540     @repository.expects(:has_commits?).returns(false)
541     assert_equal 0, @repository.count_commits_from_last_week_by_user(users(:johan))
542   end
543
544   should "returns a set of users from a list of commits" do
545     commits = []
546     users(:johan, :moe).map do |u|
547       committer = OpenStruct.new(:email => u.email)
548       commits << OpenStruct.new(:committer => committer, :author => committer)
549     end
550     users = Repository.users_by_commits(commits)
551     assert_equal users(:johan, :moe).map(&:email).sort, users.keys.sort
552     assert_equal users(:johan, :moe).map(&:login).sort, users.values.map(&:login).sort
553   end
554
555   should "know if it is a normal project repository" do
556     assert @repository.project_repo?, "@repository.project_repo? should be true"
557   end
558
559   should "know if it is a wiki repo" do
560     @repository.kind = Repository::KIND_WIKI
561     assert @repository.wiki?, "@repository.wiki? should be true"
562   end
563
564   should "has a parent, which is the owner" do
565     @repository.kind = Repository::KIND_TEAM_REPO
566     @repository.owner = groups(:team_thunderbird)
567     assert_equal groups(:team_thunderbird), @repository.breadcrumb_parent
568
569     @repository.kind = Repository::KIND_USER_REPO
570     @repository.owner = users(:johan)
571     assert_equal users(:johan), @repository.breadcrumb_parent
572   end
573
574   should "has a parent, which is the project for mainlines" do
575     @repository.kind = Repository::KIND_PROJECT_REPO
576     @repository.owner = groups(:team_thunderbird)
577     assert_equal projects(:johans), @repository.breadcrumb_parent
578
579     @repository.owner = users(:johan)
580     assert_equal projects(:johans), @repository.breadcrumb_parent
581   end
582
583   should " return its name as title" do
584     assert_equal @repository.title, @repository.name
585   end
586
587   should "return the project title as owner_title if it is a mainline" do
588     @repository.kind = Repository::KIND_PROJECT_REPO
589     assert_equal @repository.project.title, @repository.owner_title
590   end
591
592   should "return the owner title as owner_title if it is not a mainline" do
593     @repository.kind = Repository::KIND_TEAM_REPO
594     assert_equal @repository.owner.title, @repository.owner_title
595   end
596
597   should "returns a list of committers depending on owner type" do
598     repo = repositories(:johans2)
599     repo.committerships.each(&:delete)
600     repo.reload
601     assert !committers(repo).include?(users(:moe))
602
603     repo.committerships.create_with_permissions!({
604         :committer => users(:johan)
605       }, Committership::CAN_COMMIT)
606     assert_equal [users(:johan).login], committers(repo).map(&:login)
607
608     repo.committerships.create_with_permissions!({
609         :committer => groups(:team_thunderbird)
610       }, Committership::CAN_COMMIT)
611     exp_users = groups(:team_thunderbird).members.unshift(users(:johan))
612     assert_equal exp_users.map(&:login), committers(repo).map(&:login)
613
614     groups(:team_thunderbird).add_member(users(:moe), Role.admin)
615     repo.reload
616     assert committers(repo).include?(users(:moe))
617   end
618
619
620   should "know you can request merges from it"  do
621     repo = repositories(:johans2)
622     assert !repo.mainline?
623     assert committer?(users(:mike), repo)
624     assert can_request_merge?(users(:mike), repo)
625
626     repo.kind = Repository::KIND_PROJECT_REPO
627     assert repo.mainline?
628     assert !can_request_merge?(users(:mike), repo), "mainlines should not request merges"
629   end
630
631   should "use a sharded hashed path if enable_repository_dir_sharding is toggled" do
632     GitoriousConfig["enable_repository_dir_sharding"] = true
633     repository = new_repos
634     assert repository.new_record?, "repository.new_record? should be true"
635     repository.save!
636     assert_not_nil repository.hashed_path
637     assert_equal 3, repository.hashed_path.split("/").length
638     assert_match(/[a-z0-9\/]{42}/, repository.hashed_path)
639     GitoriousConfig["enable_repository_dir_sharding"] = false
640   end
641   
642   should "use repo name for path by default, not sharded hashed paths" do
643     GitoriousConfig["enable_repository_dir_sharding"] = false
644     repository = new_repos
645     FileUtils.mkdir_p(repository.full_repository_path, :mode => 0755)
646     repository.save!
647     assert_equal "#{repository.project.slug}/#{repository.name}", repository.hashed_path
648   end
649
650   should "create the initial committership on create for owner" do
651     group_repo = new_repos(:owner => groups(:team_thunderbird))
652     assert_difference("Committership.count") do
653       group_repo.save!
654       assert_equal 1, group_repo.committerships.count
655       assert_equal groups(:team_thunderbird), group_repo.committerships.first.committer
656     end
657
658     user_repo = new_repos(:owner => users(:johan), :name => "foo2")
659     assert_difference("Committership.count") do
660       user_repo.save!
661       assert_equal 1, user_repo.committerships.count
662       cs = user_repo.committerships.first
663       assert_equal users(:johan), cs.committer
664       [:reviewer?, :committer?, :admin?].each do |m|
665         assert cs.send(m), "should have #{m} permissions"
666       end
667     end
668   end
669
670   should "know the full hashed path" do
671     assert_equal @repository.hashed_path, @repository.full_hashed_path
672   end
673
674   should "allow changing ownership from a user to a group" do
675     repo = repositories(:johans)
676     repo.change_owner_to!(groups(:team_thunderbird))
677     assert_equal groups(:team_thunderbird), repo.owner
678     repo.change_owner_to!(users(:johan))
679     assert_equal groups(:team_thunderbird), repo.owner
680   end
681
682   should "not change kind when it is a project repo and changing owner" do
683     repo = repositories(:johans)
684     repo.change_owner_to!(groups(:team_thunderbird))
685     assert_equal groups(:team_thunderbird), repo.owner
686     assert_equal Repository::KIND_PROJECT_REPO, repo.kind
687   end
688
689   should "change kind when changing owner" do
690     repo = repositories(:johans)
691     repo.update_attribute(:kind, Repository::KIND_USER_REPO)
692     assert repo.user_repo?
693     repo.change_owner_to!(groups(:team_thunderbird))
694     assert_equal groups(:team_thunderbird), repo.owner
695     assert repo.team_repo?
696   end
697
698   should "changing ownership adds the new owner to the committerships" do
699     repo = repositories(:johans)
700     old_committer = repo.owner
701     repo.change_owner_to!(groups(:team_thunderbird))
702     assert !committers(repo).include?(old_committer)
703     assert committers(repo).include?(groups(:team_thunderbird).members.first)
704     [:reviewer?, :committer?, :admin?].each do |m|
705       assert repo.committerships.last.send(m), "cannot #{m}"
706     end
707   end
708
709   should "downcases the name before validation" do
710     repo = new_repos(:name => "FOOBAR")
711     repo.save!
712     assert_equal "foobar", repo.reload.name
713   end
714
715   should "have a project_or_owner" do
716     repo = repositories(:johans)
717     assert repo.project_repo?
718     assert_equal repo.project, repo.project_or_owner
719
720     repo.kind = Repository::KIND_TEAM_REPO
721     repo.owner = groups(:team_thunderbird)
722     assert_equal repo.owner, repo.project_or_owner
723
724     repo.kind = Repository::KIND_TEAM_REPO
725     repo.owner = groups(:team_thunderbird)
726     assert_equal repo.owner, repo.project_or_owner
727   end
728
729   context "participant groups" do
730     setup do
731       @repo = repositories(:moes)
732     end
733
734     should "includes the groups' members in #committers" do
735       assert committers(@repo).include?(groups(:team_thunderbird).members.first)
736     end
737
738     should "only include unique users in #committers" do
739       groups(:team_thunderbird).add_member(users(:moe), Role.member)
740       assert_equal 1, committers(@repo).select{|u| u == users(:moe)}.size
741     end
742
743     should "not include committerships without a commit permission bit" do
744       assert_equal 1, @repo.committerships.count
745       cs = @repo.committerships.first
746       cs.build_permissions(:review)
747       cs.save!
748       assert_equal [], committers(@repo).map(&:login)
749     end
750
751     should "return a list of reviewers" do
752       assert !reviewers(@repo).map(&:login).include?(users(:moe).login)
753       @repo.committerships.create_with_permissions!({
754           :committer => users(:moe)
755         }, Committership::CAN_REVIEW)
756       assert reviewers(@repo).map(&:login).include?(users(:moe).login)
757     end
758
759     context "permission helpers" do
760       setup do
761         @cs = @repo.committerships.first
762         @cs.permissions = 0
763         @cs.save!
764       end
765
766       should "know if a user is a committer" do
767         assert !committer?(@cs.committer, @repo)
768         @cs.build_permissions(:commit); @cs.save
769         assert !committer?(:false, @repo)
770         assert !committer?(@cs.committer, @repo)
771       end
772
773       should "know if a user is a reviewer" do
774         assert !reviewer?(@cs.committer, @repo)
775         @cs.build_permissions(:review); @cs.save
776         assert !reviewer?(:false, @repo)
777         assert !reviewer?(@cs.committer, @repo)
778       end
779
780       should "know if a user is a admin" do
781         assert !admin?(@cs.committer, @repo)
782         @cs.build_permissions(:commit, :admin); @cs.save
783         assert !admin?(:false, @repo)
784         assert !admin?(@cs.committer, @repo)
785       end
786     end
787   end
788
789   context "owners as User or Group" do
790     setup do
791       @repo = repositories(:moes)
792     end
793
794     should "return a list of the users who are admin for the repository if owned_by_group?" do
795       @repo.change_owner_to!(groups(:a_team))
796       assert_equal([users(:johan)], @repo.owners)
797     end
798
799     should "not throw an error if transferring ownership to a group if the group is already a committer" do
800       @repo.change_owner_to!(groups(:team_thunderbird))
801       assert_equal([users(:mike)], @repo.owners)
802     end
803
804     should "return the owner if owned by user" do
805       @repo.change_owner_to!(users(:moe))
806       assert_equal([users(:moe)], @repo.owners)
807     end
808   end
809
810   should "create an event on create if it is a project repo" do
811     repo = new_repos
812     repo.kind = Repository::KIND_PROJECT_REPO
813     assert_difference("repo.project.events.count") do
814       repo.save!
815     end
816     assert_equal repo, Event.last.target
817     assert_equal Action::ADD_PROJECT_REPOSITORY, Event.last.action
818   end
819
820   context "find_by_name_in_project" do
821     should "find with a project" do
822       Repository.expects(:find_by_name_and_project_id!).with(repositories(:johans).name, projects(:johans).id).once
823       Repository.find_by_name_in_project!(repositories(:johans).name, projects(:johans))
824     end
825
826     should "find without a project" do
827       Repository.expects(:find_by_name!).with(repositories(:johans).name).once
828       Repository.find_by_name_in_project!(repositories(:johans).name)
829     end
830   end
831
832   context "Signoff of merge requests" do
833     setup do
834       @project = projects(:johans)
835       @mainline_repository = repositories(:johans)
836       @other_repository = repositories(:johans2)
837     end
838
839     should "know that the mainline repository requires signoff of merge requests" do
840       assert @mainline_repository.mainline?
841       assert @mainline_repository.requires_signoff_on_merge_requests?
842     end
843
844     should "not require signoff of merge requests in other repositories" do
845       assert !@other_repository.mainline?
846       assert !@other_repository.requires_signoff_on_merge_requests?
847     end
848   end
849
850   context "Merge request status tags" do
851     setup {@repo = repositories(:johans)}
852
853     should "have a list of used status tags" do
854       @repo.merge_requests.last.update_attribute(:status_tag, "worksforme")
855       assert_equal %w[open worksforme], @repo.merge_request_status_tags
856     end
857   end
858
859   context "Thottling" do
860     setup{ Repository.destroy_all }
861
862     should "throttle on create" do
863       assert_nothing_raised do
864         5.times{|i| new_repos(:name => "wifebeater#{i}").save! }
865       end
866
867       assert_no_difference("Repository.count") do
868         assert_raises(RecordThrottling::LimitReachedError) do
869           new_repos(:name => "wtf-are-you-doing-bro").save!
870         end
871       end
872     end
873   end
874
875   context "Logging updates" do
876     setup {@repository = repositories(:johans)}
877
878     should "generate events for each value that is changed" do
879       assert_incremented_by(@repository.events, :size, 1) do
880         @repository.log_changes_with_user(users(:johan)) do
881           @repository.replace_value(:name, "new_name")
882         end
883         assert @repository.save
884       end
885       assert_equal "new_name", @repository.reload.name
886     end
887
888     should "not generate events when blank values are provided" do
889       assert_incremented_by(@repository.events, :size, 0) do
890         @repository.log_changes_with_user(users(:johan)) do
891           @repository.replace_value(:name, "")
892         end
893       end
894     end
895
896     should "allow blank updates if we say it is ok" do
897       @repository.update_attribute(:description, "asdf")
898       @repository.log_changes_with_user(users(:johan)) do
899         @repository.replace_value(:description, "", true)
900       end
901       @repository.save!
902       assert @repository.reload.description.blank?, "desc: #{@repository.description.inspect}"
903     end
904
905     should "not generate events when invalid values are provided" do
906       assert_incremented_by(@repository.events, :size, 0) do
907         @repository.log_changes_with_user(users(:johan)) do
908           @repository.replace_value(:name, "Some illegal value")
909         end
910       end
911     end
912
913     should "not generate events when a value is unchanged" do
914       assert_incremented_by(@repository.events, :size, 0) do
915         @repository.log_changes_with_user(users(:johan)) do
916           @repository.replace_value(:name, @repository.name)
917         end
918       end
919     end
920   end
921
922   context "changing the HEAD" do
923     setup do
924       @grit = Grit::Repo.new(grit_test_repo("dot_git"), :is_bare => true)
925       @repository.stubs(:git).returns(@grit)
926     end
927
928     should "change the head" do
929       assert the_head = @grit.get_head("test/master")
930       @grit.expects(:update_head).with(the_head).returns(true)
931       @repository.head = the_head.name
932     end
933
934     should "not change the head if given a nonexistant ref" do
935       @grit.expects(:update_head).never
936       @repository.head = "non-existant"
937       @repository.head = nil
938       @repository.head = ""
939     end
940
941     should "change the head, even if the current head is nil" do
942       assert the_head = @grit.get_head("test/master")
943       @grit.expects(:head).returns(nil)
944       @grit.expects(:update_head).with(the_head).returns(true)
945       @repository.head = the_head.name
946     end
947   end
948
949   context "Merge request repositories" do
950     setup do
951       @project = Factory.create(:user_project)
952       @main_repo = Factory.create(:repository, :project => @project, :owner => @project.owner, :user => @project.user)
953     end
954
955     should "initially not have a merge request repository" do
956       assert !@main_repo.has_tracking_repository?
957     end
958
959     should "generate a tracking repository" do
960       @merge_repo = @main_repo.create_tracking_repository
961       assert @main_repo.project_repo?
962       assert @merge_repo.tracking_repo?
963       assert_equal @main_repo, @merge_repo.parent
964       assert_equal @main_repo.owner, @merge_repo.owner
965       assert_equal @main_repo.user, @merge_repo.user
966       assert @main_repo.has_tracking_repository?
967       assert_equal @merge_repo, @main_repo.tracking_repository
968     end
969
970     should "not post a repository creation message for merge request repositories" do
971       @merge_repo = @main_repo.build_tracking_repository
972       @merge_repo.expects(:publish).never
973       assert @merge_repo.save
974     end
975   end
976
977   context "Merge requests enabling" do
978     setup do
979       @repository = Repository.new
980     end
981
982     should "by default allow merge requests" do
983       assert @repository.merge_requests_enabled?
984     end
985   end
986
987   context "garbage collection" do
988     setup do
989       @repository = repositories(:johans)
990       @now = Time.now
991       Time.stubs(:now).returns(@now)
992       @repository.stubs(:git).returns(stub())
993       @repository.git.expects(:gc_auto).returns(true)
994     end
995
996     should "have a gc! method that updates last_gc_at" do
997       assert_nil @repository.last_gc_at
998       assert @repository.gc!
999       assert_not_nil @repository.last_gc_at
1000       assert_equal @now, @repository.last_gc_at
1001     end
1002
1003     should "set push_count_since_gc to 0 when doing gc" do
1004       @repository.push_count_since_gc = 10
1005       @repository.gc!
1006       assert_equal 0, @repository.push_count_since_gc
1007     end
1008   end
1009
1010   context "Fresh repositories" do
1011     setup do
1012       Repository.destroy_all
1013       @me = Factory.create(:user, :login => "johnnie")
1014       @project = Factory.create(:project, :user => @me,
1015         :owner => @me)
1016       @repo = Factory.create(:repository, :project => @project,
1017         :owner => @project, :user => @me)
1018       @users = %w(bill steve jack nellie).map { | login |
1019         Factory.create(:user, :login => login)
1020       }
1021       @user_repos = @users.map do |u|
1022         new_repo = Repository.new_by_cloning(@repo)
1023         new_repo.name = "#{u.login}s-clone"
1024         new_repo.user = u
1025         new_repo.owner = u
1026         new_repo.kind = Repository::KIND_USER_REPO
1027         new_repo.last_pushed_at = 1.hour.ago
1028         assert new_repo.save
1029         new_repo
1030       end
1031     end
1032
1033     should "include repositories recently pushed to" do
1034       assert @project.repositories.reload.by_users.fresh(2).include?(@user_repos.first)
1035     end
1036
1037     should "not include repositories last pushed to in the middle ages" do
1038       older_repo = @user_repos.pop
1039       older_repo.last_pushed_at = 500.years.ago
1040       older_repo.save
1041       assert !@project.repositories.by_users.fresh(2).include?(older_repo)
1042     end
1043
1044   end
1045
1046   context "Searching clones" do
1047     setup do
1048       @repo = repositories(:johans)
1049       @clone = repositories(:johans2)
1050     end
1051
1052     should "find clones matching an owning group's name" do
1053       assert @repo.clones.include?(@clone)
1054       assert @repo.search_clones("sproject").include?(@clone)
1055     end
1056
1057     context "by user name" do
1058       setup do
1059         @repo = repositories(:moes)
1060         @clone = repositories(:johans_moe_clone)
1061         users(:johan).update_attribute(:login, "rohan")
1062         @clone.update_attribute(:name, "rohans-clone-of-moes")
1063       end
1064
1065       should "match users with a matching name" do
1066         assert_includes(@repo.search_clones("rohan"), @clone)
1067       end
1068
1069       should "not match user with diverging name" do
1070         assert_not_includes(@repo.search_clones("johan"), @clone)
1071       end
1072     end
1073
1074     context "by group name" do
1075       setup do
1076         @repo = repositories(:johans)
1077         @clone = repositories(:johans2)
1078       end
1079
1080       should "match groups with a matching name" do
1081         assert_includes(@repo.search_clones("thunderbird"), @clone)
1082       end
1083
1084       should "not match groups with diverging name" do
1085         assert_not_includes(@repo.search_clones("A-team"), @clone)
1086       end
1087     end
1088
1089     context "by repo name and description" do
1090       setup do
1091         @repo = repositories(:johans)
1092         @clone = repositories(:johans2)
1093       end
1094
1095       should "match repos with a matching name" do
1096         assert_includes(@repo.search_clones("projectrepos"), @clone)
1097       end
1098
1099       should "not match repos with a different parent" do
1100         assert_not_includes(@repo.search_clones("projectrepos"), repositories(:moes))
1101       end
1102     end
1103
1104   end
1105
1106   context "Sequences" do
1107     setup do
1108       @repository = repositories(:johans)
1109     end
1110
1111     should "calculate the highest existing sequence number" do
1112       assert_equal(@repository.merge_requests.max_by(&:sequence_number).sequence_number,
1113         @repository.calculate_highest_merge_request_sequence_number)
1114     end
1115
1116     should "calculate the number of merge requests" do
1117       assert_equal(3, @repository.merge_request_count)
1118     end
1119
1120     should "be the number of merge requests for a given repo" do
1121       assert_equal 3, @repository.merge_requests.size
1122       assert_equal 4, @repository.next_merge_request_sequence_number
1123     end
1124
1125     # 3 merge requests, update one to have seq 4
1126     should "handle taken sequence numbers gracefully" do
1127       last_merge_request = @repository.merge_requests.last
1128       last_merge_request.update_attribute(:sequence_number, 4)
1129       @repository.expects(:calculate_highest_merge_request_sequence_number).returns(99)
1130       assert_equal(100,
1131         @repository.next_merge_request_sequence_number)
1132     end
1133   end
1134
1135   context "default favoriting" do
1136     should "add the owner as a watcher when creating a clone" do
1137       user = users(:mike)
1138       repo = Repository.new_by_cloning(repositories(:johans), "mike")
1139       repo.user = repo.owner = user
1140       assert_difference("user.favorites.reload.count") do
1141         repo.save!
1142       end
1143       assert repo.reload.watched_by?(user)
1144     end
1145
1146     should "not add as watcher if it is an internal repository" do
1147       repo = new_repos(:user => users(:moe))
1148       repo.kind = Repository::KIND_TRACKING_REPO
1149       assert_no_difference("users(:moe).favorites.count") do
1150         repo.save!
1151       end
1152     end
1153   end
1154
1155   context "Calculation of disk usage" do
1156     setup do
1157       @repository = repositories(:johans)
1158       @bytes = 90129
1159     end
1160
1161     should "save the bytes used" do
1162       @repository.expects(:calculate_disk_usage).returns(@bytes)
1163       @repository.update_disk_usage
1164       assert_equal @bytes, @repository.disk_usage
1165     end
1166   end
1167
1168   context "Pushing" do
1169     setup do
1170       @repository = repositories(:johans)
1171     end
1172
1173     should "update last_pushed_at" do
1174       @repository.last_pushed_at = 1.hour.ago.utc
1175       @repository.stubs(:update_disk_usage)
1176       @repository.register_push
1177       assert @repository.last_pushed_at > 1.hour.ago.utc
1178     end
1179
1180     should "increment the number of pushes" do
1181       @repository.push_count_since_gc = 2
1182       @repository.stubs(:update_disk_usage)
1183       @repository.register_push
1184       assert_equal 3, @repository.push_count_since_gc
1185     end
1186
1187     should "call update_disk_usage when registering a push" do
1188       @repository.expects(:update_disk_usage)
1189       @repository.register_push
1190     end
1191   end
1192
1193   context "Database authorization" do
1194     context "with private repositories enabled" do
1195       setup do
1196         GitoriousConfig["enable_private_repositories"] = true
1197         @repository = repositories(:johans)
1198       end
1199
1200       should "mark repository as private" do
1201         @repository.make_private
1202         assert @repository.private?
1203       end
1204
1205       should "mark repository as private if project is private" do
1206         @repository.project.make_private
1207         assert @repository.private?
1208       end
1209  
1210       should "allow anonymous user to view public repository" do
1211         repository = Repository.new(:name => "My repository")
1212         assert can_read?(nil, repository)
1213       end
1214
1215       should "allow owner to view private repository" do
1216         @repository.owner = users(:johan)
1217         @repository.add_member(users(:johan))
1218         assert can_read?(users(:johan), @repository)
1219       end
1220
1221       should "disallow anonymous user to view private repository" do
1222         @repository.add_member(users(:johan))
1223         assert !can_read?(nil, @repository)
1224       end
1225
1226       should "disallow repository member if not also project member" do
1227         @repository.add_member(users(:mike))
1228         @repository.project.make_private
1229
1230         assert !can_read?(nil, @repository)
1231       end
1232
1233       should "allow member to view private repository" do
1234         @repository.owner = users(:johan)
1235         @repository.add_member(users(:mike))
1236         assert can_read?(users(:mike), @repository)
1237       end
1238
1239       should "allow member to view private repository via group membership" do
1240         @repository.owner = users(:johan)
1241         @repository.add_member(groups(:team_thunderbird))
1242         assert can_read?(users(:mike), @repository)
1243       end
1244
1245       should "display the git URL for public repositories" do
1246         assert @repository.git_cloning?
1247       end
1248
1249       should "not display the git URL for protected repositories" do
1250         @repository.owner = users(:johan)
1251         @repository.add_member(users(:mike))
1252         assert !@repository.git_cloning?
1253       end
1254     end
1255
1256     context "with private repositories disabled" do
1257       setup do
1258         GitoriousConfig["enable_private_repositories"] = false
1259         @repository = repositories(:johans)
1260       end
1261
1262       should "allow anonymous user to view 'private' repository" do
1263         @repository.add_member(users(:johan))
1264         assert can_read?(nil, @repository)
1265       end
1266     end
1267
1268     context "making repositories private" do
1269       setup do
1270         @user = users(:johan)
1271         @repository = repositories(:johans)
1272         GitoriousConfig["enable_private_repositories"] = true
1273       end
1274
1275       should "add owner as member" do
1276         @repository.make_private
1277         assert !can_read?(users(:mike), @repository)
1278       end
1279     end
1280   end
1281
1282   context "repository memberships" do
1283     should "silently ignore duplicates" do
1284       repository = repositories(:johans)
1285       repository.add_member(users(:mike))
1286       repository.add_member(users(:mike))
1287
1288       assert repository.member?(users(:mike))
1289       assert is_member?(users(:mike), repository)
1290       assert_equal 1, repository.content_memberships.count
1291     end
1292   end
1293
1294   context "Reserved repository names" do
1295
1296     should "not allow users as repository name" do
1297       repo = otherwise_valid_repository(:name => "users")
1298       assert repo.errors.on :name
1299     end
1300
1301     should "not allow 'groups' as repository name" do
1302       repo = otherwise_valid_repository(:name => "groups")
1303       assert repo.errors.on :name
1304     end
1305   end
1306
1307   def otherwise_valid_repository(options)
1308     result = new_repos(options)
1309     result.valid?
1310     result
1311   end
1312 end