Merge lp:~cjwatson/launchpad/git-testing into lp:launchpad
- git-testing
- Merge into devel
Proposed by
Colin Watson
Status: | Merged |
---|---|
Approved by: | Colin Watson |
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email:
|
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.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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, |