Merge lp:~aym-frikha/maas/add-apikey-name-api into lp:~maas-committers/maas/trunk
- add-apikey-name-api
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Mike Pontillo | ||||
Approved revision: | no longer in the source branch. | ||||
Merged at revision: | 5465 | ||||
Proposed branch: | lp:~aym-frikha/maas/add-apikey-name-api | ||||
Merge into: | lp:~maas-committers/maas/trunk | ||||
Diff against target: |
528 lines (+336/-22) 6 files modified
src/maasserver/api/account.py (+61/-8) src/maasserver/api/tests/test_api.py (+144/-5) src/maasserver/management/commands/apikey.py (+52/-5) src/maasserver/models/user.py (+6/-2) src/maasserver/models/userprofile.py (+17/-2) src/maasserver/tests/test_commands.py (+56/-0) |
||||
To merge this branch: | bzr merge lp:~aym-frikha/maas/add-apikey-name-api | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Mike Pontillo (community) | Approve | ||
Review via email: mp+307740@code.launchpad.net |
Commit message
API keys can have now names assigned to them.
* Users are now able to generate new API with a name, using:
[API] maas ubuntu account create-
[CLI] maas apikey --username=ubuntu --generate --name TestKey
* Users can also update existing API keys name, using:
[API] maas ubuntu account update-token-name token={{token key}} name=TestKey
[CLI] maas apikey --username=ubuntu --update {{token key}} --name TestKey
* Users are now able to list API keys with their names, using:
[API] maas ubuntu account list-authorisat
[CLI] maas apikey --username=ubuntu --with-names
If a new API key is generated without any name, the existing default name (which was never exposed to the user before) is assigned to it: "MAAS Consumer". (All existing keys will also have this name assigned to them.)
Description of the change
Mike Pontillo (mpontillo) wrote : | # |
I think this branch is looking really good. Just a few more minor nits below, mostly with regard to code style.
Mike Pontillo (mpontillo) wrote : | # |
I just finished testing this branch and I think it's looking great. Here's a transcript.
http://
There is one more thing I would change before I think it's okay to land this: we should swap the order of the output in "maas apikey --username admin --with-names". For example, right now I see:
MAAS consumer 9bU8Xw8GqvAEyrf
Oh, I just remembered. kT85sFzqcNTckx6
It would be easier for someone writing a script to parse this if we place the name *after* the key, since then you always know that everything after the first space is the key name (since key names can contain spaces).
So I would rather the output look like this:
9bU8Xw8GqvAEyrf
kT85sFzqcNTckx6
It also looks better this way since the keys line up.
Thank you for your hard work and dedication on this.
Preview Diff
1 | === modified file 'src/maasserver/api/account.py' |
2 | --- src/maasserver/api/account.py 2016-08-09 21:59:28 +0000 |
3 | +++ src/maasserver/api/account.py 2016-10-09 22:36:13 +0000 |
4 | @@ -5,7 +5,7 @@ |
5 | |
6 | __all__ = [ |
7 | 'AccountHandler', |
8 | - ] |
9 | +] |
10 | |
11 | import http.client |
12 | import json |
13 | @@ -15,10 +15,27 @@ |
14 | operation, |
15 | OperationsHandler, |
16 | ) |
17 | -from maasserver.api.utils import get_mandatory_param |
18 | +from maasserver.api.utils import ( |
19 | + get_mandatory_param, |
20 | + get_optional_param, |
21 | +) |
22 | from piston3.utils import rc |
23 | |
24 | |
25 | +def _format_tokens(tokens): |
26 | + """Converts the given tokens into a list of dictionaries to represent them. |
27 | + |
28 | + :param tokens: The result of `profile.get_authorisation_tokens()`. |
29 | + """ |
30 | + return [ |
31 | + { |
32 | + "name": token.consumer.name, |
33 | + "token": ":".join([token.consumer.key, token.key, token.secret]) |
34 | + } |
35 | + for token in tokens |
36 | + ] |
37 | + |
38 | + |
39 | class AccountHandler(OperationsHandler): |
40 | """Manage the current logged-in user.""" |
41 | api_doc_section_name = "Logged-in user" |
42 | @@ -28,19 +45,22 @@ |
43 | def create_authorisation_token(self, request): |
44 | """Create an authorisation OAuth token and OAuth consumer. |
45 | |
46 | - :return: a json dict with three keys: 'token_key', |
47 | - 'token_secret' and 'consumer_key' (e.g. |
48 | + :param name: Optional name of the token that will be generated. |
49 | + :type name: unicode |
50 | + :return: a json dict with four keys: 'token_key', |
51 | + 'token_secret', 'consumer_key' and 'name'(e.g. |
52 | {token_key: 's65244576fgqs', token_secret: 'qsdfdhv34', |
53 | - consumer_key: '68543fhj854fg'}). |
54 | + consumer_key: '68543fhj854fg', name: 'MAAS consumer'}). |
55 | :rtype: string (json) |
56 | |
57 | """ |
58 | profile = request.user.userprofile |
59 | - consumer, token = profile.create_authorisation_token() |
60 | + consumer_name = get_optional_param(request.data, 'name') |
61 | + consumer, token = profile.create_authorisation_token(consumer_name) |
62 | auth_info = { |
63 | 'token_key': token.key, 'token_secret': token.secret, |
64 | - 'consumer_key': consumer.key, |
65 | - } |
66 | + 'consumer_key': consumer.key, 'name': consumer.name |
67 | + } |
68 | return HttpResponse( |
69 | json.dumps(auth_info), |
70 | content_type='application/json; charset=utf-8', |
71 | @@ -58,6 +78,39 @@ |
72 | profile.delete_authorisation_token(token_key) |
73 | return rc.DELETED |
74 | |
75 | + @operation(idempotent=False) |
76 | + def update_token_name(self, request): |
77 | + """Modify the consumer name of an authorisation OAuth token. |
78 | + |
79 | + :param token: Can be the whole token or only the token key. |
80 | + :type token: unicode |
81 | + :param name: New name of the token. |
82 | + :type name: unicode |
83 | + """ |
84 | + profile = request.user.userprofile |
85 | + token = get_mandatory_param(request.data, 'token') |
86 | + token_fields = token.split(":") |
87 | + if len(token_fields) == 3: |
88 | + token_key = token_fields[1] |
89 | + else: |
90 | + token_key = token |
91 | + consumer_name = get_mandatory_param(request.data, 'name') |
92 | + profile.modify_consumer_name(token_key, consumer_name) |
93 | + return rc.ACCEPTED |
94 | + |
95 | + @operation(idempotent=True) |
96 | + def list_authorisation_tokens(self, request): |
97 | + """List authorisation tokens available to the currently logged-in user. |
98 | + |
99 | + :return: list of dictionaries representing each key's name and token. |
100 | + """ |
101 | + profile = request.user.userprofile |
102 | + tokens = _format_tokens(profile.get_authorisation_tokens()) |
103 | + return HttpResponse( |
104 | + json.dumps(tokens, indent=4), |
105 | + content_type='application/json; charset=utf-8', |
106 | + status=int(http.client.OK)) |
107 | + |
108 | @classmethod |
109 | def resource_uri(cls, *args, **kwargs): |
110 | return ('account_handler', []) |
111 | |
112 | === modified file 'src/maasserver/api/tests/test_api.py' |
113 | --- src/maasserver/api/tests/test_api.py 2016-09-23 19:21:31 +0000 |
114 | +++ src/maasserver/api/tests/test_api.py 2016-10-09 22:36:13 +0000 |
115 | @@ -181,7 +181,8 @@ |
116 | |
117 | def test_create_authorisation_token(self): |
118 | # The api operation create_authorisation_token returns a json dict |
119 | - # with the consumer_key, the token_key and the token_secret in it. |
120 | + # with the consumer_key, the token_key, the token_secret and the |
121 | + # consumer_name in it. |
122 | response = self.client.post( |
123 | reverse('account_handler'), {'op': 'create_authorisation_token'}) |
124 | self.assertEqual(http.client.OK, response.status_code) |
125 | @@ -189,11 +190,27 @@ |
126 | 'application/json; charset=utf-8', response["content-type"]) |
127 | parsed_result = json_load_bytes(response.content) |
128 | self.assertEqual( |
129 | - ['consumer_key', 'token_key', 'token_secret'], |
130 | + ['consumer_key', 'name', 'token_key', 'token_secret'], |
131 | sorted(parsed_result)) |
132 | self.assertIsInstance(parsed_result['consumer_key'], str) |
133 | self.assertIsInstance(parsed_result['token_key'], str) |
134 | self.assertIsInstance(parsed_result['token_secret'], str) |
135 | + self.assertIsInstance(parsed_result['name'], str) |
136 | + |
137 | + def test_create_authorisation_token_with_token_name(self): |
138 | + # The api operation create_authorisation_token can also accept |
139 | + # a new name for the generated token. |
140 | + token_name = 'Test_Token' |
141 | + response = self.client.post( |
142 | + reverse('account_handler'), { |
143 | + 'op': 'create_authorisation_token', |
144 | + 'name': token_name |
145 | + }) |
146 | + self.assertEqual(http.client.OK, response.status_code) |
147 | + self.assertEqual( |
148 | + 'application/json; charset=utf-8', response["content-type"]) |
149 | + parsed_result = json_load_bytes(response.content) |
150 | + self.assertEqual(parsed_result['name'], token_name) |
151 | |
152 | def test_delete_authorisation_token_not_found(self): |
153 | # If the provided token_key does not exist (for the currently |
154 | @@ -209,9 +226,131 @@ |
155 | # delete_authorisation_token. It it is not present in the request's |
156 | # parameters, the api returns a 'Bad Request' (400) error. |
157 | response = self.client.post( |
158 | - reverse('account_handler'), {'op': 'delete_authorisation_token'}) |
159 | - |
160 | - self.assertEqual(http.client.BAD_REQUEST, response.status_code) |
161 | + reverse('account_handler'), { |
162 | + 'op': 'delete_authorisation_token' |
163 | + }) |
164 | + |
165 | + self.assertEqual(http.client.BAD_REQUEST, response.status_code) |
166 | + |
167 | + def test_update_authorisation_token(self): |
168 | + token_name_orig = 'Test_Token' |
169 | + token_name_updated = 'Test_Token update' |
170 | + response_creation = self.client.post( |
171 | + reverse('account_handler'), { |
172 | + 'op': 'create_authorisation_token', |
173 | + 'name': token_name_orig |
174 | + }) |
175 | + parsed_creation_response = json_load_bytes(response_creation.content) |
176 | + created_token = ":".join([ |
177 | + parsed_creation_response['consumer_key'], |
178 | + parsed_creation_response['token_key'], |
179 | + parsed_creation_response['token_secret'] |
180 | + ]) |
181 | + self.client.post( |
182 | + reverse('account_handler'), { |
183 | + 'op': 'update_token_name', 'token': created_token, |
184 | + 'name': token_name_updated |
185 | + }) |
186 | + response_list = self.client.get( |
187 | + reverse('account_handler'), { |
188 | + 'op': 'list_authorisation_tokens' |
189 | + }) |
190 | + parsed_list_response = json_load_bytes(response_list.content) |
191 | + for token in parsed_list_response: |
192 | + if token['token'] == created_token: |
193 | + self.assertEqual(token['name'], token_name_updated) |
194 | + |
195 | + def test_update_authorisation_token_with_token_key(self): |
196 | + # We use only "token_key" portion of the authorisation token |
197 | + # to update the token name. |
198 | + token_name_orig = 'Test_Token' |
199 | + token_name_updated = 'Test_Token update' |
200 | + response_creation = self.client.post( |
201 | + reverse('account_handler'), { |
202 | + 'op': 'create_authorisation_token', |
203 | + 'name': token_name_orig |
204 | + }) |
205 | + parsed_creation_response = json_load_bytes(response_creation.content) |
206 | + created_token = ":".join([ |
207 | + parsed_creation_response['consumer_key'], |
208 | + parsed_creation_response['token_key'], |
209 | + parsed_creation_response['token_secret'] |
210 | + ]) |
211 | + self.client.post( |
212 | + reverse('account_handler'), { |
213 | + 'op': 'update_token_name', |
214 | + 'token': parsed_creation_response['token_key'], |
215 | + 'name': token_name_updated |
216 | + }) |
217 | + response_list = self.client.get( |
218 | + reverse('account_handler'), { |
219 | + 'op': 'list_authorisation_tokens' |
220 | + }) |
221 | + parsed_list_response = json_load_bytes(response_list.content) |
222 | + for token in parsed_list_response: |
223 | + if token['token'] == created_token: |
224 | + self.assertEqual(token['name'], token_name_updated) |
225 | + |
226 | + def test_update_authorisation_token_name_not_found(self): |
227 | + # If the provided token_key does not exist (for the currently |
228 | + # logged-in user), the api returns a 'Not Found' (404) error. |
229 | + response = self.client.post( |
230 | + reverse('account_handler'), { |
231 | + 'op': 'update_token_name', 'token': 'no-such-token', |
232 | + 'name': 'test_name' |
233 | + }) |
234 | + |
235 | + self.assertEqual(http.client.NOT_FOUND, response.status_code) |
236 | + |
237 | + def test_update_authorisation_token_name_bad_request_no_token(self): |
238 | + # `token` and `name` are mandatory parameters when calling |
239 | + # update_token_name. If it is not present in the request's |
240 | + # parameters, the api returns a 'Bad Request' (400) error. |
241 | + response = self.client.post( |
242 | + reverse('account_handler'), { |
243 | + 'op': 'update_token_name' |
244 | + }) |
245 | + |
246 | + self.assertEqual(http.client.BAD_REQUEST, response.status_code) |
247 | + |
248 | + def test_list_tokens(self): |
249 | + token1_name = "Test Token 1" |
250 | + response_creation = self.client.post( |
251 | + reverse('account_handler'), { |
252 | + 'op': 'create_authorisation_token', |
253 | + 'name': token1_name |
254 | + }) |
255 | + parsed_creation_response = json_load_bytes(response_creation.content) |
256 | + response = self.client.get( |
257 | + reverse('account_handler'), { |
258 | + 'op': 'list_authorisation_tokens' |
259 | + }) |
260 | + parsed_list_response = json_load_bytes(response.content) |
261 | + self.assertEqual(len(json_load_bytes(response.content)), 2) |
262 | + for token in parsed_list_response: |
263 | + if token['name'] == token1_name: |
264 | + token_fields = token['token'].split(":") |
265 | + self.assertEqual( |
266 | + token_fields[0], |
267 | + parsed_creation_response['consumer_key']) |
268 | + self.assertEqual( |
269 | + token_fields[1], |
270 | + parsed_creation_response['token_key']) |
271 | + self.assertEqual( |
272 | + token_fields[2], |
273 | + parsed_creation_response['token_secret']) |
274 | + |
275 | + def test_list_tokens_format(self): |
276 | + self.client.post( |
277 | + reverse('account_handler'), {'op': 'create_authorisation_token'}) |
278 | + response = self.client.get( |
279 | + reverse('account_handler'), {'op': 'list_authorisation_tokens'}) |
280 | + parsed_list_response = json_load_bytes(response.content) |
281 | + self.assertIsInstance(parsed_list_response, list) |
282 | + for token in parsed_list_response: |
283 | + self.assertEqual(['name', 'token'], sorted(token)) |
284 | + self.assertIsInstance(token['name'], str) |
285 | + self.assertIsInstance(token['token'], str) |
286 | |
287 | |
288 | class TestSSHKeyHandlers(APITestCase.ForUser): |
289 | |
290 | === modified file 'src/maasserver/management/commands/apikey.py' |
291 | --- src/maasserver/management/commands/apikey.py 2016-06-22 17:03:02 +0000 |
292 | +++ src/maasserver/management/commands/apikey.py 2016-10-09 22:36:13 +0000 |
293 | @@ -36,17 +36,34 @@ |
294 | parser.add_argument( |
295 | '--delete', default=None, |
296 | help="Delete the supplied api key.") |
297 | + parser.add_argument( |
298 | + '--update', default=None, |
299 | + help="Update the supplied api key name") |
300 | + parser.add_argument( |
301 | + '--name', default=None, dest="api_key_name", |
302 | + help="Name of the token. This argument should be passed to " |
303 | + "--update or --generate options") |
304 | + parser.add_argument( |
305 | + '--with-names ', dest="with_names", action="store_true", |
306 | + help="Display tokens with their names.") |
307 | |
308 | def _print_token(self, token): |
309 | - """Write `token` to stdout in the standard format.""" |
310 | - self.stdout.write(convert_tuple_to_string(get_creds_tuple(token))) |
311 | + """Write `token` to stdout in the standard format (with names if |
312 | + --with-names option is enabled)""" |
313 | + if self.display_names: |
314 | + self.stdout.write("%s %s" % ( |
315 | + convert_tuple_to_string(get_creds_tuple(token)), |
316 | + token.consumer.name |
317 | + )) |
318 | + else: |
319 | + self.stdout.write(convert_tuple_to_string(get_creds_tuple(token))) |
320 | # In Django 1.5+, self.stdout.write() adds a newline character at |
321 | # the end of the message. |
322 | if django.VERSION < (1, 5): |
323 | self.stdout.write('\n') |
324 | |
325 | - def _generate_token(self, user): |
326 | - _, token = user.userprofile.create_authorisation_token() |
327 | + def _generate_token(self, user, consumer_name=None): |
328 | + _, token = user.userprofile.create_authorisation_token(consumer_name) |
329 | self._print_token(token) |
330 | |
331 | def _delete_token(self, user, key_to_delete): |
332 | @@ -61,6 +78,17 @@ |
333 | except Http404: |
334 | raise CommandError("No matching api key found.") |
335 | |
336 | + def _update_token(self, user, key_to_update, consumer_name): |
337 | + try: |
338 | + creds_tuple = convert_string_to_tuple(key_to_update) |
339 | + except ValueError as e: |
340 | + raise CommandError(e) |
341 | + _, token_key, _ = creds_tuple |
342 | + try: |
343 | + user.userprofile.modify_consumer_name(token_key, consumer_name) |
344 | + except Http404: |
345 | + raise CommandError("No matching api key found.") |
346 | + |
347 | def handle(self, *args, **options): |
348 | username = options.get('username', None) |
349 | if username is None: |
350 | @@ -68,16 +96,30 @@ |
351 | |
352 | generate = options.get('generate') |
353 | key_to_delete = options.get('delete', None) |
354 | + key_to_update = options.get('update', None) |
355 | + consumer_name = options.get('api_key_name', None) |
356 | + self.display_names = options.get('with_names', None) |
357 | + |
358 | if generate and key_to_delete is not None: |
359 | raise CommandError("Specify one of --generate or --delete.") |
360 | |
361 | + if generate and key_to_update is not None: |
362 | + raise CommandError("Specify one of --generate or --update.") |
363 | + |
364 | + if key_to_delete and key_to_update is not None: |
365 | + raise CommandError("Specify one of --delete or --update.") |
366 | + |
367 | + if key_to_update is not None and consumer_name is None: |
368 | + raise CommandError("Should specify new name for the updated key .") |
369 | + |
370 | user = get_one(User.objects.filter(username=username)) |
371 | + |
372 | if user is None: |
373 | raise CommandError("User does not exist.") |
374 | |
375 | if generate: |
376 | # Generate a new api key. |
377 | - self._generate_token(user) |
378 | + self._generate_token(user, consumer_name) |
379 | return |
380 | |
381 | elif key_to_delete is not None: |
382 | @@ -85,6 +127,11 @@ |
383 | self._delete_token(user, key_to_delete) |
384 | return |
385 | |
386 | + elif key_to_update is not None: |
387 | + # Update an existing key name. |
388 | + self._update_token(user, key_to_update, consumer_name) |
389 | + return |
390 | + |
391 | else: |
392 | # No mutating action requested, so just print existing keys. |
393 | tokens = user.userprofile.get_authorisation_tokens() |
394 | |
395 | === modified file 'src/maasserver/models/user.py' |
396 | --- src/maasserver/models/user.py 2015-12-02 20:43:30 +0000 |
397 | +++ src/maasserver/models/user.py 2016-10-09 22:36:13 +0000 |
398 | @@ -29,17 +29,21 @@ |
399 | GENERIC_CONSUMER = 'MAAS consumer' |
400 | |
401 | |
402 | -def create_auth_token(user): |
403 | +def create_auth_token(user, consumer_name=None): |
404 | """Create new Token and Consumer (OAuth authorisation) for `user`. |
405 | |
406 | :param user: The user to create a token for. |
407 | :type user: User |
408 | + :param consumer_name: Name of the consumer to be assigned to the newly |
409 | + generated token. |
410 | :return: The created Token. |
411 | :rtype: piston.models.Token |
412 | |
413 | """ |
414 | + if consumer_name is None: |
415 | + consumer_name = GENERIC_CONSUMER |
416 | consumer = Consumer.objects.create( |
417 | - user=user, name=GENERIC_CONSUMER, status='accepted') |
418 | + user=user, name=consumer_name, status='accepted') |
419 | consumer.generate_random_codes() |
420 | # This is a 'generic' consumer aimed to service many clients, hence |
421 | # we don't authenticate the consumer with key/secret key. |
422 | |
423 | === modified file 'src/maasserver/models/userprofile.py' |
424 | --- src/maasserver/models/userprofile.py 2016-09-22 02:53:33 +0000 |
425 | +++ src/maasserver/models/userprofile.py 2016-10-09 22:36:13 +0000 |
426 | @@ -92,7 +92,7 @@ |
427 | |
428 | return get_auth_tokens(self.user) |
429 | |
430 | - def create_authorisation_token(self): |
431 | + def create_authorisation_token(self, consumer_name=None): |
432 | """Create a new Token and its related Consumer (OAuth authorisation). |
433 | |
434 | :return: A tuple containing the Consumer and the Token that were |
435 | @@ -103,7 +103,7 @@ |
436 | # Avoid circular imports. |
437 | from maasserver.models.user import create_auth_token |
438 | |
439 | - token = create_auth_token(self.user) |
440 | + token = create_auth_token(self.user, consumer_name) |
441 | return token.consumer, token |
442 | |
443 | def delete_authorisation_token(self, token_key): |
444 | @@ -119,5 +119,20 @@ |
445 | token.consumer.delete() |
446 | token.delete() |
447 | |
448 | + def modify_consumer_name(self, token_key, consumer_name): |
449 | + """Modify consumer name of an existing token key. |
450 | + |
451 | + :param token_key: The key of the token to be deleted. |
452 | + :type token_key: string |
453 | + :param consumer_name: Name of the token consumer. |
454 | + :type consumer_name: string |
455 | + :raises: `django.http.Http404` |
456 | + """ |
457 | + token = get_object_or_404( |
458 | + Token, user=self.user, token_type=Token.ACCESS, key=token_key) |
459 | + token.consumer.name = consumer_name |
460 | + token.consumer.save() |
461 | + token.save() |
462 | + |
463 | def __str__(self): |
464 | return self.user.username |
465 | |
466 | === modified file 'src/maasserver/tests/test_commands.py' |
467 | --- src/maasserver/tests/test_commands.py 2016-09-23 18:03:06 +0000 |
468 | +++ src/maasserver/tests/test_commands.py 2016-10-09 22:36:13 +0000 |
469 | @@ -427,3 +427,59 @@ |
470 | self, 'apikey', username=user.username, delete=token_string) |
471 | self.assertIn( |
472 | "No matching api key found", error_text) |
473 | + |
474 | + def test_success_modify_apikey_name(self): |
475 | + stderr = StringIO() |
476 | + stdout = StringIO() |
477 | + fake_api_key_name = "Test Key Name" |
478 | + user = factory.make_User() |
479 | + existing_token = get_one( |
480 | + user.userprofile.get_authorisation_tokens()) |
481 | + token_string = convert_tuple_to_string( |
482 | + get_creds_tuple(existing_token)) |
483 | + call_command( |
484 | + 'apikey', username=user.username, update=token_string, |
485 | + api_key_name=fake_api_key_name, stderr=stderr, stdout=stdout) |
486 | + self.assertThat(stderr, IsEmpty) |
487 | + |
488 | + def test_api_key_rejects_update_of_nonexistent_key(self): |
489 | + stderr = StringIO() |
490 | + user = factory.make_User() |
491 | + fake_api_key_name = "Test Key Name" |
492 | + existing_token = get_one( |
493 | + user.userprofile.get_authorisation_tokens()) |
494 | + token_string = convert_tuple_to_string( |
495 | + get_creds_tuple(existing_token)) |
496 | + call_command( |
497 | + 'apikey', username=user.username, delete=token_string, |
498 | + stderr=stderr) |
499 | + self.assertThat(stderr, IsEmpty) |
500 | + |
501 | + # Try to update the deleted token. |
502 | + error_text = assertCommandErrors( |
503 | + self, 'apikey', username=user.username, update=token_string, |
504 | + api_key_name=fake_api_key_name) |
505 | + self.assertIn( |
506 | + "No matching api key found", error_text) |
507 | + |
508 | + def test_api_key_rejects_update_without_key_name(self): |
509 | + user = factory.make_User() |
510 | + existing_token = get_one( |
511 | + user.userprofile.get_authorisation_tokens()) |
512 | + token_string = convert_tuple_to_string( |
513 | + get_creds_tuple(existing_token)) |
514 | + error_text = assertCommandErrors( |
515 | + self, 'apikey', username=user.username, update=token_string) |
516 | + self.assertIn("Should specify new name", error_text) |
517 | + |
518 | + def test_api_key_update_and_generate_mutually_exclusive_options(self): |
519 | + user = factory.make_User() |
520 | + fake_api_key_name = "Test Key Name" |
521 | + existing_token = get_one( |
522 | + user.userprofile.get_authorisation_tokens()) |
523 | + token_string = convert_tuple_to_string( |
524 | + get_creds_tuple(existing_token)) |
525 | + error_text = assertCommandErrors( |
526 | + self, 'apikey', username=user.username, generate=True, |
527 | + api_key_name=fake_api_key_name, update=token_string) |
528 | + self.assertIn("Specify one of --generate or --update.", error_text) |
The code looks generally good. I've got some comments below, mostly about the names of user-facing strings/ documentation. (and some code style comments.)