← Back to team overview

launchpad-reviewers team mailing list archive

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

 

Colin Watson has proposed merging lp:~cjwatson/launchpad/git-namespace-tests into lp:launchpad.

Commit message:
Add GitNamespace tests, similar to those for BranchNamespace.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/git-namespace-tests/+merge/259266

When I wrote GitNamespace, we didn't yet have enough infrastructure to allow writing unit tests for it.  Now we do, so we should fill in this gap in our test coverage.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad/git-namespace-tests into lp:launchpad.
=== modified file 'lib/lp/code/model/tests/test_gitnamespace.py'
--- lib/lp/code/model/tests/test_gitnamespace.py	2015-05-14 13:57:51 +0000
+++ lib/lp/code/model/tests/test_gitnamespace.py	2015-05-15 16:11:39 +0000
@@ -4,18 +4,679 @@
 """Tests for `IGitNamespace` implementations."""
 
 from zope.component import getUtility
+from zope.security.proxy import removeSecurityProxy
 
+from lp.app.enums import (
+    FREE_INFORMATION_TYPES,
+    InformationType,
+    NON_EMBARGOED_INFORMATION_TYPES,
+    PUBLIC_INFORMATION_TYPES,
+    )
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.app.interfaces.services import IService
+from lp.app.validators import LaunchpadValidationError
 from lp.code.errors import (
     GitDefaultConflict,
+    GitRepositoryCreatorNotMemberOfOwnerTeam,
+    GitRepositoryCreatorNotOwner,
     GitRepositoryExists,
     )
+from lp.code.interfaces.gitnamespace import (
+    get_git_namespace,
+    IGitNamespace,
+    IGitNamespacePolicy,
+    )
 from lp.code.interfaces.gitrepository import IGitRepositorySet
-from lp.code.model.gitnamespace import ProjectGitNamespace
-from lp.testing import TestCaseWithFactory
+from lp.code.model.gitnamespace import (
+    PackageGitNamespace,
+    PersonalGitNamespace,
+    ProjectGitNamespace,
+    )
+from lp.registry.enums import (
+    BranchSharingPolicy,
+    PersonVisibility,
+    SharingPermission,
+    )
+from lp.testing import (
+    person_logged_in,
+    TestCaseWithFactory,
+    )
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
+class NamespaceMixin:
+    """Tests common to all namespace implementations.
+
+    You might even call these 'interface tests'.
+    """
+
+    def test_provides_interface(self):
+        # All Git namespaces provide IGitNamespace.
+        self.assertProvides(self.getNamespace(), IGitNamespace)
+
+    def test_createRepository_right_namespace(self):
+        # createRepository creates a repository in that namespace.
+        namespace = self.getNamespace()
+        repository_name = self.factory.getUniqueUnicode()
+        registrant = removeSecurityProxy(namespace).owner
+        repository = namespace.createRepository(registrant, repository_name)
+        self.assertEqual(
+            "%s/+git/%s" % (namespace.name, repository_name),
+            repository.unique_name)
+        self.assertEqual(InformationType.PUBLIC, repository.information_type)
+
+    def test_createRepository_passes_through(self):
+        # createRepository takes all the arguments that the `GitRepository`
+        # constructor takes, except for the ones that define the namespace.
+        namespace = self.getNamespace()
+        repository_name = self.factory.getUniqueUnicode()
+        registrant = removeSecurityProxy(namespace).owner
+        reviewer = self.factory.makePerson()
+        description = self.factory.getUniqueUnicode()
+        repository = namespace.createRepository(
+            registrant, repository_name, reviewer=reviewer,
+            description=description)
+        self.assertEqual(repository_name, repository.name)
+        self.assertEqual(registrant, repository.registrant)
+        self.assertEqual(reviewer, repository.reviewer)
+        self.assertEqual(description, repository.description)
+
+    def test_createRepository_subscribes_owner(self):
+        owner = self.factory.makeTeam()
+        namespace = self.getNamespace(owner)
+        repository_name = self.factory.getUniqueUnicode()
+        registrant = owner.teamowner
+        repository = namespace.createRepository(registrant, repository_name)
+        self.assertEqual([owner], list(repository.subscribers))
+
+    def test_getRepositories_no_repositories(self):
+        # getRepositories on an IGitNamespace returns a result set of
+        # repositories in that namespace.  If there are no repositories, the
+        # result set is empty.
+        namespace = self.getNamespace()
+        self.assertEqual([], list(namespace.getRepositories()))
+
+    def test_getRepositories_some_repositories(self):
+        # getRepositories on an IGitNamespace returns a result set of
+        # repositories in that namespace.
+        namespace = self.getNamespace()
+        repository_name = self.factory.getUniqueUnicode()
+        repository = namespace.createRepository(
+            removeSecurityProxy(namespace).owner, repository_name)
+        self.assertEqual([repository], list(namespace.getRepositories()))
+
+    def test_getByName_default(self):
+        # getByName returns the given default if there is no repository in
+        # the namespace with that name.
+        namespace = self.getNamespace()
+        default = object()
+        match = namespace.getByName(self.factory.getUniqueUnicode(), default)
+        self.assertIs(default, match)
+
+    def test_getByName_default_is_none(self):
+        # The default 'default' return value is None.
+        namespace = self.getNamespace()
+        match = namespace.getByName(self.factory.getUniqueUnicode())
+        self.assertIsNone(match)
+
+    def test_getByName_matches(self):
+        namespace = self.getNamespace()
+        repository_name = self.factory.getUniqueUnicode()
+        repository = namespace.createRepository(
+            removeSecurityProxy(namespace).owner, repository_name)
+        match = namespace.getByName(repository_name)
+        self.assertEqual(repository, match)
+
+    def test_isNameUsed_not(self):
+        namespace = self.getNamespace()
+        name = self.factory.getUniqueUnicode()
+        self.assertFalse(namespace.isNameUsed(name))
+
+    def test_isNameUsed_yes(self):
+        namespace = self.getNamespace()
+        repository_name = self.factory.getUniqueUnicode()
+        namespace.createRepository(
+            removeSecurityProxy(namespace).owner, repository_name)
+        self.assertTrue(namespace.isNameUsed(repository_name))
+
+    def test_findUnusedName_unused(self):
+        # findUnusedName returns the given name if that name is not used.
+        namespace = self.getNamespace()
+        name = self.factory.getUniqueUnicode()
+        unused_name = namespace.findUnusedName(name)
+        self.assertEqual(name, unused_name)
+
+    def test_findUnusedName_used(self):
+        # findUnusedName returns the given name with a numeric suffix if
+        # it's already used.
+        namespace = self.getNamespace()
+        name = self.factory.getUniqueUnicode()
+        namespace.createRepository(removeSecurityProxy(namespace).owner, name)
+        unused_name = namespace.findUnusedName(name)
+        self.assertEqual("%s-1" % name, unused_name)
+
+    def test_findUnusedName_used_twice(self):
+        # findUnusedName returns the given name with a numeric suffix if
+        # it's already used.
+        namespace = self.getNamespace()
+        name = self.factory.getUniqueUnicode()
+        namespace.createRepository(removeSecurityProxy(namespace).owner, name)
+        namespace.createRepository(
+            removeSecurityProxy(namespace).owner, name + "-1")
+        unused_name = namespace.findUnusedName(name)
+        self.assertEqual("%s-2" % name, unused_name)
+
+    def test_validateMove(self):
+        # If the mover is allowed to move the repository into the namespace,
+        # if there are absolutely no problems at all, then validateMove
+        # raises nothing and returns None.
+        namespace = self.getNamespace()
+        namespace_owner = removeSecurityProxy(namespace).owner
+        repository = self.factory.makeGitRepository()
+        # Doesn't raise an exception.
+        namespace.validateMove(repository, namespace_owner)
+
+    def test_validateMove_repository_with_name_exists(self):
+        # If a repository with the same name as the given repository already
+        # exists in the namespace, validateMove raises a GitRepositoryExists
+        # error.
+        namespace = self.getNamespace()
+        namespace_owner = removeSecurityProxy(namespace).owner
+        name = self.factory.getUniqueUnicode()
+        namespace.createRepository(removeSecurityProxy(namespace).owner, name)
+        repository = self.factory.makeGitRepository(name=name)
+        self.assertRaises(
+            GitRepositoryExists,
+            namespace.validateMove, repository, namespace_owner)
+
+    def test_validateMove_forbidden_owner(self):
+        # If the mover isn't allowed to create repositories in the
+        # namespace, then they aren't allowed to move repositories in there
+        # either, so validateMove wil raise a GitRepositoryCreatorNotOwner
+        # error.
+        namespace = self.getNamespace()
+        repository = self.factory.makeGitRepository()
+        mover = self.factory.makePerson()
+        self.assertRaises(
+            GitRepositoryCreatorNotOwner,
+            namespace.validateMove, repository, mover)
+
+    def test_validateMove_not_team_member(self):
+        # If the mover isn't allowed to create repositories in the namespace
+        # because they aren't a member of the team that owns the namespace,
+        # validateMove raises a GitRepositoryCreatorNotMemberOfOwnerTeam
+        # error.
+        team = self.factory.makeTeam()
+        namespace = self.getNamespace(person=team)
+        repository = self.factory.makeGitRepository()
+        mover = self.factory.makePerson()
+        self.assertRaises(
+            GitRepositoryCreatorNotMemberOfOwnerTeam,
+            namespace.validateMove, repository, mover)
+
+    def test_validateMove_with_other_name(self):
+        # If you pass a name to validateMove, that'll check to see whether
+        # the repository could be safely moved given a rename.
+        namespace = self.getNamespace()
+        namespace_owner = removeSecurityProxy(namespace).owner
+        name = self.factory.getUniqueUnicode()
+        namespace.createRepository(removeSecurityProxy(namespace).owner, name)
+        repository = self.factory.makeGitRepository()
+        self.assertRaises(
+            GitRepositoryExists,
+            namespace.validateMove, repository, namespace_owner, name=name)
+
+
+class TestPersonalGitNamespace(TestCaseWithFactory, NamespaceMixin):
+    """Tests for `PersonalGitNamespace`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def getNamespace(self, person=None):
+        if person is None:
+            person = self.factory.makePerson()
+        return get_git_namespace(person, person)
+
+    def test_name(self):
+        # A personal namespace has repositories with names starting with
+        # ~foo.
+        person = self.factory.makePerson()
+        namespace = PersonalGitNamespace(person)
+        self.assertEqual("~%s" % person.name, namespace.name)
+
+    def test_owner(self):
+        # The person passed to a personal namespace is the owner.
+        person = self.factory.makePerson()
+        namespace = PersonalGitNamespace(person)
+        self.assertEqual(person, removeSecurityProxy(namespace).owner)
+
+    def test_target(self):
+        # The target of a personal namespace is the owner of that namespace.
+        person = self.factory.makePerson()
+        namespace = PersonalGitNamespace(person)
+        self.assertEqual(person, namespace.target)
+
+
+class TestProjectGitNamespace(TestCaseWithFactory, NamespaceMixin):
+    """Tests for `ProjectGitNamespace`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def getNamespace(self, person=None):
+        if person is None:
+            person = self.factory.makePerson()
+        return get_git_namespace(self.factory.makeProduct(), person)
+
+    def test_name(self):
+        # A project namespace has repositories with names starting with
+        # ~foo/bar.
+        person = self.factory.makePerson()
+        project = self.factory.makeProduct()
+        namespace = ProjectGitNamespace(person, project)
+        self.assertEqual(
+            "~%s/%s" % (person.name, project.name), namespace.name)
+
+    def test_owner(self):
+        # The person passed to a project namespace is the owner.
+        person = self.factory.makePerson()
+        project = self.factory.makeProduct()
+        namespace = ProjectGitNamespace(person, project)
+        self.assertEqual(person, removeSecurityProxy(namespace).owner)
+
+    def test_target(self):
+        # The target for a project namespace is the project.
+        person = self.factory.makePerson()
+        project = self.factory.makeProduct()
+        namespace = ProjectGitNamespace(person, project)
+        self.assertEqual(project, namespace.target)
+
+
+class TestProjectGitNamespacePrivacyWithInformationType(TestCaseWithFactory):
+    """Tests for the privacy aspects of `ProjectGitNamespace`.
+
+    This tests the behaviour for a project using the new
+    branch_sharing_policy rules.
+    """
+
+    layer = DatabaseFunctionalLayer
+
+    def makeProjectGitNamespace(self, sharing_policy, person=None):
+        if person is None:
+            person = self.factory.makePerson()
+        project = self.factory.makeProduct()
+        self.factory.makeCommercialSubscription(product=project)
+        with person_logged_in(project.owner):
+            project.setBranchSharingPolicy(sharing_policy)
+        namespace = ProjectGitNamespace(person, project)
+        return namespace
+
+    def test_public_anyone(self):
+        namespace = self.makeProjectGitNamespace(BranchSharingPolicy.PUBLIC)
+        self.assertContentEqual(
+            FREE_INFORMATION_TYPES, namespace.getAllowedInformationTypes())
+        self.assertEqual(
+            InformationType.PUBLIC, namespace.getDefaultInformationType())
+
+    def test_forbidden_anyone(self):
+        namespace = self.makeProjectGitNamespace(BranchSharingPolicy.FORBIDDEN)
+        self.assertEqual([], namespace.getAllowedInformationTypes())
+        self.assertIsNone(namespace.getDefaultInformationType())
+
+    def test_public_or_proprietary_anyone(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.PUBLIC_OR_PROPRIETARY)
+        self.assertContentEqual(
+            NON_EMBARGOED_INFORMATION_TYPES,
+            namespace.getAllowedInformationTypes())
+        self.assertEqual(
+            InformationType.PUBLIC, namespace.getDefaultInformationType())
+
+    def test_proprietary_or_public_anyone(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
+        self.assertEqual([], namespace.getAllowedInformationTypes())
+        self.assertIsNone(namespace.getDefaultInformationType())
+
+    def test_proprietary_or_public_owner_grantee(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
+        with person_logged_in(namespace.target.owner):
+            getUtility(IService, "sharing").sharePillarInformation(
+                namespace.target, namespace.owner, namespace.target.owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        self.assertContentEqual(
+            NON_EMBARGOED_INFORMATION_TYPES,
+            namespace.getAllowedInformationTypes())
+        self.assertEqual(
+            InformationType.PROPRIETARY,
+            namespace.getDefaultInformationType())
+
+    def test_proprietary_or_public_caller_grantee(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.PROPRIETARY_OR_PUBLIC)
+        grantee = self.factory.makePerson()
+        with person_logged_in(namespace.target.owner):
+            getUtility(IService, "sharing").sharePillarInformation(
+                namespace.target, grantee, namespace.target.owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        self.assertContentEqual(
+            NON_EMBARGOED_INFORMATION_TYPES,
+            namespace.getAllowedInformationTypes(grantee))
+        self.assertEqual(
+            InformationType.PROPRIETARY,
+            namespace.getDefaultInformationType(grantee))
+
+    def test_proprietary_anyone(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.PROPRIETARY)
+        self.assertEqual([], namespace.getAllowedInformationTypes())
+        self.assertIsNone(namespace.getDefaultInformationType())
+
+    def test_proprietary_repository_owner_grantee(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.PROPRIETARY)
+        with person_logged_in(namespace.target.owner):
+            getUtility(IService, "sharing").sharePillarInformation(
+                namespace.target, namespace.owner, namespace.target.owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        self.assertContentEqual(
+            [InformationType.PROPRIETARY],
+            namespace.getAllowedInformationTypes())
+        self.assertEqual(
+            InformationType.PROPRIETARY,
+            namespace.getDefaultInformationType())
+
+    def test_proprietary_caller_grantee(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.PROPRIETARY)
+        grantee = self.factory.makePerson()
+        with person_logged_in(namespace.target.owner):
+            getUtility(IService, "sharing").sharePillarInformation(
+                namespace.target, grantee, namespace.target.owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        self.assertContentEqual(
+            [InformationType.PROPRIETARY],
+            namespace.getAllowedInformationTypes(grantee))
+        self.assertEqual(
+            InformationType.PROPRIETARY,
+            namespace.getDefaultInformationType(grantee))
+
+    def test_embargoed_or_proprietary_anyone(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
+        self.assertEqual([], namespace.getAllowedInformationTypes())
+        self.assertIsNone(namespace.getDefaultInformationType())
+
+    def test_embargoed_or_proprietary_owner_grantee(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
+        with person_logged_in(namespace.target.owner):
+            getUtility(IService, "sharing").sharePillarInformation(
+                namespace.target, namespace.owner, namespace.target.owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        self.assertContentEqual(
+            [InformationType.PROPRIETARY, InformationType.EMBARGOED],
+            namespace.getAllowedInformationTypes())
+        self.assertEqual(
+            InformationType.EMBARGOED,
+            namespace.getDefaultInformationType())
+
+    def test_embargoed_or_proprietary_caller_grantee(self):
+        namespace = self.makeProjectGitNamespace(
+            BranchSharingPolicy.EMBARGOED_OR_PROPRIETARY)
+        grantee = self.factory.makePerson()
+        with person_logged_in(namespace.target.owner):
+            getUtility(IService, "sharing").sharePillarInformation(
+                namespace.target, grantee, namespace.target.owner,
+                {InformationType.PROPRIETARY: SharingPermission.ALL})
+        self.assertContentEqual(
+            [InformationType.PROPRIETARY, InformationType.EMBARGOED],
+            namespace.getAllowedInformationTypes(grantee))
+        self.assertEqual(
+            InformationType.EMBARGOED,
+            namespace.getDefaultInformationType(grantee))
+
+
+class TestPackageGitNamespace(TestCaseWithFactory, NamespaceMixin):
+    """Tests for `PackageGitNamespace`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def getNamespace(self, person=None):
+        if person is None:
+            person = self.factory.makePerson()
+        return get_git_namespace(
+            self.factory.makeDistributionSourcePackage(), person)
+
+    def test_name(self):
+        # A package namespace has repositories that start with
+        # ~foo/distribution/+source/packagename.
+        person = self.factory.makePerson()
+        dsp = self.factory.makeDistributionSourcePackage()
+        namespace = PackageGitNamespace(person, dsp)
+        self.assertEqual(
+            "~%s/%s/+source/%s" % (
+                person.name, dsp.distribution.name,
+                dsp.sourcepackagename.name),
+            namespace.name)
+
+    def test_owner(self):
+        # The person passed to a package namespace is the owner.
+        person = self.factory.makePerson()
+        dsp = self.factory.makeDistributionSourcePackage()
+        namespace = PackageGitNamespace(person, dsp)
+        self.assertEqual(person, removeSecurityProxy(namespace).owner)
+
+    def test_target(self):
+        # The target for a package namespace is the distribution source
+        # package.
+        person = self.factory.makePerson()
+        dsp = self.factory.makeDistributionSourcePackage()
+        namespace = PackageGitNamespace(person, dsp)
+        self.assertEqual(dsp, namespace.target)
+
+
+class TestNamespaceSet(TestCaseWithFactory):
+    """Tests for `get_namespace`."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_get_personal(self):
+        person = self.factory.makePerson()
+        namespace = get_git_namespace(person, person)
+        self.assertIsInstance(namespace, PersonalGitNamespace)
+
+    def test_get_project(self):
+        person = self.factory.makePerson()
+        project = self.factory.makeProduct()
+        namespace = get_git_namespace(project, person)
+        self.assertIsInstance(namespace, ProjectGitNamespace)
+
+    def test_get_package(self):
+        person = self.factory.makePerson()
+        dsp = self.factory.makeDistributionSourcePackage()
+        namespace = get_git_namespace(dsp, person)
+        self.assertIsInstance(namespace, PackageGitNamespace)
+
+
+class TestPersonalGitNamespaceAllowedInformationTypes(TestCaseWithFactory):
+    """Tests for PersonalGitNamespace.getAllowedInformationTypes."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_anyone(self):
+        # Personal repositories are not private for individuals.
+        person = self.factory.makePerson()
+        namespace = PersonalGitNamespace(person)
+        self.assertContentEqual(
+            FREE_INFORMATION_TYPES, namespace.getAllowedInformationTypes())
+
+    def test_public_team(self):
+        # Personal repositories for public teams cannot be private.
+        team = self.factory.makeTeam()
+        namespace = PersonalGitNamespace(team)
+        self.assertContentEqual(
+            FREE_INFORMATION_TYPES, namespace.getAllowedInformationTypes())
+
+    def test_private_team(self):
+        # Personal repositories can be private or public for private teams.
+        team = self.factory.makeTeam(visibility=PersonVisibility.PRIVATE)
+        namespace = PersonalGitNamespace(team)
+        self.assertContentEqual(
+            NON_EMBARGOED_INFORMATION_TYPES,
+            namespace.getAllowedInformationTypes())
+
+
+class TestPackageGitNamespaceAllowedInformationTypes(TestCaseWithFactory):
+    """Tests for PackageGitNamespace.getAllowedInformationTypes."""
+
+    layer = DatabaseFunctionalLayer
+
+    def test_anyone(self):
+        # Package repositories are always public.
+        dsp = self.factory.makeDistributionSourcePackage()
+        person = self.factory.makePerson()
+        namespace = PackageGitNamespace(person, dsp)
+        self.assertContentEqual(
+            PUBLIC_INFORMATION_TYPES, namespace.getAllowedInformationTypes())
+
+
+class BaseValidateNewRepositoryMixin:
+
+    layer = DatabaseFunctionalLayer
+
+    def _getNamespace(self, owner):
+        # Return a namespace appropriate for the owner specified.
+        raise NotImplementedError(self._getNamespace)
+
+    def test_registrant_not_owner(self):
+        # If the namespace owner is an individual, and the registrant is not
+        # the owner, GitRepositoryCreatorNotOwner is raised.
+        namespace = self._getNamespace(self.factory.makePerson())
+        self.assertRaises(
+            GitRepositoryCreatorNotOwner,
+            namespace.validateRegistrant, self.factory.makePerson())
+
+    def test_registrant_not_in_owner_team(self):
+        # If the namespace owner is a team, and the registrant is not in the
+        # team, GitRepositoryCreatorNotMemberOfOwnerTeam is raised.
+        namespace = self._getNamespace(self.factory.makeTeam())
+        self.assertRaises(
+            GitRepositoryCreatorNotMemberOfOwnerTeam,
+            namespace.validateRegistrant, self.factory.makePerson())
+
+    def test_existing_repository(self):
+        # If a repository exists with the same name, then
+        # GitRepositoryExists is raised.
+        namespace = self._getNamespace(self.factory.makePerson())
+        repository = namespace.createRepository(
+            namespace.owner, self.factory.getUniqueUnicode())
+        self.assertRaises(
+            GitRepositoryExists,
+            namespace.validateRepositoryName, repository.name)
+
+    def test_invalid_name(self):
+        # If the repository name is not valid, a LaunchpadValidationError is
+        # raised.
+        namespace = self._getNamespace(self.factory.makePerson())
+        self.assertRaises(
+            LaunchpadValidationError,
+            namespace.validateRepositoryName, u"+foo")
+
+    def test_permitted_first_character(self):
+        # The first character of a repository name must be a letter or a
+        # number.
+        namespace = self._getNamespace(self.factory.makePerson())
+        for c in [unichr(i) for i in range(128)]:
+            if c.isalnum():
+                namespace.validateRepositoryName(c)
+            else:
+                self.assertRaises(
+                    LaunchpadValidationError,
+                    namespace.validateRepositoryName, c)
+
+    def test_permitted_subsequent_character(self):
+        # After the first character, letters, numbers and certain
+        # punctuation is permitted.
+        namespace = self._getNamespace(self.factory.makePerson())
+        for c in [unichr(i) for i in range(128)]:
+            if c.isalnum() or c in "+-_@.":
+                namespace.validateRepositoryName("a" + c)
+            else:
+                self.assertRaises(
+                    LaunchpadValidationError,
+                    namespace.validateRepositoryName, "a" + c)
+
+
+class TestPersonalGitNamespaceValidateNewRepository(
+    TestCaseWithFactory, BaseValidateNewRepositoryMixin):
+
+    def _getNamespace(self, owner):
+        return PersonalGitNamespace(owner)
+
+
+class TestPackageGitNamespaceValidateNewRepository(
+    TestCaseWithFactory, BaseValidateNewRepositoryMixin):
+
+    def _getNamespace(self, owner):
+        dsp = self.factory.makeDistributionSourcePackage()
+        return PackageGitNamespace(owner, dsp)
+
+
+class TestProjectGitNamespaceValidateNewRepository(
+    TestCaseWithFactory, BaseValidateNewRepositoryMixin):
+
+    def _getNamespace(self, owner):
+        project = self.factory.makeProduct()
+        return ProjectGitNamespace(owner, project)
+
+
+class TestPersonalGitRepositories(TestCaseWithFactory):
+    """Personal repositories have no branch visibility policy."""
+
+    layer = DatabaseFunctionalLayer
+
+    def assertPublic(self, creator, owner):
+        """Assert that the policy check would result in a public repository.
+
+        :param creator: The user creating the repository.
+        :param owner: The person or team that will be the owner of the
+            repository.
+        """
+        namespace = get_git_namespace(owner, owner)
+        self.assertNotIn(
+            InformationType.PROPRIETARY,
+            namespace.getAllowedInformationTypes())
+
+    def assertPolicyCheckRaises(self, error, creator, owner):
+        """Assert that the policy check raises an exception.
+
+        :param error: The exception class that should be raised.
+        :param creator: The user creating the repository.
+        :param owner: The person or team that will be the owner of the
+            repository.
+        """
+        policy = IGitNamespacePolicy(get_git_namespace(owner, owner))
+        self.assertRaises(error, policy.validateRegistrant, registrant=creator)
+
+    def test_personal_repositories_public(self):
+        # Personal repositories created by anyone are public.
+        person = self.factory.makePerson()
+        self.assertPublic(person, person)
+
+    def test_team_personal_repositories(self):
+        # Team-owned personal repositories are allowed, and are public.
+        person = self.factory.makePerson()
+        team = self.factory.makeTeam(members=[person])
+        self.assertPublic(person, team)
+
+    def test_no_create_personal_repository_for_other_user(self):
+        # One user can't create personal repositories owned by another.
+        self.assertPolicyCheckRaises(
+            GitRepositoryCreatorNotOwner, self.factory.makePerson(),
+            self.factory.makePerson())
+
+
 class TestGitNamespaceMoveRepository(TestCaseWithFactory):
     """Test the IGitNamespace.moveRepository method."""
 


Follow ups