Merge lp:~wgrant/launchpad/invisible-person-api into lp:launchpad

Proposed by William Grant
Status: Merged
Merged at revision: 18019
Proposed branch: lp:~wgrant/launchpad/invisible-person-api
Merge into: lp:launchpad
Prerequisite: lp:~wgrant/launchpad/invisible-person
Diff against target: 539 lines (+365/-21)
5 files modified
lib/lp/registry/browser/tests/test_person_webservice.py (+115/-1)
lib/lp/registry/errors.py (+6/-0)
lib/lp/registry/interfaces/person.py (+45/-0)
lib/lp/registry/model/person.py (+39/-3)
lib/lp/registry/tests/test_personset.py (+160/-17)
To merge this branch: bzr merge lp:~wgrant/launchpad/invisible-person-api
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+292939@code.launchpad.net

Commit message

Add getUsernameForSSO and setUsernameFromSSO API methods.

Description of the change

Add getUsernameForSSO and setUsernameFromSSO API methods.

SSO will soon allow creation of usernames for users without Launchpad accounts. This is implemented with invisible Person rows with Account.status == PLACEHOLDER.

To post a comment you must log in.
Revision history for this message
Colin Watson (cjwatson) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/tests/test_person_webservice.py'
--- lib/lp/registry/browser/tests/test_person_webservice.py 2016-03-21 05:37:40 +0000
+++ lib/lp/registry/browser/tests/test_person_webservice.py 2016-05-03 23:49:59 +0000
@@ -13,7 +13,10 @@
13 TeamMembershipStatus,13 TeamMembershipStatus,
14 )14 )
15from lp.registry.interfaces.teammembership import ITeamMembershipSet15from lp.registry.interfaces.teammembership import ITeamMembershipSet
16from lp.services.identity.interfaces.account import AccountStatus16from lp.services.identity.interfaces.account import (
17 AccountStatus,
18 IAccountSet,
19 )
17from lp.services.openid.model.openididentifier import OpenIdIdentifier20from lp.services.openid.model.openididentifier import OpenIdIdentifier
18from lp.services.webapp import snapshot21from lp.services.webapp import snapshot
19from lp.services.webapp.interfaces import OAuthPermission22from lp.services.webapp.interfaces import OAuthPermission
@@ -367,3 +370,114 @@
367 sca = getUtility(IPersonSet).getByName('software-center-agent')370 sca = getUtility(IPersonSet).getByName('software-center-agent')
368 response = self.getOrCreateSoftwareCenterCustomer(sca)371 response = self.getOrCreateSoftwareCenterCustomer(sca)
369 self.assertEqual(400, response.status)372 self.assertEqual(400, response.status)
373
374 def test_getUsernameForSSO(self):
375 # canonical-identity-provider (SSO) can get the username for an
376 # OpenID identifier suffix.
377 with admin_logged_in():
378 sso = getUtility(IPersonSet).getByName('ubuntu-sso')
379 existing = self.factory.makePerson(name='username')
380 taken_openid = (
381 existing.account.openid_identifiers.any().identifier)
382 webservice = webservice_for_person(
383 sso, permission=OAuthPermission.READ_PUBLIC)
384 response = webservice.named_get(
385 '/people', 'getUsernameForSSO',
386 openid_identifier=taken_openid, api_version='devel')
387 self.assertEqual(200, response.status)
388 self.assertEqual('username', response.jsonBody())
389
390 def test_getUsernameForSSO_nonexistent(self):
391 with admin_logged_in():
392 sso = getUtility(IPersonSet).getByName('ubuntu-sso')
393 webservice = webservice_for_person(
394 sso, permission=OAuthPermission.READ_PUBLIC)
395 response = webservice.named_get(
396 '/people', 'getUsernameForSSO',
397 openid_identifier='doesnotexist', api_version='devel')
398 self.assertEqual(200, response.status)
399 self.assertEqual(None, response.jsonBody())
400
401 def setUsernameFromSSO(self, user, openid_identifier, name,
402 dry_run=False):
403 webservice = webservice_for_person(
404 user, permission=OAuthPermission.WRITE_PRIVATE)
405 response = webservice.named_post(
406 '/people', 'setUsernameFromSSO',
407 openid_identifier=openid_identifier, name=name, dry_run=dry_run,
408 api_version='devel')
409 return response
410
411 def test_setUsernameFromSSO(self):
412 # canonical-identity-provider (SSO) can create a placeholder
413 # Person to give a username to a non-LP user.
414 with admin_logged_in():
415 sso = getUtility(IPersonSet).getByName('ubuntu-sso')
416 response = self.setUsernameFromSSO(sso, 'foo', 'bar')
417 self.assertEqual(200, response.status)
418 with admin_logged_in():
419 by_name = getUtility(IPersonSet).getByName('bar')
420 by_openid = getUtility(IPersonSet).getByOpenIDIdentifier(
421 u'http://testopenid.dev/+id/foo')
422 self.assertEqual(by_name, by_openid)
423 self.assertEqual(
424 AccountStatus.PLACEHOLDER, by_name.account_status)
425
426 def test_setUsernameFromSSO_dry_run(self):
427 # setUsernameFromSSO provides a dry run mode that performs all
428 # the checks but doesn't actually make changes. Useful for input
429 # validation in SSO.
430 with admin_logged_in():
431 sso = getUtility(IPersonSet).getByName('ubuntu-sso')
432 response = self.setUsernameFromSSO(sso, 'foo', 'bar', dry_run=True)
433 self.assertEqual(200, response.status)
434 with admin_logged_in():
435 self.assertIs(None, getUtility(IPersonSet).getByName('bar'))
436 self.assertRaises(
437 LookupError,
438 getUtility(IAccountSet).getByOpenIDIdentifier, u'foo')
439
440 def test_setUsernameFromSSO_is_restricted(self):
441 # The method may only be invoked by the ~ubuntu-sso celebrity
442 # user, as it is security-sensitive.
443 with admin_logged_in():
444 random = self.factory.makePerson()
445 response = self.setUsernameFromSSO(random, 'foo', 'bar')
446 self.assertEqual(401, response.status)
447
448 def test_setUsernameFromSSO_rejects_bad_input(self, dry_run=False):
449 # The method returns meaningful errors on bad input, so SSO can
450 # give advice to users.
451 # Check canonical-identity-provider before changing these!
452 with admin_logged_in():
453 sso = getUtility(IPersonSet).getByName('ubuntu-sso')
454 self.factory.makePerson(name='taken-name')
455 existing = self.factory.makePerson()
456 taken_openid = (
457 existing.account.openid_identifiers.any().identifier)
458
459 response = self.setUsernameFromSSO(
460 sso, 'foo', 'taken-name', dry_run=dry_run)
461 self.assertEqual(400, response.status)
462 self.assertEqual(
463 'name: taken-name is already in use by another person or team.',
464 response.body)
465
466 response = self.setUsernameFromSSO(
467 sso, 'foo', 'private-name', dry_run=dry_run)
468 self.assertEqual(400, response.status)
469 self.assertEqual(
470 'name: The name 'private-name' has been blocked by the '
471 'Launchpad administrators. Contact Launchpad Support if you want '
472 'to use this name.',
473 response.body)
474
475 response = self.setUsernameFromSSO(
476 sso, taken_openid, 'bar', dry_run=dry_run)
477 self.assertEqual(400, response.status)
478 self.assertEqual(
479 'An account for that OpenID identifier already exists.',
480 response.body)
481
482 def test_setUsernameFromSSO_rejects_bad_input_in_dry_run(self):
483 self.test_setUsernameFromSSO_rejects_bad_input(dry_run=True)
370484
=== modified file 'lib/lp/registry/errors.py'
--- lib/lp/registry/errors.py 2012-11-28 21:32:06 +0000
+++ lib/lp/registry/errors.py 2016-05-03 23:49:59 +0000
@@ -21,6 +21,7 @@
21 'NameAlreadyTaken',21 'NameAlreadyTaken',
22 'NoSuchDistroSeries',22 'NoSuchDistroSeries',
23 'NoSuchSourcePackageName',23 'NoSuchSourcePackageName',
24 'NotPlaceholderAccount',
24 'InclusiveTeamLinkageError',25 'InclusiveTeamLinkageError',
25 'PPACreationError',26 'PPACreationError',
26 'PrivatePersonLinkageError',27 'PrivatePersonLinkageError',
@@ -61,6 +62,11 @@
6162
6263
63@error_status(httplib.BAD_REQUEST)64@error_status(httplib.BAD_REQUEST)
65class NotPlaceholderAccount(Exception):
66 """A non-placeholder account already exists for that OpenID identifier."""
67
68
69@error_status(httplib.BAD_REQUEST)
64class InvalidFilename(Exception):70class InvalidFilename(Exception):
65 """An invalid filename was used as an attachment filename."""71 """An invalid filename was used as an attachment filename."""
6672
6773
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2016-04-11 06:38:48 +0000
+++ lib/lp/registry/interfaces/person.py 2016-05-03 23:49:59 +0000
@@ -2223,6 +2223,51 @@
2223 :param full_name: the full name of the user.2223 :param full_name: the full name of the user.
2224 """2224 """
22252225
2226 @call_with(user=REQUEST_USER)
2227 @operation_parameters(
2228 openid_identifier=TextLine(
2229 title=_("OpenID identifier suffix"), required=True))
2230 @export_read_operation()
2231 @operation_for_version("devel")
2232 def getUsernameForSSO(user, openid_identifier):
2233 """Restricted person creation API for SSO.
2234
2235 This method can only be called by the Ubuntu SSO service. It
2236 finds the username for an account by OpenID identifier.
2237
2238 :param user: the `IPerson` performing the operation. Only the
2239 ubuntu-sso celebrity is allowed.
2240 :param openid_identifier: OpenID identifier suffix for the user.
2241 This is *not* the full URL, just the unique suffix portion.
2242 """
2243
2244 @call_with(user=REQUEST_USER)
2245 @operation_parameters(
2246 openid_identifier=TextLine(
2247 title=_("OpenID identifier suffix"), required=True),
2248 name=copy_field(IPerson['name']),
2249 dry_run=Bool(_("Don't save changes")))
2250 @export_write_operation()
2251 @operation_for_version("devel")
2252 def setUsernameFromSSO(user, openid_identifier, name, dry_run=False):
2253 """Restricted person creation API for SSO.
2254
2255 This method can only be called by the Ubuntu SSO service. It
2256 reserves a username for an account by OpenID identifier, as long as
2257 the user has no Launchpad account.
2258
2259 :param user: the `IPerson` performing the operation. Only the
2260 ubuntu-sso celebrity is allowed.
2261 :param openid_identifier: OpenID identifier suffix for the user.
2262 This is *not* the full URL, just the unique suffix portion.
2263 :param name: the desired username.
2264 :raises: `InvalidName` if the username doesn't meet character
2265 constraints.
2266 :raises: `NameAlreadyTaken` if the username is already in use.
2267 :raises: `NotPlaceholderAccount` if the OpenID identifier has a
2268 non-placeholder Launchpad account.
2269 """
2270
2226 @call_with(teamowner=REQUEST_USER)2271 @call_with(teamowner=REQUEST_USER)
2227 @rename_parameters_as(2272 @rename_parameters_as(
2228 teamdescription='team_description',2273 teamdescription='team_description',
22292274
=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py 2016-04-11 08:10:46 +0000
+++ lib/lp/registry/model/person.py 2016-05-03 23:49:59 +0000
@@ -154,6 +154,7 @@
154 InvalidName,154 InvalidName,
155 JoinNotAllowed,155 JoinNotAllowed,
156 NameAlreadyTaken,156 NameAlreadyTaken,
157 NotPlaceholderAccount,
157 PPACreationError,158 PPACreationError,
158 TeamMembershipPolicyError,159 TeamMembershipPolicyError,
159 )160 )
@@ -3429,6 +3430,38 @@
3429 trust_email=False)3430 trust_email=False)
3430 return person3431 return person
34313432
3433 def getUsernameForSSO(self, user, openid_identifier):
3434 """See `IPersonSet`."""
3435 if user != getUtility(ILaunchpadCelebrities).ubuntu_sso:
3436 raise Unauthorized()
3437 try:
3438 account = getUtility(IAccountSet).getByOpenIDIdentifier(
3439 openid_identifier)
3440 except LookupError:
3441 return None
3442 return IPerson(account).name
3443
3444 def setUsernameFromSSO(self, user, openid_identifier, name,
3445 dry_run=False):
3446 """See `IPersonSet`."""
3447 if user != getUtility(ILaunchpadCelebrities).ubuntu_sso:
3448 raise Unauthorized()
3449 self._validateName(name)
3450 try:
3451 account = getUtility(IAccountSet).getByOpenIDIdentifier(
3452 openid_identifier)
3453 except LookupError:
3454 if not dry_run:
3455 person = self.createPlaceholderPerson(openid_identifier, name)
3456 else:
3457 if account.status != AccountStatus.PLACEHOLDER:
3458 raise NotPlaceholderAccount(
3459 "An account for that OpenID identifier already exists.")
3460 if not dry_run:
3461 account = removeSecurityProxy(account)
3462 person = IPerson(account)
3463 person.name = person.display_name = account.displayname = name
3464
3432 def newTeam(self, teamowner, name, display_name, teamdescription=None,3465 def newTeam(self, teamowner, name, display_name, teamdescription=None,
3433 membership_policy=TeamMembershipPolicy.MODERATED,3466 membership_policy=TeamMembershipPolicy.MODERATED,
3434 defaultmembershipperiod=None, defaultrenewalperiod=None,3467 defaultmembershipperiod=None, defaultrenewalperiod=None,
@@ -3507,9 +3540,7 @@
3507 rationale=PersonCreationRationale.USERNAME_PLACEHOLDER,3540 rationale=PersonCreationRationale.USERNAME_PLACEHOLDER,
3508 comment="when setting a username in SSO", account=account)3541 comment="when setting a username in SSO", account=account)
35093542
3510 def _newPerson(self, name, displayname, hide_email_addresses,3543 def _validateName(self, name):
3511 rationale, comment=None, registrant=None, account=None):
3512 """Create and return a new Person with the given attributes."""
3513 if not valid_name(name):3544 if not valid_name(name):
3514 raise InvalidName(3545 raise InvalidName(
3515 "%s is not a valid name for a person." % name)3546 "%s is not a valid name for a person." % name)
@@ -3520,6 +3551,11 @@
3520 raise NameAlreadyTaken(3551 raise NameAlreadyTaken(
3521 "The name '%s' is already taken." % name)3552 "The name '%s' is already taken." % name)
35223553
3554 def _newPerson(self, name, displayname, hide_email_addresses,
3555 rationale, comment=None, registrant=None, account=None):
3556 """Create and return a new Person with the given attributes."""
3557 self._validateName(name)
3558
3523 if not displayname:3559 if not displayname:
3524 displayname = name.capitalize()3560 displayname = name.capitalize()
35253561
35263562
=== modified file 'lib/lp/registry/tests/test_personset.py'
--- lib/lp/registry/tests/test_personset.py 2016-04-11 08:10:46 +0000
+++ lib/lp/registry/tests/test_personset.py 2016-05-03 23:49:59 +0000
@@ -17,6 +17,7 @@
1717
18from lp.code.tests.helpers import remove_all_sample_data_branches18from lp.code.tests.helpers import remove_all_sample_data_branches
19from lp.registry.errors import (19from lp.registry.errors import (
20 NotPlaceholderAccount,
20 InvalidName,21 InvalidName,
21 NameAlreadyTaken,22 NameAlreadyTaken,
22 )23 )
@@ -41,6 +42,7 @@
41 AccountCreationRationale,42 AccountCreationRationale,
42 AccountStatus,43 AccountStatus,
43 AccountSuspendedError,44 AccountSuspendedError,
45 IAccountSet,
44 )46 )
45from lp.services.identity.interfaces.emailaddress import (47from lp.services.identity.interfaces.emailaddress import (
46 EmailAddressAlreadyTaken,48 EmailAddressAlreadyTaken,
@@ -67,6 +69,13 @@
67from lp.testing.matchers import HasQueryCount69from lp.testing.matchers import HasQueryCount
6870
6971
72def make_openid_identifier(account, identifier):
73 openid_identifier = OpenIdIdentifier()
74 openid_identifier.identifier = identifier
75 openid_identifier.account = account
76 return IStore(OpenIdIdentifier).add(openid_identifier)
77
78
70class TestPersonSet(TestCaseWithFactory):79class TestPersonSet(TestCaseWithFactory):
71 """Test `IPersonSet`."""80 """Test `IPersonSet`."""
72 layer = DatabaseFunctionalLayer81 layer = DatabaseFunctionalLayer
@@ -248,7 +257,7 @@
248257
249 # Generate some valid test data.258 # Generate some valid test data.
250 self.account = self.makeAccount()259 self.account = self.makeAccount()
251 self.identifier = self.makeOpenIdIdentifier(self.account, u'whatever')260 self.identifier = make_openid_identifier(self.account, u'whatever')
252 self.person = self.makePerson(self.account)261 self.person = self.makePerson(self.account)
253 self.email = self.makeEmailAddress(262 self.email = self.makeEmailAddress(
254 email='whatever@example.com', person=self.person)263 email='whatever@example.com', person=self.person)
@@ -259,12 +268,6 @@
259 creation_rationale=AccountCreationRationale.UNKNOWN,268 creation_rationale=AccountCreationRationale.UNKNOWN,
260 status=AccountStatus.ACTIVE))269 status=AccountStatus.ACTIVE))
261270
262 def makeOpenIdIdentifier(self, account, identifier):
263 openid_identifier = OpenIdIdentifier()
264 openid_identifier.identifier = identifier
265 openid_identifier.account = account
266 return self.store.add(openid_identifier)
267
268 def makePerson(self, account):271 def makePerson(self, account):
269 return self.store.add(Person(272 return self.store.add(Person(
270 name='acc%d' % account.id, account=account,273 name='acc%d' % account.id, account=account,
@@ -627,12 +630,6 @@
627 super(TestPersonSetGetOrCreateSoftwareCenterCustomer, self).setUp()630 super(TestPersonSetGetOrCreateSoftwareCenterCustomer, self).setUp()
628 self.sca = getUtility(IPersonSet).getByName('software-center-agent')631 self.sca = getUtility(IPersonSet).getByName('software-center-agent')
629632
630 def makeOpenIdIdentifier(self, account, identifier):
631 openid_identifier = OpenIdIdentifier()
632 openid_identifier.identifier = identifier
633 openid_identifier.account = account
634 return IStore(OpenIdIdentifier).add(openid_identifier)
635
636 def test_restricted_to_sca(self):633 def test_restricted_to_sca(self):
637 # Only the software-center-agent celebrity can invoke this634 # Only the software-center-agent celebrity can invoke this
638 # privileged method.635 # privileged method.
@@ -659,7 +656,7 @@
659 def test_finds_by_openid(self):656 def test_finds_by_openid(self):
660 # A Person with the requested OpenID identifier is returned.657 # A Person with the requested OpenID identifier is returned.
661 somebody = self.factory.makePerson()658 somebody = self.factory.makePerson()
662 self.makeOpenIdIdentifier(somebody.account, u'somebody')659 make_openid_identifier(somebody.account, u'somebody')
663 with person_logged_in(self.sca):660 with person_logged_in(self.sca):
664 got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer(661 got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer(
665 self.sca, u'somebody', 'somebody@example.com', 'Example')662 self.sca, u'somebody', 'somebody@example.com', 'Example')
@@ -692,7 +689,7 @@
692 somebody = self.factory.makePerson(689 somebody = self.factory.makePerson(
693 email='existing@example.com',690 email='existing@example.com',
694 account_status=AccountStatus.NOACCOUNT)691 account_status=AccountStatus.NOACCOUNT)
695 self.makeOpenIdIdentifier(somebody.account, u'somebody')692 make_openid_identifier(somebody.account, u'somebody')
696 self.assertEqual(AccountStatus.NOACCOUNT, somebody.account.status)693 self.assertEqual(AccountStatus.NOACCOUNT, somebody.account.status)
697 with person_logged_in(self.sca):694 with person_logged_in(self.sca):
698 got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer(695 got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer(
@@ -725,7 +722,7 @@
725 def test_fails_if_account_is_suspended(self):722 def test_fails_if_account_is_suspended(self):
726 # Suspended accounts cannot be returned.723 # Suspended accounts cannot be returned.
727 somebody = self.factory.makePerson()724 somebody = self.factory.makePerson()
728 self.makeOpenIdIdentifier(somebody.account, u'somebody')725 make_openid_identifier(somebody.account, u'somebody')
729 with admin_logged_in():726 with admin_logged_in():
730 somebody.setAccountStatus(727 somebody.setAccountStatus(
731 AccountStatus.SUSPENDED, None, "Go away!")728 AccountStatus.SUSPENDED, None, "Go away!")
@@ -740,7 +737,7 @@
740 # nor do we want to potentially compromise them with a bad email737 # nor do we want to potentially compromise them with a bad email
741 # address.738 # address.
742 somebody = self.factory.makePerson()739 somebody = self.factory.makePerson()
743 self.makeOpenIdIdentifier(somebody.account, u'somebody')740 make_openid_identifier(somebody.account, u'somebody')
744 with admin_logged_in():741 with admin_logged_in():
745 somebody.setAccountStatus(742 somebody.setAccountStatus(
746 AccountStatus.DEACTIVATED, None, "Goodbye cruel world.")743 AccountStatus.DEACTIVATED, None, "Goodbye cruel world.")
@@ -749,3 +746,149 @@
749 NameAlreadyTaken,746 NameAlreadyTaken,
750 getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer,747 getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer,
751 self.sca, u'somebody', 'somebody@example.com', 'Example')748 self.sca, u'somebody', 'somebody@example.com', 'Example')
749
750
751class TestPersonGetUsernameForSSO(TestCaseWithFactory):
752
753 layer = DatabaseFunctionalLayer
754
755 def setUp(self):
756 super(TestPersonGetUsernameForSSO, self).setUp()
757 self.sso = getUtility(IPersonSet).getByName(u'ubuntu-sso')
758
759 def test_restricted_to_sca(self):
760 # Only the ubuntu-sso celebrity can invoke this
761 # privileged method.
762 target = self.factory.makePerson(name='username')
763 make_openid_identifier(target.account, u'openid')
764
765 def do_it():
766 return getUtility(IPersonSet).getUsernameForSSO(
767 getUtility(ILaunchBag).user, u'openid')
768 random = self.factory.makePerson()
769 admin = self.factory.makePerson(
770 member_of=[getUtility(IPersonSet).getByName(u'admins')])
771
772 # Anonymous, random or admin users can't invoke the method.
773 with anonymous_logged_in():
774 self.assertRaises(Unauthorized, do_it)
775 with person_logged_in(random):
776 self.assertRaises(Unauthorized, do_it)
777 with person_logged_in(admin):
778 self.assertRaises(Unauthorized, do_it)
779
780 with person_logged_in(self.sso):
781 self.assertEqual('username', do_it())
782
783
784class TestPersonSetUsernameFromSSO(TestCaseWithFactory):
785
786 layer = DatabaseFunctionalLayer
787
788 def setUp(self):
789 super(TestPersonSetUsernameFromSSO, self).setUp()
790 self.sso = getUtility(IPersonSet).getByName(u'ubuntu-sso')
791
792 def test_restricted_to_sca(self):
793 # Only the ubuntu-sso celebrity can invoke this
794 # privileged method.
795 def do_it():
796 getUtility(IPersonSet).setUsernameFromSSO(
797 getUtility(ILaunchBag).user, u'openid', u'username')
798 random = self.factory.makePerson()
799 admin = self.factory.makePerson(
800 member_of=[getUtility(IPersonSet).getByName(u'admins')])
801
802 # Anonymous, random or admin users can't invoke the method.
803 with anonymous_logged_in():
804 self.assertRaises(Unauthorized, do_it)
805 with person_logged_in(random):
806 self.assertRaises(Unauthorized, do_it)
807 with person_logged_in(admin):
808 self.assertRaises(Unauthorized, do_it)
809
810 with person_logged_in(self.sso):
811 do_it()
812
813 def test_creates_new_placeholder(self):
814 # If an unknown OpenID identifier and email address are
815 # provided, a new account is created with the given username and
816 # returned.
817 with person_logged_in(self.sso):
818 getUtility(IPersonSet).setUsernameFromSSO(
819 self.sso, u'openid', u'username')
820 person = getUtility(IPersonSet).getByName(u'username')
821 self.assertEqual(u'username', person.name)
822 self.assertEqual(u'username', person.displayname)
823 self.assertEqual(AccountStatus.PLACEHOLDER, person.account.status)
824 with admin_logged_in():
825 self.assertContentEqual(
826 [u'openid'],
827 [oid.identifier for oid in person.account.openid_identifiers])
828 self.assertContentEqual([], person.validatedemails)
829 self.assertContentEqual([], person.guessedemails)
830
831 def test_creates_new_placeholder_dry_run(self):
832 with person_logged_in(self.sso):
833 getUtility(IPersonSet).setUsernameFromSSO(
834 self.sso, u'openid', u'username', dry_run=True)
835 self.assertRaises(
836 LookupError,
837 getUtility(IAccountSet).getByOpenIDIdentifier, u'openid')
838 self.assertIs(None, getUtility(IPersonSet).getByName(u'username'))
839
840 def test_updates_existing_placeholder(self):
841 # An existing placeholder Person with the request OpenID
842 # identifier has its name updated.
843 getUtility(IPersonSet).setUsernameFromSSO(
844 self.sso, u'openid', u'username')
845 person = getUtility(IPersonSet).getByName(u'username')
846
847 # Another call for the same OpenID identifier updates the
848 # existing Person.
849 getUtility(IPersonSet).setUsernameFromSSO(
850 self.sso, u'openid', u'newsername')
851 self.assertEqual(u'newsername', person.name)
852 self.assertEqual(u'newsername', person.displayname)
853 self.assertEqual(AccountStatus.PLACEHOLDER, person.account.status)
854 with admin_logged_in():
855 self.assertContentEqual([], person.validatedemails)
856 self.assertContentEqual([], person.guessedemails)
857
858 def test_updates_existing_placeholder_dry_run(self):
859 getUtility(IPersonSet).setUsernameFromSSO(
860 self.sso, u'openid', u'username')
861 person = getUtility(IPersonSet).getByName(u'username')
862
863 getUtility(IPersonSet).setUsernameFromSSO(
864 self.sso, u'openid', u'newsername', dry_run=True)
865 self.assertEqual(u'username', person.name)
866
867 def test_validation(self, dry_run=False):
868 # An invalid username is rejected with an InvalidName exception.
869 self.assertRaises(
870 InvalidName,
871 getUtility(IPersonSet).setUsernameFromSSO,
872 self.sso, u'openid', u'username!!', dry_run=dry_run)
873 transaction.abort()
874
875 # A username that's already in use is rejected with a
876 # NameAlreadyTaken exception.
877 self.factory.makePerson(name='taken')
878 self.assertRaises(
879 NameAlreadyTaken,
880 getUtility(IPersonSet).setUsernameFromSSO,
881 self.sso, u'openid', u'taken', dry_run=dry_run)
882 transaction.abort()
883
884 # setUsernameFromSSO can't be used to set an OpenID
885 # identifier's username if a non-placeholder account exists.
886 somebody = self.factory.makePerson()
887 make_openid_identifier(somebody.account, u'openid-taken')
888 self.assertRaises(
889 NotPlaceholderAccount,
890 getUtility(IPersonSet).setUsernameFromSSO,
891 self.sso, u'openid-taken', u'username', dry_run=dry_run)
892
893 def test_validation_dry_run(self):
894 self.test_validation(dry_run=True)