Merge lp:~maxiberta/launchpad/named-auth-tokens into lp:launchpad

Proposed by Maximiliano Bertacchini
Status: Merged
Merged at revision: 18134
Proposed branch: lp:~maxiberta/launchpad/named-auth-tokens
Merge into: lp:launchpad
Diff against target: 624 lines (+305/-33)
8 files modified
lib/lp/services/features/flags.py (+6/-0)
lib/lp/soyuz/interfaces/archive.py (+84/-6)
lib/lp/soyuz/interfaces/archiveauthtoken.py (+35/-9)
lib/lp/soyuz/model/archive.py (+58/-0)
lib/lp/soyuz/model/archiveauthtoken.py (+23/-9)
lib/lp/soyuz/templates/person-archive-subscription.pt (+1/-1)
lib/lp/soyuz/tests/test_archive.py (+94/-5)
lib/lp/testing/__init__.py (+4/-3)
To merge this branch: bzr merge lp:~maxiberta/launchpad/named-auth-tokens
Reviewer Review Type Date Requested Status
Colin Watson (community) Approve
Review via email: mp+299432@code.launchpad.net

Commit message

Add new named ArchiveAuthToken API.

Description of the change

Add new named ArchiveAuthToken API. This is required for UA customer delivery of kernel livepatches. See required DB changes in https://code.launchpad.net/~maxiberta/launchpad/db-named-auth-tokens/+merge/299433 .

Still missing:
* Hide behind a feature flag (to submit in a different MP, if that's ok).

Also:
* Use "authorize" instead of "authorise" in Archive related classes.
* Some extra cleanups.

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

Good start, thanks, but some bits to fix up.

review: Needs Fixing
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :

Will fix asap. Thanks!

Revision history for this message
Maximiliano Bertacchini (maxiberta) :
Revision history for this message
Colin Watson (cjwatson) :
Revision history for this message
Maximiliano Bertacchini (maxiberta) wrote :

Updated with feature flag + fixes and improvements.

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/services/features/flags.py'
2--- lib/lp/services/features/flags.py 2016-01-25 12:21:31 +0000
3+++ lib/lp/services/features/flags.py 2016-07-07 21:32:46 +0000
4@@ -227,6 +227,12 @@
5 'disabled',
6 'Git recipes',
7 'https://help.launchpad.net/Packaging/SourceBuilds'),
8+ ('soyuz.named_auth_token.allow_new',
9+ 'boolean',
10+ 'If true, allow creation of named authorization tokens for archives.',
11+ 'disabled',
12+ 'Named authorization tokens for archives',
13+ ''),
14 ])
15
16 # The set of all flag names that are documented.
17
18=== modified file 'lib/lp/soyuz/interfaces/archive.py'
19--- lib/lp/soyuz/interfaces/archive.py 2016-01-26 15:47:37 +0000
20+++ lib/lp/soyuz/interfaces/archive.py 2016-07-07 21:32:46 +0000
21@@ -20,6 +20,7 @@
22 'CannotUploadToPPA',
23 'CannotUploadToPocket',
24 'CannotUploadToSeries',
25+ 'DuplicateTokenName',
26 'FULL_COMPONENT_SUPPORT',
27 'IArchive',
28 'IArchiveAdmin',
29@@ -38,6 +39,8 @@
30 'InvalidPocketForPPA',
31 'IPPA',
32 'MAIN_ARCHIVE_PURPOSES',
33+ 'NAMED_AUTH_TOKEN_FEATURE_FLAG',
34+ 'NamedAuthTokenFeatureDisabled',
35 'NoRightsForArchive',
36 'NoRightsForComponent',
37 'NoSuchPPA',
38@@ -92,6 +95,7 @@
39 Text,
40 TextLine,
41 )
42+from zope.security.interfaces import Unauthorized
43
44 from lp import _
45 from lp.app.errors import NameLookupFailed
46@@ -111,6 +115,9 @@
47 from lp.soyuz.interfaces.component import IComponent
48
49
50+NAMED_AUTH_TOKEN_FEATURE_FLAG = u"soyuz.named_auth_token.allow_new"
51+
52+
53 @error_status(httplib.BAD_REQUEST)
54 class ArchiveDependencyError(Exception):
55 """Raised when an `IArchiveDependency` does not fit the context archive.
56@@ -312,6 +319,21 @@
57 self._fmt % {'processor': processor.name})
58
59
60+@error_status(httplib.CONFLICT)
61+class DuplicateTokenName(Exception):
62+ """Raised when creating a named token and an active token for this archive
63+ with this name already exists."""
64+
65+
66+@error_status(httplib.UNAUTHORIZED)
67+class NamedAuthTokenFeatureDisabled(Unauthorized):
68+ """Only certain users can create named authorization tokens."""
69+
70+ def __init__(self):
71+ super(NamedAuthTokenFeatureDisabled, self).__init__(
72+ "You do not have permission to create named authorization tokens")
73+
74+
75 class IArchivePublic(IPrivacy, IHasOwner):
76 """An Archive interface for publicly available operations."""
77 # Most of this stuff should really be on View, but it's needed for
78@@ -526,12 +548,12 @@
79 """
80
81 def newAuthToken(person, token=None, date_created=None):
82- """Create a new authorisation token.
83+ """Create a new authorization token.
84
85- :param person: An IPerson whom this token is for
86+ :param person: An IPerson whom this token is for.
87 :param token: Optional unicode text to use as the token. One will be
88- generated if not given
89- :param date_created: Optional, defaults to now
90+ generated if not given.
91+ :param date_created: Optional, defaults to now.
92
93 :return: A new IArchiveAuthToken
94 """
95@@ -1324,7 +1346,7 @@
96 @operation_returns_collection_of(Interface)
97 @export_read_operation()
98 def getQueueAdminsForComponent(component_name):
99- """Return `IArchivePermission` records for authorised queue admins.
100+ """Return `IArchivePermission` records for authorized queue admins.
101
102 :param component_name: An `IComponent` or textual name for the
103 component.
104@@ -1377,7 +1399,7 @@
105 @export_read_operation()
106 @operation_for_version("devel")
107 def getQueueAdminsForPocket(pocket, distroseries=None):
108- """Return `IArchivePermission` records for authorised queue admins.
109+ """Return `IArchivePermission` records for authorized queue admins.
110
111 :param pocket: A `PackagePublishingPocket`.
112 :param distroseries: An optional `IDistroSeries`.
113@@ -2062,6 +2084,7 @@
114 :return: a `IArchiveDependency` object targeted to the context
115 `IArchive` requiring 'dependency' `IArchive`.
116 """
117+
118 @operation_parameters(
119 dependency=Reference(schema=Interface, required=True),
120 # Really IArchive
121@@ -2074,6 +2097,61 @@
122 :param dependency: is an `IArchive` object.
123 """
124
125+ @operation_parameters(
126+ name=TextLine(title=_("Authorization token name"), required=True),
127+ token=TextLine(
128+ title=_("Optional secret for this named token"), required=False))
129+ @export_write_operation()
130+ @operation_for_version("devel")
131+ def newNamedAuthToken(name, token=None):
132+ """Create a new named authorization token.
133+
134+ :param name: An identifier string for this token.
135+ :param token: Optional unicode text to use as the token. One will be
136+ generated if not given.
137+
138+ :return: A dictionary where the value of `token` is the secret and
139+ the value of `archive_url` is the externally-usable archive URL
140+ including basic auth.
141+ """
142+
143+ @operation_parameters(
144+ name=TextLine(title=_("Authorization token name"), required=True))
145+ @export_read_operation()
146+ @operation_for_version("devel")
147+ def getNamedAuthToken(name):
148+ """Return a named authorization token for the given name in this
149+ archive.
150+
151+ :param name: The identifier string for a token.
152+
153+ :return: A dictionary where the value of `token` is the secret and
154+ the value of `archive_url` is the externally-usable archive URL
155+ including basic auth.
156+ :raises NotFoundError: if no matching token could be found.
157+ """
158+
159+ @export_read_operation()
160+ @operation_for_version("devel")
161+ def getNamedAuthTokens():
162+ """Return a list of named authorization tokens for this archive.
163+
164+ :return: A list of dictionaries where the value of `token` is the
165+ secret and the value of `archive_url` is the externally-usable
166+ archive URL including basic auth.
167+ """
168+
169+ @operation_parameters(
170+ name=TextLine(title=_("Authorization token name"), required=True))
171+ @export_write_operation()
172+ @operation_for_version("devel")
173+ def revokeNamedAuthToken(name):
174+ """Deactivates a named authorization token.
175+
176+ :param name: The identifier string for a token.
177+ :raises NotFoundError: if no matching token could be found.
178+ """
179+
180
181 class IArchiveAdmin(Interface):
182 """Archive interface for operations restricted by commercial."""
183
184=== modified file 'lib/lp/soyuz/interfaces/archiveauthtoken.py'
185--- lib/lp/soyuz/interfaces/archiveauthtoken.py 2013-01-07 02:40:55 +0000
186+++ lib/lp/soyuz/interfaces/archiveauthtoken.py 2016-07-07 21:32:46 +0000
187@@ -27,16 +27,16 @@
188
189
190 class IArchiveAuthTokenView(Interface):
191- """Interface for Archive Authorisation Tokens requiring launchpad.View."""
192+ """Interface for Archive Authorization Tokens requiring launchpad.View."""
193 id = Int(title=_('ID'), required=True, readonly=True)
194
195 archive = Reference(
196 IArchive, title=_("Archive"), required=True, readonly=True,
197- description=_("The archive for this authorisation token."))
198+ description=_("The archive for this authorization token."))
199
200 person = Reference(
201- IPerson, title=_("Person"), required=True, readonly=True,
202- description=_("The person for this authorisation token."))
203+ IPerson, title=_("Person"), required=False, readonly=True,
204+ description=_("The person for this authorization token."))
205 person_id = Attribute('db person value')
206
207 date_created = Datetime(
208@@ -56,9 +56,20 @@
209 description=_(
210 "External archive URL including basic auth for this person"))
211
212- def deactivate(self):
213+ name = TextLine(
214+ title=_("Name"), required=False, readonly=True,
215+ description=_(
216+ "The name in the case of a named authorization token, or None."))
217+
218+ def deactivate():
219 """Deactivate the token by setting date_deactivated to UTC_NOW."""
220
221+ def asDict():
222+ """Returns a dictionary where the value of `token` is the secret and
223+ the value of `archive_url` is the externally-usable archive URL
224+ including basic auth.
225+ """
226+
227
228 class IArchiveAuthTokenEdit(Interface):
229 """Interface for Archive Auth Tokens requiring launchpad.Edit."""
230@@ -74,15 +85,15 @@
231 def get(token_id):
232 """Retrieve a token by its database ID.
233
234- :param token_id: The database ID
235- :return: An object conforming to IArchiveAuthToken
236+ :param token_id: The database ID.
237+ :return: An object conforming to `IArchiveAuthToken`.
238 """
239
240 def getByToken(token):
241 """Retrieve a token by its token text.
242
243 :param token: The token text for the token.
244- :return: An object conforming to IArchiveAuthToken
245+ :return: An object conforming to `IArchiveAuthToken`.
246 """
247
248 def getByArchive(archive):
249@@ -97,5 +108,20 @@
250
251 :param archive: The archive to which the token corresponds.
252 :param person: The person to which the token corresponds.
253- :return An object conforming to IArchiveAuthToken or None.
254+ :return: An `IArchiveAuthToken` or None.
255+ """
256+
257+ def getActiveNamedTokenForArchive(archive, name):
258+ """Retrieve an active named token for the given archive and name.
259+
260+ :param archive: The archive to which the token corresponds.
261+ :param name: The name of a named authorization token.
262+ :return An object conforming to `IArchiveAuthToken` or None.
263+ """
264+
265+ def getActiveNamedTokensForArchive(archive):
266+ """Retrieve all active named tokens for the given archive.
267+
268+ :param archive: The archive to which the tokens correspond.
269+ :return: A result set containing `IArchiveAuthToken`s.
270 """
271
272=== modified file 'lib/lp/soyuz/model/archive.py'
273--- lib/lp/soyuz/model/archive.py 2016-03-14 23:42:45 +0000
274+++ lib/lp/soyuz/model/archive.py 2016-07-07 21:32:46 +0000
275@@ -110,6 +110,7 @@
276 sqlvalues,
277 )
278 from lp.services.database.stormexpr import BulkUpdate
279+from lp.services.features import getFeatureFlag
280 from lp.services.job.interfaces.job import JobStatus
281 from lp.services.librarian.model import (
282 LibraryFileAlias,
283@@ -149,6 +150,7 @@
284 CannotUploadToSeries,
285 ComponentNotFound,
286 default_name_by_purpose,
287+ DuplicateTokenName,
288 FULL_COMPONENT_SUPPORT,
289 IArchive,
290 IArchiveSet,
291@@ -160,6 +162,8 @@
292 InvalidPocketForPPA,
293 IPPA,
294 MAIN_ARCHIVE_PURPOSES,
295+ NAMED_AUTH_TOKEN_FEATURE_FLAG,
296+ NamedAuthTokenFeatureDisabled,
297 NoRightsForArchive,
298 NoRightsForComponent,
299 NoSuchPPA,
300@@ -1948,6 +1952,60 @@
301 IStore(ArchiveAuthToken).add(archive_auth_token)
302 return archive_auth_token
303
304+ def newNamedAuthToken(self, name, token=None):
305+ """See `IArchive`."""
306+
307+ if not getFeatureFlag(NAMED_AUTH_TOKEN_FEATURE_FLAG):
308+ raise NamedAuthTokenFeatureDisabled()
309+
310+ # Bail if the archive isn't private
311+ if not self.private:
312+ raise ArchiveNotPrivate("Archive must be private.")
313+
314+ try:
315+ # Check for duplicate name.
316+ self.getNamedAuthToken(name)
317+ raise DuplicateTokenName(
318+ "An active token with name %s for archive %s already exists." %
319+ (name, self.displayname))
320+ except NotFoundError:
321+ # No duplicate name found: continue.
322+ pass
323+
324+ # Now onto the actual token creation:
325+ if token is None:
326+ token = create_token(20)
327+ archive_auth_token = ArchiveAuthToken()
328+ archive_auth_token.archive = self
329+ archive_auth_token.name = name
330+ archive_auth_token.token = token
331+ IStore(ArchiveAuthToken).add(archive_auth_token)
332+ return archive_auth_token.asDict()
333+
334+ def getNamedAuthToken(self, name):
335+ """See `IArchive`."""
336+ token_set = getUtility(IArchiveAuthTokenSet)
337+ auth_token = token_set.getActiveNamedTokenForArchive(self, name)
338+ if auth_token is not None:
339+ return auth_token.asDict()
340+ else:
341+ raise NotFoundError(name)
342+
343+ def getNamedAuthTokens(self):
344+ """See `IArchive`."""
345+ token_set = getUtility(IArchiveAuthTokenSet)
346+ auth_tokens = token_set.getActiveNamedTokensForArchive(self)
347+ return [auth_token.asDict() for auth_token in auth_tokens]
348+
349+ def revokeNamedAuthToken(self, name):
350+ """See `IArchive`."""
351+ token_set = getUtility(IArchiveAuthTokenSet)
352+ auth_token = token_set.getActiveNamedTokenForArchive(self, name)
353+ if auth_token is not None:
354+ auth_token.deactivate()
355+ else:
356+ raise NotFoundError(name)
357+
358 def newSubscription(self, subscriber, registrant, date_expires=None,
359 description=None):
360 """See `IArchive`."""
361
362=== modified file 'lib/lp/soyuz/model/archiveauthtoken.py'
363--- lib/lp/soyuz/model/archiveauthtoken.py 2015-10-21 09:37:08 +0000
364+++ lib/lp/soyuz/model/archiveauthtoken.py 2016-07-07 21:32:46 +0000
365@@ -39,7 +39,7 @@
366 archive_id = Int(name='archive', allow_none=False)
367 archive = Reference(archive_id, 'Archive.id')
368
369- person_id = Int(name='person', allow_none=False)
370+ person_id = Int(name='person', allow_none=True)
371 person = Reference(person_id, 'Person.id')
372
373 date_created = DateTime(
374@@ -50,6 +50,8 @@
375
376 token = Unicode(name='token', allow_none=False)
377
378+ name = Unicode(name='name', allow_none=True)
379+
380 def deactivate(self):
381 """See `IArchiveAuthTokenSet`."""
382 self.date_deactivated = UTC_NOW
383@@ -58,10 +60,16 @@
384 def archive_url(self):
385 """Return a custom archive url for basic authentication."""
386 normal_url = URI(self.archive.archive_url)
387- auth_url = normal_url.replace(
388- userinfo="%s:%s" % (self.person.name, self.token))
389+ if self.name:
390+ name = '+' + self.name
391+ else:
392+ name = self.person.name
393+ auth_url = normal_url.replace(userinfo="%s:%s" % (name, self.token))
394 return str(auth_url)
395
396+ def asDict(self):
397+ return {"token": self.token, "archive_url": self.archive_url}
398+
399
400 @implementer(IArchiveAuthTokenSet)
401 class ArchiveAuthTokenSet:
402@@ -87,9 +95,15 @@
403
404 def getActiveTokenForArchiveAndPerson(self, archive, person):
405 """See `IArchiveAuthTokenSet`."""
406- store = Store.of(archive)
407- return store.find(
408- ArchiveAuthToken,
409- ArchiveAuthToken.archive == archive,
410- ArchiveAuthToken.person == person,
411- ArchiveAuthToken.date_deactivated == None).one()
412+ return self.getByArchive(archive).find(
413+ ArchiveAuthToken.person == person).one()
414+
415+ def getActiveNamedTokenForArchive(self, archive, name):
416+ """See `IArchiveAuthTokenSet`."""
417+ return self.getByArchive(archive).find(
418+ ArchiveAuthToken.name == name).one()
419+
420+ def getActiveNamedTokensForArchive(self, archive):
421+ """See `IArchiveAuthTokenSet`."""
422+ return self.getByArchive(archive).find(
423+ ArchiveAuthToken.name != None)
424
425=== modified file 'lib/lp/soyuz/templates/person-archive-subscription.pt'
426--- lib/lp/soyuz/templates/person-archive-subscription.pt 2012-03-01 18:17:56 +0000
427+++ lib/lp/soyuz/templates/person-archive-subscription.pt 2016-07-07 21:32:46 +0000
428@@ -50,7 +50,7 @@
429 <div id="regenerate_token" class="portlet" style="clear:both">
430 <h2>Reset password</h2>
431 <p>If you believe the security of your password for this access
432- has been compromised, you reset your password. After you've
433+ has been compromised, you should reset your password. After you've
434 requested a new password, you'll see new "sources.list" entries
435 on this page. You'll need to update them on your computer.
436 </p>
437
438=== modified file 'lib/lp/soyuz/tests/test_archive.py'
439--- lib/lp/soyuz/tests/test_archive.py 2016-04-07 00:04:42 +0000
440+++ lib/lp/soyuz/tests/test_archive.py 2016-07-07 21:32:46 +0000
441@@ -41,6 +41,7 @@
442 from lp.registry.interfaces.teammembership import TeamMembershipStatus
443 from lp.services.database.interfaces import IStore
444 from lp.services.database.sqlbase import sqlvalues
445+from lp.services.features.testing import FeatureFixture
446 from lp.services.job.interfaces.job import JobStatus
447 from lp.services.propertycache import (
448 clear_property_cache,
449@@ -65,21 +66,26 @@
450 from lp.soyuz.interfaces.archive import (
451 ArchiveDependencyError,
452 ArchiveDisabled,
453+ ArchiveNotPrivate,
454 CannotCopy,
455 CannotModifyArchiveProcessor,
456 CannotUploadToPocket,
457 CannotUploadToPPA,
458 CannotUploadToSeries,
459+ DuplicateTokenName,
460 IArchiveSet,
461 InsufficientUploadRights,
462 InvalidPocketForPartnerArchive,
463 InvalidPocketForPPA,
464+ NAMED_AUTH_TOKEN_FEATURE_FLAG,
465+ NamedAuthTokenFeatureDisabled,
466 NoRightsForArchive,
467 NoRightsForComponent,
468 NoSuchPPA,
469 RedirectedPocket,
470 VersionRequiresName,
471 )
472+from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet
473 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
474 from lp.soyuz.interfaces.binarypackagebuild import BuildSetStatus
475 from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet
476@@ -1246,6 +1252,23 @@
477 self.assertEqual("really secret", self.archive.buildd_secret)
478
479
480+class TestNamedAuthTokenFeatureFlag(TestCaseWithFactory):
481+ layer = LaunchpadZopelessLayer
482+
483+ def test_feature_flag_disabled(self):
484+ # With feature flag disabled, we will not create new named auth tokens.
485+ private_ppa = self.factory.makeArchive(private=True)
486+ with FeatureFixture({NAMED_AUTH_TOKEN_FEATURE_FLAG: u""}):
487+ self.assertRaises(NamedAuthTokenFeatureDisabled,
488+ private_ppa.newNamedAuthToken, u"tokenname")
489+
490+ def test_feature_flag_disabled_by_default(self):
491+ # Without a feature flag, we will not create new named auth tokens.
492+ private_ppa = self.factory.makeArchive(private=True)
493+ self.assertRaises(NamedAuthTokenFeatureDisabled,
494+ private_ppa.newNamedAuthToken, u"tokenname")
495+
496+
497 class TestArchiveTokens(TestCaseWithFactory):
498 layer = LaunchpadZopelessLayer
499
500@@ -1255,13 +1278,14 @@
501 self.private_ppa = self.factory.makeArchive(owner=owner, private=True)
502 self.joe = self.factory.makePerson(name='joe')
503 self.private_ppa.newSubscription(self.joe, owner)
504+ self.useFixture(FeatureFixture({NAMED_AUTH_TOKEN_FEATURE_FLAG: u"on"}))
505
506 def test_getAuthToken_with_no_token(self):
507- token = self.private_ppa.getAuthToken(self.joe)
508- self.assertEqual(token, None)
509+ self.assertIsNone(self.private_ppa.getAuthToken(self.joe))
510
511 def test_getAuthToken_with_token(self):
512 token = self.private_ppa.newAuthToken(self.joe)
513+ self.assertIsNone(token.name)
514 self.assertEqual(self.private_ppa.getAuthToken(self.joe), token)
515
516 def test_getArchiveSubscriptionURL(self):
517@@ -1269,6 +1293,74 @@
518 token = self.private_ppa.getAuthToken(self.joe)
519 self.assertEqual(token.archive_url, url)
520
521+ def test_newNamedAuthToken_private_archive(self):
522+ res = self.private_ppa.newNamedAuthToken(u"tokenname")
523+ token = getUtility(IArchiveAuthTokenSet).getActiveNamedTokenForArchive(
524+ self.private_ppa, u"tokenname")
525+ self.assertIsNotNone(token)
526+ self.assertIsNone(token.person)
527+ self.assertEqual("tokenname", token.name)
528+ self.assertIsNotNone(token.token)
529+ self.assertEqual(self.private_ppa, token.archive)
530+ self.assertIn(
531+ "://+%s:%s@" % (token.name, token.token), token.archive_url)
532+ self.assertDictEqual(
533+ {"token": token.token, "archive_url": token.archive_url},
534+ res
535+ )
536+
537+ def test_newNamedAuthToken_public_archive(self):
538+ public_ppa = self.factory.makeArchive(private=False)
539+ self.assertRaises(ArchiveNotPrivate,
540+ public_ppa.newNamedAuthToken, u"tokenname")
541+
542+ def test_newNamedAuthToken_duplicate_name(self):
543+ self.private_ppa.newNamedAuthToken(u"tokenname")
544+ self.assertRaises(DuplicateTokenName,
545+ self.private_ppa.newNamedAuthToken, u"tokenname")
546+
547+ def test_newNamedAuthToken_with_custom_secret(self):
548+ self.private_ppa.newNamedAuthToken(u"tokenname", u"somesecret")
549+ token = getUtility(IArchiveAuthTokenSet).getActiveNamedTokenForArchive(
550+ self.private_ppa, u"tokenname")
551+ self.assertEqual(u"somesecret", token.token)
552+
553+ def test_getNamedAuthToken_with_no_token(self):
554+ self.assertRaises(
555+ NotFoundError, self.private_ppa.getNamedAuthToken, u"tokenname")
556+
557+ def test_getNamedAuthToken_with_token(self):
558+ res = self.private_ppa.newNamedAuthToken(u"tokenname")
559+ self.assertEqual(
560+ self.private_ppa.getNamedAuthToken(u"tokenname"),
561+ res)
562+
563+ def test_revokeNamedAuthToken_with_token(self):
564+ self.private_ppa.newNamedAuthToken(u"tokenname")
565+ token = getUtility(IArchiveAuthTokenSet).getActiveNamedTokenForArchive(
566+ self.private_ppa, u"tokenname")
567+ self.private_ppa.revokeNamedAuthToken(u"tokenname")
568+ self.assertIsNotNone(token.date_deactivated)
569+
570+ def test_revokeNamedAuthToken_with_no_token(self):
571+ self.assertRaises(
572+ NotFoundError, self.private_ppa.revokeNamedAuthToken, u"tokenname")
573+
574+ def test_getNamedAuthToken_with_revoked_token(self):
575+ self.private_ppa.newNamedAuthToken(u"tokenname")
576+ self.private_ppa.revokeNamedAuthToken(u"tokenname")
577+ self.assertRaises(
578+ NotFoundError, self.private_ppa.getNamedAuthToken, u"tokenname")
579+
580+ def test_getNamedAuthTokens(self):
581+ res1 = self.private_ppa.newNamedAuthToken(u"tokenname1")
582+ res2 = self.private_ppa.newNamedAuthToken(u"tokenname2")
583+ self.private_ppa.newNamedAuthToken(u"tokenname3")
584+ self.private_ppa.revokeNamedAuthToken(u"tokenname3")
585+ self.assertContentEqual(
586+ [res1, res2],
587+ self.private_ppa.getNamedAuthTokens())
588+
589
590 class TestGetBinaryPackageRelease(TestCaseWithFactory):
591 """Ensure that getBinaryPackageRelease works as expected."""
592@@ -3505,9 +3597,6 @@
593
594 layer = LaunchpadFunctionalLayer
595
596- def assertDictEqual(self, one, two):
597- self.assertContentEqual(one.items(), two.items())
598-
599 def test_cprov_build_counters_in_sampledata(self):
600 cprov_archive = getUtility(IPersonSet).getByName("cprov").archive
601 expected_counters = {
602
603=== modified file 'lib/lp/testing/__init__.py'
604--- lib/lp/testing/__init__.py 2015-11-08 01:05:24 +0000
605+++ lib/lp/testing/__init__.py 2016-07-07 21:32:46 +0000
606@@ -619,14 +619,15 @@
607 self.assertIsNot(
608 None, pattern.search(normalise_whitespace(text)), text)
609
610- def assertIsInstance(self, instance, assert_class):
611+ def assertIsInstance(self, instance, assert_class, msg=None):
612 """Assert that an instance is an instance of assert_class.
613
614 instance and assert_class have the same semantics as the parameters
615 to isinstance.
616 """
617- self.assertTrue(zope_isinstance(instance, assert_class),
618- '%r is not an instance of %r' % (instance, assert_class))
619+ if msg is None:
620+ msg = '%r is not an instance of %r' % (instance, assert_class)
621+ self.assertTrue(zope_isinstance(instance, assert_class), msg)
622
623 def assertIsNot(self, expected, observed, msg=None):
624 """Assert that `expected` is not the same object as `observed`."""