Merge lp:~cjwatson/launchpad/git-testing into lp:launchpad

Proposed by Colin Watson on 2015-02-16
Status: Merged
Approved by: Colin Watson on 2015-02-20
Approved revision: no longer in the source branch.
Merged at revision: 17356
Proposed branch: lp:~cjwatson/launchpad/git-testing
Merge into: lp:launchpad
Prerequisite: lp:~cjwatson/launchpad/git-sharing
Diff against target: 457 lines (+427/-1)
2 files modified
lib/lp/code/model/tests/test_gitrepository.py (+394/-0)
lib/lp/testing/factory.py (+33/-1)
To merge this branch: bzr merge lp:~cjwatson/launchpad/git-testing
Reviewer Review Type Date Requested Status
William Grant code 2015-02-16 Approve on 2015-02-19
Review via email: mp+249840@code.launchpad.net

Commit message

Add initial Git repository testing support, and a first batch of tests for GitRepository itself.

Description of the change

Add initial Git repository testing support, and a first batch of tests for GitRepository itself.

To post a comment you must log in.
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'lib/lp/code/model/tests/test_gitrepository.py'
2--- lib/lp/code/model/tests/test_gitrepository.py 1970-01-01 00:00:00 +0000
3+++ lib/lp/code/model/tests/test_gitrepository.py 2015-02-20 00:57:45 +0000
4@@ -0,0 +1,394 @@
5+# Copyright 2015 Canonical Ltd. This software is licensed under the
6+# GNU Affero General Public License version 3 (see the file LICENSE).
7+
8+"""Tests for Git repositories."""
9+
10+__metaclass__ = type
11+
12+from datetime import datetime
13+
14+from lazr.lifecycle.event import ObjectModifiedEvent
15+import pytz
16+from zope.component import getUtility
17+from zope.event import notify
18+from zope.security.proxy import removeSecurityProxy
19+
20+from lp.app.enums import (
21+ InformationType,
22+ PRIVATE_INFORMATION_TYPES,
23+ PUBLIC_INFORMATION_TYPES,
24+ )
25+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
26+from lp.code.errors import (
27+ GitRepositoryCreatorNotMemberOfOwnerTeam,
28+ GitRepositoryCreatorNotOwner,
29+ GitTargetError,
30+ )
31+from lp.code.interfaces.gitnamespace import (
32+ IGitNamespacePolicy,
33+ IGitNamespaceSet,
34+ )
35+from lp.code.interfaces.gitrepository import IGitRepository
36+from lp.registry.enums import BranchSharingPolicy
37+from lp.services.database.constants import UTC_NOW
38+from lp.services.webapp.authorization import check_permission
39+from lp.testing import (
40+ admin_logged_in,
41+ celebrity_logged_in,
42+ person_logged_in,
43+ TestCaseWithFactory,
44+ verifyObject,
45+ )
46+from lp.testing.layers import DatabaseFunctionalLayer
47+
48+
49+class TestGitRepository(TestCaseWithFactory):
50+ """Test basic properties about Launchpad database Git repositories."""
51+
52+ layer = DatabaseFunctionalLayer
53+
54+ def test_implements_IGitRepository(self):
55+ repository = self.factory.makeGitRepository()
56+ verifyObject(IGitRepository, repository)
57+
58+ def test_unique_name_project(self):
59+ project = self.factory.makeProduct()
60+ repository = self.factory.makeGitRepository(target=project)
61+ self.assertEqual(
62+ "~%s/%s/+git/%s" % (
63+ repository.owner.name, project.name, repository.name),
64+ repository.unique_name)
65+
66+ def test_unique_name_package(self):
67+ dsp = self.factory.makeDistributionSourcePackage()
68+ repository = self.factory.makeGitRepository(target=dsp)
69+ self.assertEqual(
70+ "~%s/%s/+source/%s/+git/%s" % (
71+ repository.owner.name, dsp.distribution.name,
72+ dsp.sourcepackagename.name, repository.name),
73+ repository.unique_name)
74+
75+ def test_unique_name_personal(self):
76+ owner = self.factory.makePerson()
77+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
78+ self.assertEqual(
79+ "~%s/+git/%s" % (owner.name, repository.name),
80+ repository.unique_name)
81+
82+ def test_target_project(self):
83+ project = self.factory.makeProduct()
84+ repository = self.factory.makeGitRepository(target=project)
85+ self.assertEqual(project, repository.target)
86+
87+ def test_target_package(self):
88+ dsp = self.factory.makeDistributionSourcePackage()
89+ repository = self.factory.makeGitRepository(target=dsp)
90+ self.assertEqual(dsp, repository.target)
91+
92+ def test_target_personal(self):
93+ owner = self.factory.makePerson()
94+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
95+ self.assertEqual(owner, repository.target)
96+
97+
98+class TestGitIdentityMixin(TestCaseWithFactory):
99+ """Test the defaults and identities provided by GitIdentityMixin."""
100+
101+ layer = DatabaseFunctionalLayer
102+
103+ def assertGitIdentity(self, repository, identity_path):
104+ """Assert that the Git identity of 'repository' is 'identity_path'.
105+
106+ Actually, it'll be lp:<identity_path>.
107+ """
108+ self.assertEqual(
109+ identity_path, repository.shortened_path, "shortened path")
110+ self.assertEqual(
111+ "lp:%s" % identity_path, repository.git_identity, "git identity")
112+
113+ def test_git_identity_default(self):
114+ # By default, the Git identity is the repository's unique name.
115+ repository = self.factory.makeGitRepository()
116+ self.assertGitIdentity(repository, repository.unique_name)
117+
118+ def test_identities_no_defaults(self):
119+ # If there are no defaults, the only repository identity is the
120+ # unique name.
121+ repository = self.factory.makeGitRepository()
122+ self.assertEqual(
123+ [(repository.unique_name, repository)],
124+ repository.getRepositoryIdentities())
125+
126+ # XXX cjwatson 2015-02-12: This will need to be expanded once support
127+ # for default repositories is in place.
128+
129+
130+class TestGitRepositoryDateLastModified(TestCaseWithFactory):
131+ """Exercise the situations where date_last_modified is updated."""
132+
133+ layer = DatabaseFunctionalLayer
134+
135+ def test_initial_value(self):
136+ # The initial value of date_last_modified is date_created.
137+ repository = self.factory.makeGitRepository()
138+ self.assertEqual(
139+ repository.date_created, repository.date_last_modified)
140+
141+ def test_modifiedevent_sets_date_last_modified(self):
142+ # When a GitRepository receives an object modified event, the last
143+ # modified date is set to UTC_NOW.
144+ repository = self.factory.makeGitRepository(
145+ date_created=datetime(2015, 02, 04, 17, 42, 0, tzinfo=pytz.UTC))
146+ notify(ObjectModifiedEvent(
147+ removeSecurityProxy(repository), repository,
148+ [IGitRepository["name"]]))
149+ self.assertSqlAttributeEqualsDate(
150+ repository, "date_last_modified", UTC_NOW)
151+
152+ # XXX cjwatson 2015-02-04: This will need to be expanded once Launchpad
153+ # actually notices any interesting kind of repository modifications.
154+
155+
156+class TestCodebrowse(TestCaseWithFactory):
157+ """Tests for Git repository codebrowse support."""
158+
159+ layer = DatabaseFunctionalLayer
160+
161+ def test_simple(self):
162+ # The basic codebrowse URL for a repository is an 'https' URL.
163+ repository = self.factory.makeGitRepository()
164+ self.assertEqual(
165+ "https://git.launchpad.dev/" + repository.unique_name,
166+ repository.getCodebrowseUrl())
167+
168+
169+class TestGitRepositoryNamespace(TestCaseWithFactory):
170+ """Test `IGitRepository.namespace`."""
171+
172+ layer = DatabaseFunctionalLayer
173+
174+ def test_namespace_personal(self):
175+ # The namespace attribute of a personal repository points to the
176+ # namespace that corresponds to ~owner.
177+ owner = self.factory.makePerson()
178+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
179+ namespace = getUtility(IGitNamespaceSet).get(person=owner)
180+ self.assertEqual(namespace, repository.namespace)
181+
182+ def test_namespace_project(self):
183+ # The namespace attribute of a project repository points to the
184+ # namespace that corresponds to ~owner/project.
185+ project = self.factory.makeProduct()
186+ repository = self.factory.makeGitRepository(target=project)
187+ namespace = getUtility(IGitNamespaceSet).get(
188+ person=repository.owner, project=project)
189+ self.assertEqual(namespace, repository.namespace)
190+
191+ def test_namespace_package(self):
192+ # The namespace attribute of a package repository points to the
193+ # namespace that corresponds to
194+ # ~owner/distribution/+source/sourcepackagename.
195+ dsp = self.factory.makeDistributionSourcePackage()
196+ repository = self.factory.makeGitRepository(target=dsp)
197+ namespace = getUtility(IGitNamespaceSet).get(
198+ person=repository.owner, distribution=dsp.distribution,
199+ sourcepackagename=dsp.sourcepackagename)
200+ self.assertEqual(namespace, repository.namespace)
201+
202+
203+class TestGitRepositoryGetAllowedInformationTypes(TestCaseWithFactory):
204+ """Test `IGitRepository.getAllowedInformationTypes`."""
205+
206+ layer = DatabaseFunctionalLayer
207+
208+ def test_normal_user_sees_namespace_types(self):
209+ # An unprivileged user sees the types allowed by the namespace.
210+ repository = self.factory.makeGitRepository()
211+ policy = IGitNamespacePolicy(repository.namespace)
212+ self.assertContentEqual(
213+ policy.getAllowedInformationTypes(),
214+ repository.getAllowedInformationTypes(repository.owner))
215+ self.assertNotIn(
216+ InformationType.PROPRIETARY,
217+ repository.getAllowedInformationTypes(repository.owner))
218+ self.assertNotIn(
219+ InformationType.EMBARGOED,
220+ repository.getAllowedInformationTypes(repository.owner))
221+
222+ def test_admin_sees_namespace_types(self):
223+ # An admin sees all the types, since they occasionally need to
224+ # override the namespace rules. This is hopefully temporary, and
225+ # can go away once the new sharing rules (granting non-commercial
226+ # projects limited use of private repositories) are deployed.
227+ repository = self.factory.makeGitRepository()
228+ admin = self.factory.makeAdministrator()
229+ self.assertContentEqual(
230+ PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES,
231+ repository.getAllowedInformationTypes(admin))
232+ self.assertIn(
233+ InformationType.PROPRIETARY,
234+ repository.getAllowedInformationTypes(admin))
235+
236+
237+class TestGitRepositoryModerate(TestCaseWithFactory):
238+ """Test that project owners and commercial admins can moderate Git
239+ repositories."""
240+
241+ layer = DatabaseFunctionalLayer
242+
243+ def test_moderate_permission(self):
244+ # Test the ModerateGitRepository security checker.
245+ project = self.factory.makeProduct()
246+ repository = self.factory.makeGitRepository(target=project)
247+ with person_logged_in(project.owner):
248+ self.assertTrue(check_permission("launchpad.Moderate", repository))
249+ with celebrity_logged_in("commercial_admin"):
250+ self.assertTrue(check_permission("launchpad.Moderate", repository))
251+ with person_logged_in(self.factory.makePerson()):
252+ self.assertFalse(
253+ check_permission("launchpad.Moderate", repository))
254+
255+ def test_attribute_smoketest(self):
256+ # Users with launchpad.Moderate can set attributes.
257+ project = self.factory.makeProduct()
258+ repository = self.factory.makeGitRepository(target=project)
259+ with person_logged_in(project.owner):
260+ repository.name = u"not-secret"
261+ self.assertEqual(u"not-secret", repository.name)
262+
263+
264+class TestGitRepositorySetOwner(TestCaseWithFactory):
265+ """Test `IGitRepository.setOwner`."""
266+
267+ layer = DatabaseFunctionalLayer
268+
269+ def test_owner_sets_team(self):
270+ # The owner of the repository can set the owner of the repository to
271+ # be a team they are a member of.
272+ repository = self.factory.makeGitRepository()
273+ team = self.factory.makeTeam(owner=repository.owner)
274+ with person_logged_in(repository.owner):
275+ repository.setOwner(team, repository.owner)
276+ self.assertEqual(team, repository.owner)
277+
278+ def test_owner_cannot_set_nonmember_team(self):
279+ # The owner of the repository cannot set the owner to be a team they
280+ # are not a member of.
281+ repository = self.factory.makeGitRepository()
282+ team = self.factory.makeTeam()
283+ with person_logged_in(repository.owner):
284+ self.assertRaises(
285+ GitRepositoryCreatorNotMemberOfOwnerTeam,
286+ repository.setOwner, team, repository.owner)
287+
288+ def test_owner_cannot_set_other_user(self):
289+ # The owner of the repository cannot set the new owner to be another
290+ # person.
291+ repository = self.factory.makeGitRepository()
292+ person = self.factory.makePerson()
293+ with person_logged_in(repository.owner):
294+ self.assertRaises(
295+ GitRepositoryCreatorNotOwner,
296+ repository.setOwner, person, repository.owner)
297+
298+ def test_admin_can_set_any_team_or_person(self):
299+ # A Launchpad admin can set the repository to be owned by any team
300+ # or person.
301+ repository = self.factory.makeGitRepository()
302+ team = self.factory.makeTeam()
303+ # To get a random administrator, choose the admin team owner.
304+ admin = getUtility(ILaunchpadCelebrities).admin.teamowner
305+ with person_logged_in(admin):
306+ repository.setOwner(team, admin)
307+ self.assertEqual(team, repository.owner)
308+ person = self.factory.makePerson()
309+ repository.setOwner(person, admin)
310+ self.assertEqual(person, repository.owner)
311+
312+
313+class TestGitRepositorySetTarget(TestCaseWithFactory):
314+ """Test `IGitRepository.setTarget`."""
315+
316+ layer = DatabaseFunctionalLayer
317+
318+ def test_personal_to_project(self):
319+ # A personal repository can be moved to a project.
320+ owner = self.factory.makePerson()
321+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
322+ project = self.factory.makeProduct()
323+ with person_logged_in(owner):
324+ repository.setTarget(target=project, user=owner)
325+ self.assertEqual(project, repository.target)
326+
327+ def test_personal_to_package(self):
328+ # A personal repository can be moved to a package.
329+ owner = self.factory.makePerson()
330+ repository = self.factory.makeGitRepository(owner=owner, target=owner)
331+ dsp = self.factory.makeDistributionSourcePackage()
332+ with person_logged_in(owner):
333+ repository.setTarget(target=dsp, user=owner)
334+ self.assertEqual(dsp, repository.target)
335+
336+ def test_project_to_other_project(self):
337+ # Move a repository from one project to another.
338+ repository = self.factory.makeGitRepository()
339+ project = self.factory.makeProduct()
340+ with person_logged_in(repository.owner):
341+ repository.setTarget(target=project, user=repository.owner)
342+ self.assertEqual(project, repository.target)
343+
344+ def test_project_to_package(self):
345+ # Move a repository from a project to a package.
346+ repository = self.factory.makeGitRepository()
347+ dsp = self.factory.makeDistributionSourcePackage()
348+ with person_logged_in(repository.owner):
349+ repository.setTarget(target=dsp, user=repository.owner)
350+ self.assertEqual(dsp, repository.target)
351+
352+ def test_project_to_personal(self):
353+ # Move a repository from a project to a personal namespace.
354+ owner = self.factory.makePerson()
355+ repository = self.factory.makeGitRepository(owner=owner)
356+ with person_logged_in(owner):
357+ repository.setTarget(target=owner, user=owner)
358+ self.assertEqual(owner, repository.target)
359+
360+ def test_package_to_other_package(self):
361+ # Move a repository from one package to another.
362+ repository = self.factory.makeGitRepository(
363+ target=self.factory.makeDistributionSourcePackage())
364+ dsp = self.factory.makeDistributionSourcePackage()
365+ with person_logged_in(repository.owner):
366+ repository.setTarget(target=dsp, user=repository.owner)
367+ self.assertEqual(dsp, repository.target)
368+
369+ def test_package_to_project(self):
370+ # Move a repository from a package to a project.
371+ repository = self.factory.makeGitRepository(
372+ target=self.factory.makeDistributionSourcePackage())
373+ project = self.factory.makeProduct()
374+ with person_logged_in(repository.owner):
375+ repository.setTarget(target=project, user=repository.owner)
376+ self.assertEqual(project, repository.target)
377+
378+ def test_package_to_personal(self):
379+ # Move a repository from a package to a personal namespace.
380+ owner = self.factory.makePerson()
381+ repository = self.factory.makeGitRepository(
382+ owner=owner, target=self.factory.makeDistributionSourcePackage())
383+ with person_logged_in(owner):
384+ repository.setTarget(target=owner, user=owner)
385+ self.assertEqual(owner, repository.target)
386+
387+ def test_public_to_proprietary_only_project(self):
388+ # A repository cannot be moved to a target where the sharing policy
389+ # does not allow it.
390+ owner = self.factory.makePerson()
391+ commercial_project = self.factory.makeProduct(
392+ owner=owner, branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
393+ repository = self.factory.makeGitRepository(
394+ owner=owner, information_type=InformationType.PUBLIC)
395+ with admin_logged_in():
396+ self.assertRaises(
397+ GitTargetError, repository.setTarget,
398+ target=commercial_project, user=owner)
399
400=== modified file 'lib/lp/testing/factory.py'
401--- lib/lp/testing/factory.py 2015-01-29 16:28:30 +0000
402+++ lib/lp/testing/factory.py 2015-02-20 00:57:45 +0000
403@@ -2,7 +2,7 @@
404 # NOTE: The first line above must stay first; do not move the copyright
405 # notice to the top. See http://www.python.org/dev/peps/pep-0263/.
406 #
407-# Copyright 2009-2014 Canonical Ltd. This software is licensed under the
408+# Copyright 2009-2015 Canonical Ltd. This software is licensed under the
409 # GNU Affero General Public License version 3 (see the file LICENSE).
410
411 """Testing infrastructure for the Launchpad application.
412@@ -118,6 +118,7 @@
413 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
414 from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet
415 from lp.code.interfaces.codeimportresult import ICodeImportResultSet
416+from lp.code.interfaces.gitnamespace import get_git_namespace
417 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
418 from lp.code.interfaces.revision import IRevisionSet
419 from lp.code.interfaces.sourcepackagerecipe import (
420@@ -1668,6 +1669,37 @@
421 revision_date=revision_date)
422 return branch.createBranchRevision(sequence, revision)
423
424+ def makeGitRepository(self, owner=None, target=_DEFAULT, registrant=None,
425+ name=None, information_type=None,
426+ **optional_repository_args):
427+ """Create and return a new, arbitrary GitRepository.
428+
429+ Any parameters for `IGitNamespace.createRepository` can be specified
430+ to override the default ones.
431+ """
432+ if owner is None:
433+ owner = self.makePerson()
434+ if name is None:
435+ name = self.getUniqueString('gitrepository').decode('utf-8')
436+
437+ if target is _DEFAULT:
438+ target = self.makeProduct()
439+
440+ if registrant is None:
441+ if owner.is_team:
442+ registrant = removeSecurityProxy(owner).teamowner
443+ else:
444+ registrant = owner
445+
446+ namespace = get_git_namespace(target, owner)
447+ repository = namespace.createRepository(
448+ registrant=registrant, name=name, **optional_repository_args)
449+ naked_repository = removeSecurityProxy(repository)
450+ if information_type is not None:
451+ naked_repository.transitionToInformationType(
452+ information_type, registrant, verify_policy=False)
453+ return repository
454+
455 def makeBug(self, target=None, owner=None, bug_watch_url=None,
456 information_type=None, date_closed=None, title=None,
457 date_created=None, description=None, comment=None,