Merge lp:~wgrant/launchpad/invisible-person-api into lp:launchpad
- invisible-person-api
- Merge into devel
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 |
Related bugs: |
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
1 | === modified file 'lib/lp/registry/browser/tests/test_person_webservice.py' |
2 | --- lib/lp/registry/browser/tests/test_person_webservice.py 2016-03-21 05:37:40 +0000 |
3 | +++ lib/lp/registry/browser/tests/test_person_webservice.py 2016-05-03 23:49:59 +0000 |
4 | @@ -13,7 +13,10 @@ |
5 | TeamMembershipStatus, |
6 | ) |
7 | from lp.registry.interfaces.teammembership import ITeamMembershipSet |
8 | -from lp.services.identity.interfaces.account import AccountStatus |
9 | +from lp.services.identity.interfaces.account import ( |
10 | + AccountStatus, |
11 | + IAccountSet, |
12 | + ) |
13 | from lp.services.openid.model.openididentifier import OpenIdIdentifier |
14 | from lp.services.webapp import snapshot |
15 | from lp.services.webapp.interfaces import OAuthPermission |
16 | @@ -367,3 +370,114 @@ |
17 | sca = getUtility(IPersonSet).getByName('software-center-agent') |
18 | response = self.getOrCreateSoftwareCenterCustomer(sca) |
19 | self.assertEqual(400, response.status) |
20 | + |
21 | + def test_getUsernameForSSO(self): |
22 | + # canonical-identity-provider (SSO) can get the username for an |
23 | + # OpenID identifier suffix. |
24 | + with admin_logged_in(): |
25 | + sso = getUtility(IPersonSet).getByName('ubuntu-sso') |
26 | + existing = self.factory.makePerson(name='username') |
27 | + taken_openid = ( |
28 | + existing.account.openid_identifiers.any().identifier) |
29 | + webservice = webservice_for_person( |
30 | + sso, permission=OAuthPermission.READ_PUBLIC) |
31 | + response = webservice.named_get( |
32 | + '/people', 'getUsernameForSSO', |
33 | + openid_identifier=taken_openid, api_version='devel') |
34 | + self.assertEqual(200, response.status) |
35 | + self.assertEqual('username', response.jsonBody()) |
36 | + |
37 | + def test_getUsernameForSSO_nonexistent(self): |
38 | + with admin_logged_in(): |
39 | + sso = getUtility(IPersonSet).getByName('ubuntu-sso') |
40 | + webservice = webservice_for_person( |
41 | + sso, permission=OAuthPermission.READ_PUBLIC) |
42 | + response = webservice.named_get( |
43 | + '/people', 'getUsernameForSSO', |
44 | + openid_identifier='doesnotexist', api_version='devel') |
45 | + self.assertEqual(200, response.status) |
46 | + self.assertEqual(None, response.jsonBody()) |
47 | + |
48 | + def setUsernameFromSSO(self, user, openid_identifier, name, |
49 | + dry_run=False): |
50 | + webservice = webservice_for_person( |
51 | + user, permission=OAuthPermission.WRITE_PRIVATE) |
52 | + response = webservice.named_post( |
53 | + '/people', 'setUsernameFromSSO', |
54 | + openid_identifier=openid_identifier, name=name, dry_run=dry_run, |
55 | + api_version='devel') |
56 | + return response |
57 | + |
58 | + def test_setUsernameFromSSO(self): |
59 | + # canonical-identity-provider (SSO) can create a placeholder |
60 | + # Person to give a username to a non-LP user. |
61 | + with admin_logged_in(): |
62 | + sso = getUtility(IPersonSet).getByName('ubuntu-sso') |
63 | + response = self.setUsernameFromSSO(sso, 'foo', 'bar') |
64 | + self.assertEqual(200, response.status) |
65 | + with admin_logged_in(): |
66 | + by_name = getUtility(IPersonSet).getByName('bar') |
67 | + by_openid = getUtility(IPersonSet).getByOpenIDIdentifier( |
68 | + u'http://testopenid.dev/+id/foo') |
69 | + self.assertEqual(by_name, by_openid) |
70 | + self.assertEqual( |
71 | + AccountStatus.PLACEHOLDER, by_name.account_status) |
72 | + |
73 | + def test_setUsernameFromSSO_dry_run(self): |
74 | + # setUsernameFromSSO provides a dry run mode that performs all |
75 | + # the checks but doesn't actually make changes. Useful for input |
76 | + # validation in SSO. |
77 | + with admin_logged_in(): |
78 | + sso = getUtility(IPersonSet).getByName('ubuntu-sso') |
79 | + response = self.setUsernameFromSSO(sso, 'foo', 'bar', dry_run=True) |
80 | + self.assertEqual(200, response.status) |
81 | + with admin_logged_in(): |
82 | + self.assertIs(None, getUtility(IPersonSet).getByName('bar')) |
83 | + self.assertRaises( |
84 | + LookupError, |
85 | + getUtility(IAccountSet).getByOpenIDIdentifier, u'foo') |
86 | + |
87 | + def test_setUsernameFromSSO_is_restricted(self): |
88 | + # The method may only be invoked by the ~ubuntu-sso celebrity |
89 | + # user, as it is security-sensitive. |
90 | + with admin_logged_in(): |
91 | + random = self.factory.makePerson() |
92 | + response = self.setUsernameFromSSO(random, 'foo', 'bar') |
93 | + self.assertEqual(401, response.status) |
94 | + |
95 | + def test_setUsernameFromSSO_rejects_bad_input(self, dry_run=False): |
96 | + # The method returns meaningful errors on bad input, so SSO can |
97 | + # give advice to users. |
98 | + # Check canonical-identity-provider before changing these! |
99 | + with admin_logged_in(): |
100 | + sso = getUtility(IPersonSet).getByName('ubuntu-sso') |
101 | + self.factory.makePerson(name='taken-name') |
102 | + existing = self.factory.makePerson() |
103 | + taken_openid = ( |
104 | + existing.account.openid_identifiers.any().identifier) |
105 | + |
106 | + response = self.setUsernameFromSSO( |
107 | + sso, 'foo', 'taken-name', dry_run=dry_run) |
108 | + self.assertEqual(400, response.status) |
109 | + self.assertEqual( |
110 | + 'name: taken-name is already in use by another person or team.', |
111 | + response.body) |
112 | + |
113 | + response = self.setUsernameFromSSO( |
114 | + sso, 'foo', 'private-name', dry_run=dry_run) |
115 | + self.assertEqual(400, response.status) |
116 | + self.assertEqual( |
117 | + 'name: The name 'private-name' has been blocked by the ' |
118 | + 'Launchpad administrators. Contact Launchpad Support if you want ' |
119 | + 'to use this name.', |
120 | + response.body) |
121 | + |
122 | + response = self.setUsernameFromSSO( |
123 | + sso, taken_openid, 'bar', dry_run=dry_run) |
124 | + self.assertEqual(400, response.status) |
125 | + self.assertEqual( |
126 | + 'An account for that OpenID identifier already exists.', |
127 | + response.body) |
128 | + |
129 | + def test_setUsernameFromSSO_rejects_bad_input_in_dry_run(self): |
130 | + self.test_setUsernameFromSSO_rejects_bad_input(dry_run=True) |
131 | |
132 | === modified file 'lib/lp/registry/errors.py' |
133 | --- lib/lp/registry/errors.py 2012-11-28 21:32:06 +0000 |
134 | +++ lib/lp/registry/errors.py 2016-05-03 23:49:59 +0000 |
135 | @@ -21,6 +21,7 @@ |
136 | 'NameAlreadyTaken', |
137 | 'NoSuchDistroSeries', |
138 | 'NoSuchSourcePackageName', |
139 | + 'NotPlaceholderAccount', |
140 | 'InclusiveTeamLinkageError', |
141 | 'PPACreationError', |
142 | 'PrivatePersonLinkageError', |
143 | @@ -61,6 +62,11 @@ |
144 | |
145 | |
146 | @error_status(httplib.BAD_REQUEST) |
147 | +class NotPlaceholderAccount(Exception): |
148 | + """A non-placeholder account already exists for that OpenID identifier.""" |
149 | + |
150 | + |
151 | +@error_status(httplib.BAD_REQUEST) |
152 | class InvalidFilename(Exception): |
153 | """An invalid filename was used as an attachment filename.""" |
154 | |
155 | |
156 | === modified file 'lib/lp/registry/interfaces/person.py' |
157 | --- lib/lp/registry/interfaces/person.py 2016-04-11 06:38:48 +0000 |
158 | +++ lib/lp/registry/interfaces/person.py 2016-05-03 23:49:59 +0000 |
159 | @@ -2223,6 +2223,51 @@ |
160 | :param full_name: the full name of the user. |
161 | """ |
162 | |
163 | + @call_with(user=REQUEST_USER) |
164 | + @operation_parameters( |
165 | + openid_identifier=TextLine( |
166 | + title=_("OpenID identifier suffix"), required=True)) |
167 | + @export_read_operation() |
168 | + @operation_for_version("devel") |
169 | + def getUsernameForSSO(user, openid_identifier): |
170 | + """Restricted person creation API for SSO. |
171 | + |
172 | + This method can only be called by the Ubuntu SSO service. It |
173 | + finds the username for an account by OpenID identifier. |
174 | + |
175 | + :param user: the `IPerson` performing the operation. Only the |
176 | + ubuntu-sso celebrity is allowed. |
177 | + :param openid_identifier: OpenID identifier suffix for the user. |
178 | + This is *not* the full URL, just the unique suffix portion. |
179 | + """ |
180 | + |
181 | + @call_with(user=REQUEST_USER) |
182 | + @operation_parameters( |
183 | + openid_identifier=TextLine( |
184 | + title=_("OpenID identifier suffix"), required=True), |
185 | + name=copy_field(IPerson['name']), |
186 | + dry_run=Bool(_("Don't save changes"))) |
187 | + @export_write_operation() |
188 | + @operation_for_version("devel") |
189 | + def setUsernameFromSSO(user, openid_identifier, name, dry_run=False): |
190 | + """Restricted person creation API for SSO. |
191 | + |
192 | + This method can only be called by the Ubuntu SSO service. It |
193 | + reserves a username for an account by OpenID identifier, as long as |
194 | + the user has no Launchpad account. |
195 | + |
196 | + :param user: the `IPerson` performing the operation. Only the |
197 | + ubuntu-sso celebrity is allowed. |
198 | + :param openid_identifier: OpenID identifier suffix for the user. |
199 | + This is *not* the full URL, just the unique suffix portion. |
200 | + :param name: the desired username. |
201 | + :raises: `InvalidName` if the username doesn't meet character |
202 | + constraints. |
203 | + :raises: `NameAlreadyTaken` if the username is already in use. |
204 | + :raises: `NotPlaceholderAccount` if the OpenID identifier has a |
205 | + non-placeholder Launchpad account. |
206 | + """ |
207 | + |
208 | @call_with(teamowner=REQUEST_USER) |
209 | @rename_parameters_as( |
210 | teamdescription='team_description', |
211 | |
212 | === modified file 'lib/lp/registry/model/person.py' |
213 | --- lib/lp/registry/model/person.py 2016-04-11 08:10:46 +0000 |
214 | +++ lib/lp/registry/model/person.py 2016-05-03 23:49:59 +0000 |
215 | @@ -154,6 +154,7 @@ |
216 | InvalidName, |
217 | JoinNotAllowed, |
218 | NameAlreadyTaken, |
219 | + NotPlaceholderAccount, |
220 | PPACreationError, |
221 | TeamMembershipPolicyError, |
222 | ) |
223 | @@ -3429,6 +3430,38 @@ |
224 | trust_email=False) |
225 | return person |
226 | |
227 | + def getUsernameForSSO(self, user, openid_identifier): |
228 | + """See `IPersonSet`.""" |
229 | + if user != getUtility(ILaunchpadCelebrities).ubuntu_sso: |
230 | + raise Unauthorized() |
231 | + try: |
232 | + account = getUtility(IAccountSet).getByOpenIDIdentifier( |
233 | + openid_identifier) |
234 | + except LookupError: |
235 | + return None |
236 | + return IPerson(account).name |
237 | + |
238 | + def setUsernameFromSSO(self, user, openid_identifier, name, |
239 | + dry_run=False): |
240 | + """See `IPersonSet`.""" |
241 | + if user != getUtility(ILaunchpadCelebrities).ubuntu_sso: |
242 | + raise Unauthorized() |
243 | + self._validateName(name) |
244 | + try: |
245 | + account = getUtility(IAccountSet).getByOpenIDIdentifier( |
246 | + openid_identifier) |
247 | + except LookupError: |
248 | + if not dry_run: |
249 | + person = self.createPlaceholderPerson(openid_identifier, name) |
250 | + else: |
251 | + if account.status != AccountStatus.PLACEHOLDER: |
252 | + raise NotPlaceholderAccount( |
253 | + "An account for that OpenID identifier already exists.") |
254 | + if not dry_run: |
255 | + account = removeSecurityProxy(account) |
256 | + person = IPerson(account) |
257 | + person.name = person.display_name = account.displayname = name |
258 | + |
259 | def newTeam(self, teamowner, name, display_name, teamdescription=None, |
260 | membership_policy=TeamMembershipPolicy.MODERATED, |
261 | defaultmembershipperiod=None, defaultrenewalperiod=None, |
262 | @@ -3507,9 +3540,7 @@ |
263 | rationale=PersonCreationRationale.USERNAME_PLACEHOLDER, |
264 | comment="when setting a username in SSO", account=account) |
265 | |
266 | - def _newPerson(self, name, displayname, hide_email_addresses, |
267 | - rationale, comment=None, registrant=None, account=None): |
268 | - """Create and return a new Person with the given attributes.""" |
269 | + def _validateName(self, name): |
270 | if not valid_name(name): |
271 | raise InvalidName( |
272 | "%s is not a valid name for a person." % name) |
273 | @@ -3520,6 +3551,11 @@ |
274 | raise NameAlreadyTaken( |
275 | "The name '%s' is already taken." % name) |
276 | |
277 | + def _newPerson(self, name, displayname, hide_email_addresses, |
278 | + rationale, comment=None, registrant=None, account=None): |
279 | + """Create and return a new Person with the given attributes.""" |
280 | + self._validateName(name) |
281 | + |
282 | if not displayname: |
283 | displayname = name.capitalize() |
284 | |
285 | |
286 | === modified file 'lib/lp/registry/tests/test_personset.py' |
287 | --- lib/lp/registry/tests/test_personset.py 2016-04-11 08:10:46 +0000 |
288 | +++ lib/lp/registry/tests/test_personset.py 2016-05-03 23:49:59 +0000 |
289 | @@ -17,6 +17,7 @@ |
290 | |
291 | from lp.code.tests.helpers import remove_all_sample_data_branches |
292 | from lp.registry.errors import ( |
293 | + NotPlaceholderAccount, |
294 | InvalidName, |
295 | NameAlreadyTaken, |
296 | ) |
297 | @@ -41,6 +42,7 @@ |
298 | AccountCreationRationale, |
299 | AccountStatus, |
300 | AccountSuspendedError, |
301 | + IAccountSet, |
302 | ) |
303 | from lp.services.identity.interfaces.emailaddress import ( |
304 | EmailAddressAlreadyTaken, |
305 | @@ -67,6 +69,13 @@ |
306 | from lp.testing.matchers import HasQueryCount |
307 | |
308 | |
309 | +def make_openid_identifier(account, identifier): |
310 | + openid_identifier = OpenIdIdentifier() |
311 | + openid_identifier.identifier = identifier |
312 | + openid_identifier.account = account |
313 | + return IStore(OpenIdIdentifier).add(openid_identifier) |
314 | + |
315 | + |
316 | class TestPersonSet(TestCaseWithFactory): |
317 | """Test `IPersonSet`.""" |
318 | layer = DatabaseFunctionalLayer |
319 | @@ -248,7 +257,7 @@ |
320 | |
321 | # Generate some valid test data. |
322 | self.account = self.makeAccount() |
323 | - self.identifier = self.makeOpenIdIdentifier(self.account, u'whatever') |
324 | + self.identifier = make_openid_identifier(self.account, u'whatever') |
325 | self.person = self.makePerson(self.account) |
326 | self.email = self.makeEmailAddress( |
327 | email='whatever@example.com', person=self.person) |
328 | @@ -259,12 +268,6 @@ |
329 | creation_rationale=AccountCreationRationale.UNKNOWN, |
330 | status=AccountStatus.ACTIVE)) |
331 | |
332 | - def makeOpenIdIdentifier(self, account, identifier): |
333 | - openid_identifier = OpenIdIdentifier() |
334 | - openid_identifier.identifier = identifier |
335 | - openid_identifier.account = account |
336 | - return self.store.add(openid_identifier) |
337 | - |
338 | def makePerson(self, account): |
339 | return self.store.add(Person( |
340 | name='acc%d' % account.id, account=account, |
341 | @@ -627,12 +630,6 @@ |
342 | super(TestPersonSetGetOrCreateSoftwareCenterCustomer, self).setUp() |
343 | self.sca = getUtility(IPersonSet).getByName('software-center-agent') |
344 | |
345 | - def makeOpenIdIdentifier(self, account, identifier): |
346 | - openid_identifier = OpenIdIdentifier() |
347 | - openid_identifier.identifier = identifier |
348 | - openid_identifier.account = account |
349 | - return IStore(OpenIdIdentifier).add(openid_identifier) |
350 | - |
351 | def test_restricted_to_sca(self): |
352 | # Only the software-center-agent celebrity can invoke this |
353 | # privileged method. |
354 | @@ -659,7 +656,7 @@ |
355 | def test_finds_by_openid(self): |
356 | # A Person with the requested OpenID identifier is returned. |
357 | somebody = self.factory.makePerson() |
358 | - self.makeOpenIdIdentifier(somebody.account, u'somebody') |
359 | + make_openid_identifier(somebody.account, u'somebody') |
360 | with person_logged_in(self.sca): |
361 | got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( |
362 | self.sca, u'somebody', 'somebody@example.com', 'Example') |
363 | @@ -692,7 +689,7 @@ |
364 | somebody = self.factory.makePerson( |
365 | email='existing@example.com', |
366 | account_status=AccountStatus.NOACCOUNT) |
367 | - self.makeOpenIdIdentifier(somebody.account, u'somebody') |
368 | + make_openid_identifier(somebody.account, u'somebody') |
369 | self.assertEqual(AccountStatus.NOACCOUNT, somebody.account.status) |
370 | with person_logged_in(self.sca): |
371 | got = getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer( |
372 | @@ -725,7 +722,7 @@ |
373 | def test_fails_if_account_is_suspended(self): |
374 | # Suspended accounts cannot be returned. |
375 | somebody = self.factory.makePerson() |
376 | - self.makeOpenIdIdentifier(somebody.account, u'somebody') |
377 | + make_openid_identifier(somebody.account, u'somebody') |
378 | with admin_logged_in(): |
379 | somebody.setAccountStatus( |
380 | AccountStatus.SUSPENDED, None, "Go away!") |
381 | @@ -740,7 +737,7 @@ |
382 | # nor do we want to potentially compromise them with a bad email |
383 | # address. |
384 | somebody = self.factory.makePerson() |
385 | - self.makeOpenIdIdentifier(somebody.account, u'somebody') |
386 | + make_openid_identifier(somebody.account, u'somebody') |
387 | with admin_logged_in(): |
388 | somebody.setAccountStatus( |
389 | AccountStatus.DEACTIVATED, None, "Goodbye cruel world.") |
390 | @@ -749,3 +746,149 @@ |
391 | NameAlreadyTaken, |
392 | getUtility(IPersonSet).getOrCreateSoftwareCenterCustomer, |
393 | self.sca, u'somebody', 'somebody@example.com', 'Example') |
394 | + |
395 | + |
396 | +class TestPersonGetUsernameForSSO(TestCaseWithFactory): |
397 | + |
398 | + layer = DatabaseFunctionalLayer |
399 | + |
400 | + def setUp(self): |
401 | + super(TestPersonGetUsernameForSSO, self).setUp() |
402 | + self.sso = getUtility(IPersonSet).getByName(u'ubuntu-sso') |
403 | + |
404 | + def test_restricted_to_sca(self): |
405 | + # Only the ubuntu-sso celebrity can invoke this |
406 | + # privileged method. |
407 | + target = self.factory.makePerson(name='username') |
408 | + make_openid_identifier(target.account, u'openid') |
409 | + |
410 | + def do_it(): |
411 | + return getUtility(IPersonSet).getUsernameForSSO( |
412 | + getUtility(ILaunchBag).user, u'openid') |
413 | + random = self.factory.makePerson() |
414 | + admin = self.factory.makePerson( |
415 | + member_of=[getUtility(IPersonSet).getByName(u'admins')]) |
416 | + |
417 | + # Anonymous, random or admin users can't invoke the method. |
418 | + with anonymous_logged_in(): |
419 | + self.assertRaises(Unauthorized, do_it) |
420 | + with person_logged_in(random): |
421 | + self.assertRaises(Unauthorized, do_it) |
422 | + with person_logged_in(admin): |
423 | + self.assertRaises(Unauthorized, do_it) |
424 | + |
425 | + with person_logged_in(self.sso): |
426 | + self.assertEqual('username', do_it()) |
427 | + |
428 | + |
429 | +class TestPersonSetUsernameFromSSO(TestCaseWithFactory): |
430 | + |
431 | + layer = DatabaseFunctionalLayer |
432 | + |
433 | + def setUp(self): |
434 | + super(TestPersonSetUsernameFromSSO, self).setUp() |
435 | + self.sso = getUtility(IPersonSet).getByName(u'ubuntu-sso') |
436 | + |
437 | + def test_restricted_to_sca(self): |
438 | + # Only the ubuntu-sso celebrity can invoke this |
439 | + # privileged method. |
440 | + def do_it(): |
441 | + getUtility(IPersonSet).setUsernameFromSSO( |
442 | + getUtility(ILaunchBag).user, u'openid', u'username') |
443 | + random = self.factory.makePerson() |
444 | + admin = self.factory.makePerson( |
445 | + member_of=[getUtility(IPersonSet).getByName(u'admins')]) |
446 | + |
447 | + # Anonymous, random or admin users can't invoke the method. |
448 | + with anonymous_logged_in(): |
449 | + self.assertRaises(Unauthorized, do_it) |
450 | + with person_logged_in(random): |
451 | + self.assertRaises(Unauthorized, do_it) |
452 | + with person_logged_in(admin): |
453 | + self.assertRaises(Unauthorized, do_it) |
454 | + |
455 | + with person_logged_in(self.sso): |
456 | + do_it() |
457 | + |
458 | + def test_creates_new_placeholder(self): |
459 | + # If an unknown OpenID identifier and email address are |
460 | + # provided, a new account is created with the given username and |
461 | + # returned. |
462 | + with person_logged_in(self.sso): |
463 | + getUtility(IPersonSet).setUsernameFromSSO( |
464 | + self.sso, u'openid', u'username') |
465 | + person = getUtility(IPersonSet).getByName(u'username') |
466 | + self.assertEqual(u'username', person.name) |
467 | + self.assertEqual(u'username', person.displayname) |
468 | + self.assertEqual(AccountStatus.PLACEHOLDER, person.account.status) |
469 | + with admin_logged_in(): |
470 | + self.assertContentEqual( |
471 | + [u'openid'], |
472 | + [oid.identifier for oid in person.account.openid_identifiers]) |
473 | + self.assertContentEqual([], person.validatedemails) |
474 | + self.assertContentEqual([], person.guessedemails) |
475 | + |
476 | + def test_creates_new_placeholder_dry_run(self): |
477 | + with person_logged_in(self.sso): |
478 | + getUtility(IPersonSet).setUsernameFromSSO( |
479 | + self.sso, u'openid', u'username', dry_run=True) |
480 | + self.assertRaises( |
481 | + LookupError, |
482 | + getUtility(IAccountSet).getByOpenIDIdentifier, u'openid') |
483 | + self.assertIs(None, getUtility(IPersonSet).getByName(u'username')) |
484 | + |
485 | + def test_updates_existing_placeholder(self): |
486 | + # An existing placeholder Person with the request OpenID |
487 | + # identifier has its name updated. |
488 | + getUtility(IPersonSet).setUsernameFromSSO( |
489 | + self.sso, u'openid', u'username') |
490 | + person = getUtility(IPersonSet).getByName(u'username') |
491 | + |
492 | + # Another call for the same OpenID identifier updates the |
493 | + # existing Person. |
494 | + getUtility(IPersonSet).setUsernameFromSSO( |
495 | + self.sso, u'openid', u'newsername') |
496 | + self.assertEqual(u'newsername', person.name) |
497 | + self.assertEqual(u'newsername', person.displayname) |
498 | + self.assertEqual(AccountStatus.PLACEHOLDER, person.account.status) |
499 | + with admin_logged_in(): |
500 | + self.assertContentEqual([], person.validatedemails) |
501 | + self.assertContentEqual([], person.guessedemails) |
502 | + |
503 | + def test_updates_existing_placeholder_dry_run(self): |
504 | + getUtility(IPersonSet).setUsernameFromSSO( |
505 | + self.sso, u'openid', u'username') |
506 | + person = getUtility(IPersonSet).getByName(u'username') |
507 | + |
508 | + getUtility(IPersonSet).setUsernameFromSSO( |
509 | + self.sso, u'openid', u'newsername', dry_run=True) |
510 | + self.assertEqual(u'username', person.name) |
511 | + |
512 | + def test_validation(self, dry_run=False): |
513 | + # An invalid username is rejected with an InvalidName exception. |
514 | + self.assertRaises( |
515 | + InvalidName, |
516 | + getUtility(IPersonSet).setUsernameFromSSO, |
517 | + self.sso, u'openid', u'username!!', dry_run=dry_run) |
518 | + transaction.abort() |
519 | + |
520 | + # A username that's already in use is rejected with a |
521 | + # NameAlreadyTaken exception. |
522 | + self.factory.makePerson(name='taken') |
523 | + self.assertRaises( |
524 | + NameAlreadyTaken, |
525 | + getUtility(IPersonSet).setUsernameFromSSO, |
526 | + self.sso, u'openid', u'taken', dry_run=dry_run) |
527 | + transaction.abort() |
528 | + |
529 | + # setUsernameFromSSO can't be used to set an OpenID |
530 | + # identifier's username if a non-placeholder account exists. |
531 | + somebody = self.factory.makePerson() |
532 | + make_openid_identifier(somebody.account, u'openid-taken') |
533 | + self.assertRaises( |
534 | + NotPlaceholderAccount, |
535 | + getUtility(IPersonSet).setUsernameFromSSO, |
536 | + self.sso, u'openid-taken', u'username', dry_run=dry_run) |
537 | + |
538 | + def test_validation_dry_run(self): |
539 | + self.test_validation(dry_run=True) |