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