← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-testing into lp:launchpad with lp:~cjwatson/launchpad/git-sharing as a prerequisite.

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

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #1032731 in Launchpad itself: "Support for Launchpad-hosted Git repositories"
  https://bugs.launchpad.net/launchpad/+bug/1032731

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-testing/+merge/249840

Add initial Git repository testing support, and a first batch of tests for GitRepository itself.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-testing into lp:launchpad.
=== added file 'lib/lp/code/model/tests/test_gitrepository.py'
--- lib/lp/code/model/tests/test_gitrepository.py	1970-01-01 00:00:00 +0000
+++ lib/lp/code/model/tests/test_gitrepository.py	2015-02-16 15:41:09 +0000
@@ -0,0 +1,389 @@
+# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""Tests for Git repositories."""
+
+__metaclass__ = type
+
+from datetime import datetime
+
+from lazr.lifecycle.event import ObjectModifiedEvent
+import pytz
+from zope.component import getUtility
+from zope.event import notify
+from zope.security.proxy import removeSecurityProxy
+
+from lp.app.enums import (
+    InformationType,
+    PRIVATE_INFORMATION_TYPES,
+    PUBLIC_INFORMATION_TYPES,
+    )
+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.code.errors import (
+    GitRepositoryCreatorNotMemberOfOwnerTeam,
+    GitRepositoryCreatorNotOwner,
+    GitTargetError,
+    )
+from lp.code.interfaces.gitnamespace import (
+    IGitNamespacePolicy,
+    IGitNamespaceSet,
+    )
+from lp.code.interfaces.gitrepository import IGitRepository
+from lp.registry.enums import BranchSharingPolicy
+from lp.services.database.constants import UTC_NOW
+from lp.services.webapp.authorization import check_permission
+from lp.testing import (
+    admin_logged_in,
+    celebrity_logged_in,
+    person_logged_in,
+    TestCaseWithFactory,
+    verifyObject,
+    )
+from lp.testing.layers import DatabaseFunctionalLayer
+
+
+class TestGitRepository(TestCaseWithFactory):
+    """Test basic properties about Launchpad database Git repositories."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_implements_IGitRepository(self):
+        repository = self.factory.makeGitRepository()
+        verifyObject(IGitRepository, repository)
+
+    def test_unique_name_project(self):
+        project = self.factory.makeProduct()
+        repository = self.factory.makeProjectGitRepository(project=project)
+        self.assertEqual(
+            "~%s/%s/+git/%s" % (
+                repository.owner.name, project.name, repository.name),
+            repository.unique_name)
+
+    def test_unique_name_package(self):
+        dsp = self.factory.makeDistributionSourcePackage()
+        repository = self.factory.makePackageGitRepository(
+            distro_source_package=dsp)
+        self.assertEqual(
+            "~%s/%s/+source/%s/+git/%s" % (
+                repository.owner.name, dsp.distribution.name,
+                dsp.sourcepackagename.name, repository.name),
+            repository.unique_name)
+
+    def test_unique_name_personal(self):
+        repository = self.factory.makePersonalGitRepository()
+        self.assertEqual(
+            "~%s/+git/%s" % (repository.owner.name, repository.name),
+            repository.unique_name)
+
+    def test_target_project(self):
+        project = self.factory.makeProduct()
+        repository = self.factory.makeProjectGitRepository(project=project)
+        self.assertEqual(project, repository.target)
+
+    def test_target_package(self):
+        dsp = self.factory.makeDistributionSourcePackage()
+        repository = self.factory.makePackageGitRepository(
+            distro_source_package=dsp)
+        self.assertEqual(dsp, repository.target)
+
+    def test_target_personal(self):
+        repository = self.factory.makePersonalGitRepository()
+        self.assertEqual(repository.owner, repository.target)
+
+
+class TestGitIdentityMixin(TestCaseWithFactory):
+    """Test the defaults and identities provided by GitIdentityMixin."""
+
+    layer = DatabaseFunctionalLayer
+
+    def assertGitIdentity(self, repository, identity_path):
+        """Assert that the Git identity of 'repository' is 'identity_path'.
+
+        Actually, it'll be lp:<identity_path>.
+        """
+        self.assertEqual(
+            identity_path, repository.shortened_path, "shortened path")
+        self.assertEqual(
+            "lp:%s" % identity_path, repository.git_identity, "git identity")
+
+    def test_git_identity_default(self):
+        # By default, the Git identity is the repository's unique name.
+        repository = self.factory.makeAnyGitRepository()
+        self.assertGitIdentity(repository, repository.unique_name)
+
+    def test_identities_no_defaults(self):
+        # If there are no defaults, the only repository identity is the
+        # unique name.
+        repository = self.factory.makeAnyGitRepository()
+        self.assertEqual(
+            [(repository.unique_name, repository)],
+            repository.getRepositoryIdentities())
+
+    # XXX cjwatson 2015-02-12: This will need to be expanded once support
+    # for default repositories is in place.
+
+
+class TestGitRepositoryDateLastModified(TestCaseWithFactory):
+    """Exercise the situations where date_last_modified is updated."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_initial_value(self):
+        # The initial value of date_last_modified is date_created.
+        repository = self.factory.makeAnyGitRepository()
+        self.assertEqual(
+            repository.date_created, repository.date_last_modified)
+
+    def test_modifiedevent_sets_date_last_modified(self):
+        # When a GitRepository receives an object modified event, the last
+        # modified date is set to UTC_NOW.
+        repository = self.factory.makeAnyGitRepository(
+            date_created=datetime(2015, 02, 04, 17, 42, 0, tzinfo=pytz.UTC))
+        notify(ObjectModifiedEvent(
+            removeSecurityProxy(repository), repository,
+            [IGitRepository["name"]]))
+        self.assertSqlAttributeEqualsDate(
+            repository, "date_last_modified", UTC_NOW)
+
+    # XXX cjwatson 2015-02-04: This will need to be expanded once Launchpad
+    # actually notices any interesting kind of repository modifications.
+
+
+class TestCodebrowse(TestCaseWithFactory):
+    """Tests for Git repository codebrowse support."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_simple(self):
+        # The basic codebrowse URL for a repository is an 'https' URL.
+        repository = self.factory.makeAnyGitRepository()
+        self.assertEqual(
+            "https://git.launchpad.dev/"; + repository.unique_name,
+            repository.getCodebrowseUrl())
+
+
+class TestGitRepositoryNamespace(TestCaseWithFactory):
+    """Test `IGitRepository.namespace`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_namespace_personal(self):
+        # The namespace attribute of a personal repository points to the
+        # namespace that corresponds to ~owner.
+        repository = self.factory.makePersonalGitRepository()
+        namespace = getUtility(IGitNamespaceSet).get(person=repository.owner)
+        self.assertEqual(namespace, repository.namespace)
+
+    def test_namespace_project(self):
+        # The namespace attribute of a project repository points to the
+        # namespace that corresponds to ~owner/project.
+        project = self.factory.makeProduct()
+        repository = self.factory.makeProjectGitRepository(project=project)
+        namespace = getUtility(IGitNamespaceSet).get(
+            person=repository.owner, project=project)
+        self.assertEqual(namespace, repository.namespace)
+
+    def test_namespace_package(self):
+        # The namespace attribute of a package repository points to the
+        # namespace that corresponds to
+        # ~owner/distribution/+source/sourcepackagename.
+        dsp = self.factory.makeDistributionSourcePackage()
+        repository = self.factory.makePackageGitRepository(
+            distro_source_package=dsp)
+        namespace = getUtility(IGitNamespaceSet).get(
+            person=repository.owner, distribution=dsp.distribution,
+            sourcepackagename=dsp.sourcepackagename)
+        self.assertEqual(namespace, repository.namespace)
+
+
+class TestGitRepositoryGetAllowedInformationTypes(TestCaseWithFactory):
+    """Test `IGitRepository.getAllowedInformationTypes`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_normal_user_sees_namespace_types(self):
+        # An unprivileged user sees the types allowed by the namespace.
+        repository = self.factory.makeGitRepository()
+        policy = IGitNamespacePolicy(repository.namespace)
+        self.assertContentEqual(
+            policy.getAllowedInformationTypes(),
+            repository.getAllowedInformationTypes(repository.owner))
+        self.assertNotIn(
+            InformationType.PROPRIETARY,
+            repository.getAllowedInformationTypes(repository.owner))
+        self.assertNotIn(
+            InformationType.EMBARGOED,
+            repository.getAllowedInformationTypes(repository.owner))
+
+    def test_admin_sees_namespace_types(self):
+        # An admin sees all the types, since they occasionally need to
+        # override the namespace rules.  This is hopefully temporary, and
+        # can go away once the new sharing rules (granting non-commercial
+        # projects limited use of private repositories) are deployed.
+        repository = self.factory.makeGitRepository()
+        admin = self.factory.makeAdministrator()
+        self.assertContentEqual(
+            PUBLIC_INFORMATION_TYPES + PRIVATE_INFORMATION_TYPES,
+            repository.getAllowedInformationTypes(admin))
+        self.assertIn(
+            InformationType.PROPRIETARY,
+            repository.getAllowedInformationTypes(admin))
+
+
+class TestGitRepositoryModerate(TestCaseWithFactory):
+    """Test that project owners and commercial admins can moderate Git
+    repositories."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_moderate_permission(self):
+        # Test the ModerateGitRepository security checker.
+        project = self.factory.makeProduct()
+        repository = self.factory.makeProjectGitRepository(project=project)
+        with person_logged_in(project.owner):
+            self.assertTrue(check_permission("launchpad.Moderate", repository))
+        with celebrity_logged_in("commercial_admin"):
+            self.assertTrue(check_permission("launchpad.Moderate", repository))
+        with person_logged_in(self.factory.makePerson()):
+            self.assertFalse(
+                check_permission("launchpad.Moderate", repository))
+
+    def test_attribute_smoketest(self):
+        # Users with launchpad.Moderate can set attributes.
+        project = self.factory.makeProduct()
+        repository = self.factory.makeProjectGitRepository(project=project)
+        with person_logged_in(project.owner):
+            repository.name = u"not-secret"
+        self.assertEqual(u"not-secret", repository.name)
+
+
+class TestGitRepositorySetOwner(TestCaseWithFactory):
+    """Test `IGitRepository.setOwner`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_owner_sets_team(self):
+        # The owner of the repository can set the owner of the repository to
+        # be a team they are a member of.
+        repository = self.factory.makeAnyGitRepository()
+        team = self.factory.makeTeam(owner=repository.owner)
+        with person_logged_in(repository.owner):
+            repository.setOwner(team, repository.owner)
+        self.assertEqual(team, repository.owner)
+
+    def test_owner_cannot_set_nonmember_team(self):
+        # The owner of the repository cannot set the owner to be a team they
+        # are not a member of.
+        repository = self.factory.makeAnyGitRepository()
+        team = self.factory.makeTeam()
+        with person_logged_in(repository.owner):
+            self.assertRaises(
+                GitRepositoryCreatorNotMemberOfOwnerTeam,
+                repository.setOwner, team, repository.owner)
+
+    def test_owner_cannot_set_other_user(self):
+        # The owner of the repository cannot set the new owner to be another
+        # person.
+        repository = self.factory.makeAnyGitRepository()
+        person = self.factory.makePerson()
+        with person_logged_in(repository.owner):
+            self.assertRaises(
+                GitRepositoryCreatorNotOwner,
+                repository.setOwner, person, repository.owner)
+
+    def test_admin_can_set_any_team_or_person(self):
+        # A Launchpad admin can set the repository to be owned by any team
+        # or person.
+        repository = self.factory.makeAnyGitRepository()
+        team = self.factory.makeTeam()
+        # To get a random administrator, choose the admin team owner.
+        admin = getUtility(ILaunchpadCelebrities).admin.teamowner
+        with person_logged_in(admin):
+            repository.setOwner(team, admin)
+            self.assertEqual(team, repository.owner)
+            person = self.factory.makePerson()
+            repository.setOwner(person, admin)
+            self.assertEqual(person, repository.owner)
+
+
+class TestGitRepositorySetTarget(TestCaseWithFactory):
+    """Test `IGitRepository.setTarget`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_personal_to_project(self):
+        # A personal repository can be moved to a project.
+        repository = self.factory.makePersonalGitRepository()
+        project = self.factory.makeProduct()
+        with person_logged_in(repository.owner):
+            repository.setTarget(target=project, user=repository.owner)
+        self.assertEqual(project, repository.target)
+
+    def test_personal_to_package(self):
+        # A personal repository can be moved to a package.
+        repository = self.factory.makePersonalGitRepository()
+        dsp = self.factory.makeDistributionSourcePackage()
+        with person_logged_in(repository.owner):
+            repository.setTarget(target=dsp, user=repository.owner)
+        self.assertEqual(dsp, repository.target)
+
+    def test_project_to_other_project(self):
+        # Move a repository from one project to another.
+        repository = self.factory.makeProjectGitRepository()
+        project = self.factory.makeProduct()
+        with person_logged_in(repository.owner):
+            repository.setTarget(target=project, user=repository.owner)
+        self.assertEqual(project, repository.target)
+
+    def test_project_to_package(self):
+        # Move a repository from a project to a package.
+        repository = self.factory.makeProjectGitRepository()
+        dsp = self.factory.makeDistributionSourcePackage()
+        with person_logged_in(repository.owner):
+            repository.setTarget(target=dsp, user=repository.owner)
+        self.assertEqual(dsp, repository.target)
+
+    def test_project_to_personal(self):
+        # Move a repository from a project to a personal namespace.
+        owner = self.factory.makePerson()
+        repository = self.factory.makeProjectGitRepository(owner=owner)
+        with person_logged_in(owner):
+            repository.setTarget(target=owner, user=owner)
+        self.assertEqual(owner, repository.target)
+
+    def test_package_to_other_package(self):
+        # Move a repository from one package to another.
+        repository = self.factory.makePackageGitRepository()
+        dsp = self.factory.makeDistributionSourcePackage()
+        with person_logged_in(repository.owner):
+            repository.setTarget(target=dsp, user=repository.owner)
+        self.assertEqual(dsp, repository.target)
+
+    def test_package_to_project(self):
+        # Move a repository from a package to a project.
+        repository = self.factory.makePackageGitRepository()
+        project = self.factory.makeProduct()
+        with person_logged_in(repository.owner):
+            repository.setTarget(target=project, user=repository.owner)
+        self.assertEqual(project, repository.target)
+
+    def test_package_to_personal(self):
+        # Move a repository from a package to a personal namespace.
+        owner = self.factory.makePerson()
+        repository = self.factory.makePackageGitRepository(owner=owner)
+        with person_logged_in(owner):
+            repository.setTarget(target=owner, user=owner)
+        self.assertEqual(owner, repository.target)
+
+    def test_public_to_proprietary_only_project(self):
+        # A repository cannot be moved to a target where the sharing policy does
+        # not allow it.
+        owner = self.factory.makePerson()
+        commercial_project = self.factory.makeProduct(
+            owner=owner, branch_sharing_policy=BranchSharingPolicy.PROPRIETARY)
+        repository = self.factory.makeGitRepository(
+            owner=owner, information_type=InformationType.PUBLIC)
+        with admin_logged_in():
+            self.assertRaises(
+                GitTargetError, repository.setTarget,
+                target=commercial_project, user=owner)

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2015-01-29 16:28:30 +0000
+++ lib/lp/testing/factory.py	2015-02-16 15:41:09 +0000
@@ -118,6 +118,7 @@
 from lp.code.interfaces.codeimportevent import ICodeImportEventSet
 from lp.code.interfaces.codeimportmachine import ICodeImportMachineSet
 from lp.code.interfaces.codeimportresult import ICodeImportResultSet
+from lp.code.interfaces.gitnamespace import get_git_namespace
 from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
 from lp.code.interfaces.revision import IRevisionSet
 from lp.code.interfaces.sourcepackagerecipe import (
@@ -1668,6 +1669,93 @@
             revision_date=revision_date)
         return branch.createBranchRevision(sequence, revision)
 
+    def makeGitRepository(self, owner=None, project=_DEFAULT,
+                          distro_source_package=None, registrant=None,
+                          name=None, information_type=None,
+                          **optional_repository_args):
+        """Create and return a new, arbitrary GitRepository.
+
+        Any parameters for `IGitNamespace.createRepository` can be specified
+        to override the default ones.
+        """
+        if owner is None:
+            owner = self.makePerson()
+        if name is None:
+            name = self.getUniqueString('gitrepository').decode('utf-8')
+
+        if distro_source_package is None:
+            if project is _DEFAULT:
+                project = self.makeProduct()
+            target = project
+        else:
+            assert project is _DEFAULT, (
+                "Passed both distribution source package and project details")
+            target = distro_source_package
+
+        if registrant is None:
+            if owner.is_team:
+                registrant = removeSecurityProxy(owner).teamowner
+            else:
+                registrant = owner
+
+        namespace = get_git_namespace(target, owner)
+        repository = namespace.createRepository(
+            registrant=registrant, name=name, **optional_repository_args)
+        naked_repository = removeSecurityProxy(repository)
+        if information_type is not None:
+            naked_repository.transitionToInformationType(
+                information_type, registrant, verify_policy=False)
+        return repository
+
+    def makePackageGitRepository(self, distro_source_package=None,
+                                 distribution=None, sourcepackagename=None,
+                                 **kwargs):
+        """Make a package Git repository on an arbitrary package.
+
+        See `makeGitRepository` for more information on arguments.
+
+        You can pass in either `distro_source_package` or one or both of
+        `distribution` and `sourcepackagename`, but not combinations or all
+        of them.
+        """
+        assert not(distro_source_package is not None and
+                   distribution is not None), (
+            "Don't pass in both distro_source_package and distribution")
+        assert not(distro_source_package is not None
+                   and sourcepackagename is not None), (
+            "Don't pass in both distro_source_package and sourcepackagename")
+        if distro_source_package is None:
+            distro_source_package = self.makeDistributionSourcePackage(
+                distribution=distribution, sourcepackagename=sourcepackagename)
+        return self.makeGitRepository(
+            distro_source_package=distro_source_package, **kwargs)
+
+    def makePersonalGitRepository(self, owner=None, **kwargs):
+        """Make a personal Git repository on an arbitrary person.
+
+        See `makeGitRepository` for more information on arguments.
+        """
+        if owner is None:
+            owner = self.makePerson()
+        return self.makeGitRepository(
+            owner=owner, project=None, distro_source_package=None, **kwargs)
+
+    def makeProjectGitRepository(self, project=None, **kwargs):
+        """Make a project Git repository on an arbitrary project.
+
+        See `makeGitRepository` for more information on arguments.
+        """
+        if project is None:
+            project = self.makeProduct()
+        return self.makeGitRepository(project=project, **kwargs)
+
+    def makeAnyGitRepository(self, **kwargs):
+        """Make a Git repository without caring about its container.
+
+        See `makeGitRepository` for more information on arguments.
+        """
+        return self.makeProjectGitRepository(**kwargs)
+
     def makeBug(self, target=None, owner=None, bug_watch_url=None,
                 information_type=None, date_closed=None, title=None,
                 date_created=None, description=None, comment=None,


Follow ups