~smoser/ubuntu/wily/maas/lp1474417

« back to all changes in this revision

Viewing changes to src/maasserver/tests/test_api.py

  • Committer: Package Import Robot
  • Author(s): Andres Rodriguez, Dave Walker, Andres Rodriguez
  • Date: 2012-03-07 12:46:17 UTC
  • mfrom: (1.1.2)
  • Revision ID: package-import@ubuntu.com-20120307124617-319lac0nc230srs9
Tags: 0.1+bzr232+dfsg-0ubuntu1
* New upstream snapshot.

[ Dave Walker ]
* debian/control:
  - Changed depends from psycopg2 to python-psycopg2 and dropped versioning.
    - LP: #937982 

[ Andres Rodriguez ]
* debian/maas.install: Install maas-import-isos and related files.
* Integrate squid3 as proxy solution:
  - debian/extras/squid.conf: Added
  - debian/control: Depend on squid3.
  - debian/maas.{postinst,postrm}: Handle installation/removal of custom
    squid config file.
* Split into different binary packages (maas,python-django-maas).
  - debian/maas.postinst: Handle the apache2 configuration.

Show diffs side-by-side

added added

removed removed

Lines of Context:
11
11
__metaclass__ = type
12
12
__all__ = []
13
13
 
 
14
from base64 import b64encode
14
15
import httplib
15
16
import json
16
17
import os
18
19
 
19
20
from django.conf import settings
20
21
from maasserver.models import (
 
22
    ARCHITECTURE,
 
23
    Config,
21
24
    MACAddress,
22
25
    Node,
23
26
    NODE_STATUS,
24
27
    )
25
28
from maasserver.testing import (
26
29
    LoggedInTestCase,
 
30
    reload_object,
 
31
    reload_objects,
27
32
    TestCase,
28
33
    )
 
34
from maasserver.testing.enum import map_enum
29
35
from maasserver.testing.factory import factory
30
36
from maasserver.testing.oauthclient import OAuthAuthenticatedClient
31
 
from metadataserver.models import NodeKey
 
37
from metadataserver.models import (
 
38
    NodeKey,
 
39
    NodeUserData,
 
40
    )
32
41
from metadataserver.nodeinituser import get_node_init_user
33
42
 
34
43
 
35
 
class AnonymousEnlistmentAPITest(TestCase):
 
44
class APIv10TestMixin:
 
45
 
 
46
    def get_uri(self, path):
 
47
        """GET an API V1 uri.
 
48
 
 
49
        :return: The API uri.
 
50
        """
 
51
        api_root = '/api/1.0/'
 
52
        return api_root + path
 
53
 
 
54
 
 
55
class AnonymousEnlistmentAPITest(APIv10TestMixin, TestCase):
36
56
    # Nodes can be enlisted anonymously.
37
57
 
38
58
    def test_POST_new_creates_node(self):
39
59
        # The API allows a Node to be created.
40
60
        response = self.client.post(
41
 
            '/api/nodes/',
 
61
            self.get_uri('nodes/'),
42
62
            {
43
63
                'op': 'new',
44
64
                'hostname': 'diane',
 
65
                'architecture': 'amd64',
45
66
                'after_commissioning_action': '2',
46
67
                'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
47
68
            })
53
74
        self.assertNotEqual(0, len(parsed_result.get('system_id')))
54
75
        [diane] = Node.objects.filter(hostname='diane')
55
76
        self.assertEqual(2, diane.after_commissioning_action)
 
77
        self.assertEqual(ARCHITECTURE.amd64, diane.architecture)
56
78
 
57
79
    def test_POST_new_associates_mac_addresses(self):
58
80
        # The API allows a Node to be created and associated with MAC
59
81
        # Addresses.
60
82
        self.client.post(
61
 
            '/api/nodes/',
 
83
            self.get_uri('nodes/'),
62
84
            {
63
85
                'op': 'new',
64
86
                'hostname': 'diane',
72
94
 
73
95
    def test_POST_returns_limited_fields(self):
74
96
        response = self.client.post(
75
 
            '/api/nodes/',
 
97
            self.get_uri('nodes/'),
76
98
            {
77
99
                'op': 'new',
78
100
                'hostname': 'diane',
81
103
            })
82
104
        parsed_result = json.loads(response.content)
83
105
        self.assertItemsEqual(
84
 
            ['hostname', 'system_id', 'macaddress_set'], parsed_result.keys())
 
106
            ['hostname', 'system_id', 'macaddress_set', 'architecture'],
 
107
            list(parsed_result))
85
108
 
86
109
    def test_POST_fails_without_operation(self):
87
110
        # If there is no operation ('op=operation_name') specified in the
88
111
        # request data, a 'Bad request' response is returned.
89
112
        response = self.client.post(
90
 
            '/api/nodes/',
 
113
            self.get_uri('nodes/'),
91
114
            {
92
115
                'hostname': 'diane',
93
116
                'mac_addresses': ['aa:bb:cc:dd:ee:ff', 'invalid'],
101
124
        # If the operation ('op=operation_name') specified in the
102
125
        # request data is unknown, a 'Bad request' response is returned.
103
126
        response = self.client.post(
104
 
            '/api/nodes/',
 
127
            self.get_uri('nodes/'),
105
128
            {
106
129
                'op': 'invalid_operation',
107
130
                'hostname': 'diane',
116
139
        # If the data provided to create a node with an invalid MAC
117
140
        # Address, a 'Bad request' response is returned.
118
141
        response = self.client.post(
119
 
            '/api/nodes/',
 
142
            self.get_uri('nodes/'),
120
143
            {
121
144
                'op': 'new',
122
145
                'hostname': 'diane',
131
154
            ["One or more MAC Addresses is invalid."],
132
155
            parsed_result['mac_addresses'])
133
156
 
134
 
 
135
 
class NodeAnonAPITest(TestCase):
 
157
    def test_POST_invalid_architecture_returns_bad_request(self):
 
158
        # If the architecture name provided to create a node is not a valid
 
159
        # architecture name, a 'Bad request' response is returned.
 
160
        response = self.client.post(
 
161
            self.get_uri('nodes/'),
 
162
            {
 
163
                'op': 'new',
 
164
                'hostname': 'diane',
 
165
                'mac_addresses': ['aa:bb:cc:dd:ee:ff'],
 
166
                'architecture': 'invalid-architecture',
 
167
            })
 
168
        parsed_result = json.loads(response.content)
 
169
 
 
170
        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
 
171
        self.assertIn('application/json', response['Content-Type'])
 
172
        self.assertItemsEqual(['architecture'], parsed_result)
 
173
 
 
174
 
 
175
class NodeAnonAPITest(APIv10TestMixin, TestCase):
136
176
 
137
177
    def test_anon_nodes_GET(self):
138
178
        # Anonymous requests to the API are denied.
139
 
        response = self.client.get('/api/nodes/')
 
179
        response = self.client.get(self.get_uri('nodes/'))
140
180
 
141
181
        self.assertEqual(httplib.UNAUTHORIZED, response.status_code)
142
182
 
143
183
    def test_anon_api_doc(self):
144
184
        # The documentation is accessible to anon users.
145
 
        response = self.client.get('/api/doc/')
 
185
        response = self.client.get(self.get_uri('doc/'))
146
186
 
147
187
        self.assertEqual(httplib.OK, response.status_code)
148
188
 
149
189
    def test_node_init_user_cannot_access(self):
150
 
        token = NodeKey.objects.create_token(factory.make_node())
 
190
        token = NodeKey.objects.get_token_for_node(factory.make_node())
151
191
        client = OAuthAuthenticatedClient(get_node_init_user(), token)
152
 
        response = client.get('/api/nodes/', {'op': 'list'})
 
192
        response = client.get(self.get_uri('nodes/'), {'op': 'list'})
153
193
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
154
194
 
155
195
 
156
 
class APITestCase(TestCase):
 
196
class APITestCase(APIv10TestMixin, TestCase):
157
197
    """Extension to `TestCase`: log in first.
158
198
 
159
199
    :ivar logged_in_user: A user who is currently logged in and can access
179
219
    return [node.get('system_id') for node in parsed_result]
180
220
 
181
221
 
182
 
class NodeAPILoggedInTest(LoggedInTestCase):
 
222
class NodeAPILoggedInTest(APIv10TestMixin, LoggedInTestCase):
183
223
 
184
224
    def test_nodes_GET_logged_in(self):
185
225
        # A (Django) logged-in user can access the API.
186
226
        node = factory.make_node()
187
 
        response = self.client.get('/api/nodes/', {'op': 'list'})
 
227
        response = self.client.get(self.get_uri('nodes/'), {'op': 'list'})
188
228
        parsed_result = json.loads(response.content)
189
229
 
190
230
        self.assertEqual(httplib.OK, response.status_code)
192
232
 
193
233
 
194
234
class TestNodeAPI(APITestCase):
195
 
    """Tests for /api/nodes/<node>/."""
 
235
    """Tests for /api/1.0/nodes/<node>/."""
196
236
 
197
 
    def get_uri(self, node):
 
237
    def get_node_uri(self, node):
198
238
        """Get the API URI for `node`."""
199
 
        return '/api/nodes/%s/' % node.system_id
 
239
        return self.get_uri('nodes/%s/') % node.system_id
200
240
 
201
241
    def test_GET_returns_node(self):
202
242
        # The api allows for fetching a single Node (using system_id).
203
243
        node = factory.make_node(set_hostname=True)
204
 
        response = self.client.get(self.get_uri(node))
 
244
        response = self.client.get(self.get_node_uri(node))
205
245
        parsed_result = json.loads(response.content)
206
246
 
207
247
        self.assertEqual(httplib.OK, response.status_code)
214
254
        other_node = factory.make_node(
215
255
            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
216
256
 
217
 
        response = self.client.get(self.get_uri(other_node))
 
257
        response = self.client.get(self.get_node_uri(other_node))
218
258
 
219
259
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
220
260
 
221
261
    def test_GET_refuses_to_access_nonexistent_node(self):
222
262
        # When fetching a Node, the api returns a 'Not Found' (404) error
223
263
        # if no node is found.
224
 
        response = self.client.get('/api/nodes/invalid-uuid/')
 
264
        response = self.client.get(self.get_uri('nodes/invalid-uuid/'))
225
265
 
226
266
        self.assertEqual(httplib.NOT_FOUND, response.status_code)
227
267
 
228
268
    def test_POST_stop_checks_permission(self):
229
269
        node = factory.make_node()
230
 
        response = self.client.post(self.get_uri(node), {'op': 'stop'})
 
270
        response = self.client.post(self.get_node_uri(node), {'op': 'stop'})
231
271
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
232
272
 
233
273
    def test_POST_stop_returns_node(self):
234
274
        node = factory.make_node(owner=self.logged_in_user)
235
 
        response = self.client.post(self.get_uri(node), {'op': 'stop'})
 
275
        response = self.client.post(self.get_node_uri(node), {'op': 'stop'})
236
276
        self.assertEqual(httplib.OK, response.status_code)
237
277
        self.assertEqual(
238
278
            node.system_id, json.loads(response.content)['system_id'])
239
279
 
240
280
    def test_POST_stop_may_be_repeated(self):
241
281
        node = factory.make_node(owner=self.logged_in_user)
242
 
        self.client.post(self.get_uri(node), {'op': 'stop'})
243
 
        response = self.client.post(self.get_uri(node), {'op': 'stop'})
 
282
        self.client.post(self.get_node_uri(node), {'op': 'stop'})
 
283
        response = self.client.post(self.get_node_uri(node), {'op': 'stop'})
244
284
        self.assertEqual(httplib.OK, response.status_code)
245
285
 
246
286
    def test_POST_start_checks_permission(self):
247
287
        node = factory.make_node()
248
 
        response = self.client.post(self.get_uri(node), {'op': 'start'})
 
288
        response = self.client.post(self.get_node_uri(node), {'op': 'start'})
249
289
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
250
290
 
251
291
    def test_POST_start_returns_node(self):
252
292
        node = factory.make_node(owner=self.logged_in_user)
253
 
        response = self.client.post(self.get_uri(node), {'op': 'start'})
 
293
        response = self.client.post(self.get_node_uri(node), {'op': 'start'})
254
294
        self.assertEqual(httplib.OK, response.status_code)
255
295
        self.assertEqual(
256
296
            node.system_id, json.loads(response.content)['system_id'])
257
297
 
258
298
    def test_POST_start_may_be_repeated(self):
259
299
        node = factory.make_node(owner=self.logged_in_user)
260
 
        self.client.post(self.get_uri(node), {'op': 'start'})
261
 
        response = self.client.post(self.get_uri(node), {'op': 'start'})
262
 
        self.assertEqual(httplib.OK, response.status_code)
 
300
        self.client.post(self.get_node_uri(node), {'op': 'start'})
 
301
        response = self.client.post(self.get_node_uri(node), {'op': 'start'})
 
302
        self.assertEqual(httplib.OK, response.status_code)
 
303
 
 
304
    def test_POST_start_stores_user_data(self):
 
305
        node = factory.make_node(owner=self.logged_in_user)
 
306
        user_data = (
 
307
            b'\xff\x00\xff\xfe\xff\xff\xfe' +
 
308
            factory.getRandomString().encode('ascii'))
 
309
        response = self.client.post(
 
310
            self.get_node_uri(node), {
 
311
                'op': 'start',
 
312
                'user_data': b64encode(user_data),
 
313
                })
 
314
        self.assertEqual(httplib.OK, response.status_code)
 
315
        self.assertEqual(user_data, NodeUserData.objects.get_user_data(node))
 
316
 
 
317
    def test_POST_release_releases_owned_node(self):
 
318
        owned_statuses = [
 
319
            NODE_STATUS.RESERVED,
 
320
            NODE_STATUS.ALLOCATED,
 
321
            ]
 
322
        owned_nodes = [
 
323
            factory.make_node(owner=self.logged_in_user, status=status)
 
324
            for status in owned_statuses]
 
325
        responses = [
 
326
            self.client.post(self.get_node_uri(node), {'op': 'release'})
 
327
            for node in owned_nodes]
 
328
        self.assertEqual(
 
329
            [httplib.OK] * len(owned_nodes),
 
330
            [response.status_code for response in responses])
 
331
        self.assertItemsEqual(
 
332
            [NODE_STATUS.READY] * len(owned_nodes),
 
333
            [node.status for node in reload_objects(Node, owned_nodes)])
 
334
 
 
335
    def test_POST_release_does_nothing_for_unowned_node(self):
 
336
        node = factory.make_node(status=NODE_STATUS.READY)
 
337
        response = self.client.post(
 
338
            self.get_node_uri(node), {'op': 'release'})
 
339
        self.assertEqual(httplib.OK, response.status_code)
 
340
        self.assertEqual(NODE_STATUS.READY, reload_object(node).status)
 
341
 
 
342
    def test_POST_release_fails_for_other_node_states(self):
 
343
        releasable_statuses = [
 
344
            NODE_STATUS.RESERVED,
 
345
            NODE_STATUS.ALLOCATED,
 
346
            NODE_STATUS.READY,
 
347
            ]
 
348
        unreleasable_statuses = [
 
349
            status
 
350
            for status in map_enum(NODE_STATUS).values()
 
351
                if status not in releasable_statuses]
 
352
        nodes = [
 
353
            factory.make_node(status=status, owner=self.logged_in_user)
 
354
            for status in unreleasable_statuses]
 
355
        responses = [
 
356
            self.client.post(self.get_node_uri(node), {'op': 'release'})
 
357
            for node in nodes]
 
358
        self.assertEqual(
 
359
            [httplib.CONFLICT] * len(unreleasable_statuses),
 
360
            [response.status_code for response in responses])
 
361
        self.assertEqual(
 
362
            unreleasable_statuses,
 
363
            [node.status for node in reload_objects(Node, nodes)])
 
364
 
 
365
    def test_POST_release_in_wrong_state_reports_current_state(self):
 
366
        node = factory.make_node(
 
367
            status=NODE_STATUS.RETIRED, owner=self.logged_in_user)
 
368
        response = self.client.post(
 
369
            self.get_node_uri(node), {'op': 'release'})
 
370
        self.assertEqual(
 
371
            (
 
372
                httplib.CONFLICT,
 
373
                "Node cannot be released in its current state ('Retired').",
 
374
            ),
 
375
            (response.status_code, response.content))
 
376
 
 
377
    def test_POST_release_rejects_request_from_unauthorized_user(self):
 
378
        node = factory.make_node(
 
379
            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
 
380
        response = self.client.post(
 
381
            self.get_node_uri(node), {'op': 'release'})
 
382
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
383
        self.assertEqual(NODE_STATUS.ALLOCATED, reload_object(node).status)
 
384
 
 
385
    def test_POST_release_allows_admin_to_release_anyones_node(self):
 
386
        node = factory.make_node(
 
387
            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
 
388
        self.become_admin()
 
389
        response = self.client.post(
 
390
            self.get_node_uri(node), {'op': 'release'})
 
391
        self.assertEqual(httplib.OK, response.status_code)
 
392
        self.assertEqual(NODE_STATUS.READY, reload_object(node).status)
 
393
 
 
394
    def test_POST_release_combines_with_acquire(self):
 
395
        node = factory.make_node(status=NODE_STATUS.READY)
 
396
        response = self.client.post(
 
397
            self.get_uri('nodes/'), {'op': 'acquire'})
 
398
        self.assertEqual(NODE_STATUS.ALLOCATED, reload_object(node).status)
 
399
        node_uri = json.loads(response.content)['resource_uri']
 
400
        response = self.client.post(node_uri, {'op': 'release'})
 
401
        self.assertEqual(httplib.OK, response.status_code)
 
402
        self.assertEqual(NODE_STATUS.READY, reload_object(node).status)
263
403
 
264
404
    def test_PUT_updates_node(self):
265
405
        # The api allows to update a Node.
266
406
        node = factory.make_node(hostname='diane')
267
407
        response = self.client.put(
268
 
            self.get_uri(node), {'hostname': 'francis'})
 
408
            self.get_node_uri(node), {'hostname': 'francis'})
269
409
        parsed_result = json.loads(response.content)
270
410
 
271
411
        self.assertEqual(httplib.OK, response.status_code)
277
417
        # When a Node is returned by the API, the field 'resource_uri'
278
418
        # provides the URI for this Node.
279
419
        node = factory.make_node(hostname='diane')
280
 
        response = self.client.put(self.get_uri(node), {'hostname': 'francis'})
 
420
        response = self.client.put(
 
421
            self.get_node_uri(node), {'hostname': 'francis'})
281
422
        parsed_result = json.loads(response.content)
282
423
 
283
424
        self.assertEqual(
284
 
            '/api/nodes/%s/' % (parsed_result['system_id']),
 
425
            self.get_uri('nodes/%s/') % (parsed_result['system_id']),
285
426
            parsed_result['resource_uri'])
286
427
 
287
428
    def test_PUT_rejects_invalid_data(self):
289
430
        # response is returned.
290
431
        node = factory.make_node(hostname='diane')
291
432
        response = self.client.put(
292
 
            self.get_uri(node), {'hostname': 'too long' * 100})
 
433
            self.get_node_uri(node), {'hostname': 'too long' * 100})
293
434
        parsed_result = json.loads(response.content)
294
435
 
295
436
        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
305
446
        other_node = factory.make_node(
306
447
            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
307
448
 
308
 
        response = self.client.put(self.get_uri(other_node))
 
449
        response = self.client.put(self.get_node_uri(other_node))
309
450
 
310
451
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
311
452
 
312
453
    def test_PUT_refuses_to_update_nonexistent_node(self):
313
454
        # When updating a Node, the api returns a 'Not Found' (404) error
314
455
        # if no node is found.
315
 
        response = self.client.put('/api/nodes/no-node-here/')
 
456
        response = self.client.put(self.get_uri('nodes/no-node-here/'))
316
457
 
317
458
        self.assertEqual(httplib.NOT_FOUND, response.status_code)
318
459
 
320
461
        # The api allows to delete a Node.
321
462
        node = factory.make_node(set_hostname=True)
322
463
        system_id = node.system_id
323
 
        response = self.client.delete(self.get_uri(node))
 
464
        response = self.client.delete(self.get_node_uri(node))
324
465
 
325
466
        self.assertEqual(204, response.status_code)
326
467
        self.assertItemsEqual([], Node.objects.filter(system_id=system_id))
331
472
        other_node = factory.make_node(
332
473
            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
333
474
 
334
 
        response = self.client.delete(self.get_uri(other_node))
 
475
        response = self.client.delete(self.get_node_uri(other_node))
335
476
 
336
477
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
337
478
 
338
479
    def test_DELETE_refuses_to_delete_nonexistent_node(self):
339
480
        # When deleting a Node, the api returns a 'Not Found' (404) error
340
481
        # if no node is found.
341
 
        response = self.client.delete('/api/nodes/no-node-here/')
 
482
        response = self.client.delete(self.get_uri('nodes/no-node-here/'))
342
483
 
343
484
        self.assertEqual(httplib.NOT_FOUND, response.status_code)
344
485
 
345
486
 
346
487
class TestNodesAPI(APITestCase):
347
 
    """Tests for /api/nodes/."""
 
488
    """Tests for /api/1.0/nodes/."""
 
489
 
 
490
    def test_POST_new_creates_node(self):
 
491
        # The API allows a Node to be created, even as a logged-in user.
 
492
        response = self.client.post(
 
493
            self.get_uri('nodes/'),
 
494
            {
 
495
                'op': 'new',
 
496
                'hostname': 'diane',
 
497
                'after_commissioning_action': '2',
 
498
                'mac_addresses': ['aa:bb:cc:dd:ee:ff', '22:bb:cc:dd:ee:ff'],
 
499
            })
 
500
 
 
501
        self.assertEqual(httplib.OK, response.status_code)
348
502
 
349
503
    def test_GET_list_lists_nodes(self):
350
504
        # The api allows for fetching the list of Nodes.
352
506
        node2 = factory.make_node(
353
507
            set_hostname=True, status=NODE_STATUS.ALLOCATED,
354
508
            owner=self.logged_in_user)
355
 
        response = self.client.get('/api/nodes/', {'op': 'list'})
 
509
        response = self.client.get(self.get_uri('nodes/'), {'op': 'list'})
356
510
        parsed_result = json.loads(response.content)
357
511
 
358
512
        self.assertEqual(httplib.OK, response.status_code)
363
517
    def test_GET_list_without_nodes_returns_empty_list(self):
364
518
        # If there are no nodes to list, the "list" op still works but
365
519
        # returns an empty list.
366
 
        response = self.client.get('/api/nodes/', {'op': 'list'})
 
520
        response = self.client.get(self.get_uri('nodes/'), {'op': 'list'})
367
521
        self.assertItemsEqual([], json.loads(response.content))
368
522
 
369
523
    def test_GET_list_orders_by_id(self):
370
524
        # Nodes are returned in id order.
371
525
        nodes = [factory.make_node() for counter in range(3)]
372
 
        response = self.client.get('/api/nodes/', {'op': 'list'})
 
526
        response = self.client.get(self.get_uri('nodes/'), {'op': 'list'})
373
527
        parsed_result = json.loads(response.content)
374
528
        self.assertSequenceEqual(
375
529
            [node.system_id for node in nodes],
380
534
        # nodes with matching ids will be returned.
381
535
        ids = [factory.make_node().system_id for counter in range(3)]
382
536
        matching_id = ids[0]
383
 
        response = self.client.get('/api/nodes/', {
 
537
        response = self.client.get(self.get_uri('nodes/'), {
384
538
            'op': 'list',
385
539
            'id': [matching_id],
386
540
        })
393
547
        # no nodes -- even if other (non-matching) nodes exist.
394
548
        existing_id = factory.make_node().system_id
395
549
        nonexistent_id = existing_id + factory.getRandomString()
396
 
        response = self.client.get('/api/nodes/', {
 
550
        response = self.client.get(self.get_uri('nodes/'), {
397
551
            'op': 'list',
398
552
            'id': [nonexistent_id],
399
553
        })
403
557
        # Even when ids are passed to "list," nodes are returned in id
404
558
        # order, not necessarily in the order of the id arguments.
405
559
        ids = [factory.make_node().system_id for counter in range(3)]
406
 
        response = self.client.get('/api/nodes/', {
 
560
        response = self.client.get(self.get_uri('nodes/'), {
407
561
            'op': 'list',
408
562
            'id': list(reversed(ids)),
409
563
        })
415
569
        # matching ones are returned.
416
570
        existing_id = factory.make_node().system_id
417
571
        nonexistent_id = existing_id + factory.getRandomString()
418
 
        response = self.client.get('/api/nodes/', {
 
572
        response = self.client.get(self.get_uri('nodes/'), {
419
573
            'op': 'list',
420
574
            'id': [existing_id, nonexistent_id],
421
575
        })
427
581
        # The "acquire" operation returns an available node.
428
582
        available_status = NODE_STATUS.READY
429
583
        node = factory.make_node(status=available_status, owner=None)
430
 
        response = self.client.post('/api/nodes/', {'op': 'acquire'})
 
584
        response = self.client.post(self.get_uri('nodes/'), {'op': 'acquire'})
431
585
        self.assertEqual(200, response.status_code)
432
586
        parsed_result = json.loads(response.content)
433
587
        self.assertEqual(node.system_id, parsed_result['system_id'])
436
590
        # The "acquire" operation allocates the node it returns.
437
591
        available_status = NODE_STATUS.READY
438
592
        node = factory.make_node(status=available_status, owner=None)
439
 
        self.client.post('/api/nodes/', {'op': 'acquire'})
 
593
        self.client.post(self.get_uri('nodes/'), {'op': 'acquire'})
440
594
        node = Node.objects.get(system_id=node.system_id)
441
595
        self.assertEqual(self.logged_in_user, node.owner)
442
596
 
443
597
    def test_POST_acquire_fails_if_no_node_present(self):
444
598
        # The "acquire" operation returns a Conflict error if no nodes
445
599
        # are available.
446
 
        response = self.client.post('/api/nodes/', {'op': 'acquire'})
 
600
        response = self.client.post(self.get_uri('nodes/'), {'op': 'acquire'})
447
601
        # Fails with Conflict error: resource can't satisfy request.
448
602
        self.assertEqual(httplib.CONFLICT, response.status_code)
449
603
 
458
612
 
459
613
    def test_macs_GET(self):
460
614
        # The api allows for fetching the list of the MAC Addresss for a node.
461
 
        response = self.client.get('/api/nodes/%s/macs/' % self.node.system_id)
 
615
        response = self.client.get(
 
616
            self.get_uri('nodes/%s/macs/') % self.node.system_id)
462
617
        parsed_result = json.loads(response.content)
463
618
 
464
619
        self.assertEqual(httplib.OK, response.status_code)
474
629
        other_node = factory.make_node(
475
630
            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
476
631
        response = self.client.get(
477
 
            '/api/nodes/%s/macs/' % other_node.system_id)
 
632
            self.get_uri('nodes/%s/macs/') % other_node.system_id)
478
633
 
479
634
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
480
635
 
481
636
    def test_macs_GET_not_found(self):
482
637
        # When fetching MAC Addresses, the api returns a 'Not Found' (404)
483
638
        # error if no node is found.
484
 
        response = self.client.get('/api/nodes/invalid-id/macs/')
 
639
        response = self.client.get(self.get_uri('nodes/invalid-id/macs/'))
485
640
 
486
641
        self.assertEqual(httplib.NOT_FOUND, response.status_code)
487
642
 
489
644
        # When fetching a MAC Address, the api returns a 'Not Found' (404)
490
645
        # error if the MAC Address does not exist.
491
646
        response = self.client.get(
492
 
            '/api/nodes/%s/macs/00-aa-22-cc-44-dd/' % self.node.system_id)
 
647
            self.get_uri(
 
648
                'nodes/%s/macs/00-aa-22-cc-44-dd/') % self.node.system_id)
493
649
 
494
650
        self.assertEqual(httplib.NOT_FOUND, response.status_code)
495
651
 
499
655
        other_node = factory.make_node(
500
656
            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
501
657
        response = self.client.get(
502
 
            '/api/nodes/%s/macs/0-aa-22-cc-44-dd/' % other_node.system_id)
 
658
            self.get_uri(
 
659
                'nodes/%s/macs/0-aa-22-cc-44-dd/') % other_node.system_id)
503
660
 
504
661
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
505
662
 
507
664
        # When fetching a MAC Address, the api returns a 'Bad Request' (400)
508
665
        # error if the MAC Address is not valid.
509
666
        response = self.client.get(
510
 
            '/api/nodes/%s/macs/invalid-mac/' % self.node.system_id)
 
667
            self.get_uri('nodes/%s/macs/invalid-mac/') % self.node.system_id)
511
668
 
512
669
        self.assertEqual(400, response.status_code)
513
670
 
515
672
        # The api allows to add a MAC Address to an existing node.
516
673
        nb_macs = MACAddress.objects.filter(node=self.node).count()
517
674
        response = self.client.post(
518
 
            '/api/nodes/%s/macs/' % self.node.system_id,
 
675
            self.get_uri('nodes/%s/macs/') % self.node.system_id,
519
676
            {'mac_address': 'AA:BB:CC:DD:EE:FF'})
520
677
        parsed_result = json.loads(response.content)
521
678
 
529
686
        # A 'Bad Request' response is returned if one tries to add an invalid
530
687
        # MAC Address to a node.
531
688
        response = self.client.post(
532
 
            '/api/nodes/%s/macs/' % self.node.system_id,
 
689
            self.get_uri('nodes/%s/macs/') % self.node.system_id,
533
690
            {'mac_address': 'invalid-mac'})
534
691
        parsed_result = json.loads(response.content)
535
692
 
543
700
        # The api allows to delete a MAC Address.
544
701
        nb_macs = self.node.macaddress_set.count()
545
702
        response = self.client.delete(
546
 
            '/api/nodes/%s/macs/%s/' % (
 
703
            self.get_uri('nodes/%s/macs/%s/') % (
547
704
                self.node.system_id, self.mac1.mac_address))
548
705
 
549
706
        self.assertEqual(204, response.status_code)
557
714
        other_node = factory.make_node(
558
715
            status=NODE_STATUS.ALLOCATED, owner=factory.make_user())
559
716
        response = self.client.delete(
560
 
            '/api/nodes/%s/macs/%s/' % (
 
717
            self.get_uri('nodes/%s/macs/%s/') % (
561
718
                other_node.system_id, self.mac1.mac_address))
562
719
 
563
720
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
566
723
        # When deleting a MAC Address, the api returns a 'Not Found' (404)
567
724
        # error if no existing MAC Address is found.
568
725
        response = self.client.delete(
569
 
            '/api/nodes/%s/macs/%s/' % (
 
726
            self.get_uri('nodes/%s/macs/%s/') % (
570
727
                self.node.system_id, '00-aa-22-cc-44-dd'))
571
728
 
572
729
        self.assertEqual(httplib.NOT_FOUND, response.status_code)
575
732
        # When deleting a MAC Address, the api returns a 'Bad Request' (400)
576
733
        # error if the provided MAC Address is not valid.
577
734
        response = self.client.delete(
578
 
            '/api/nodes/%s/macs/%s/' % (
 
735
            self.get_uri('nodes/%s/macs/%s/') % (
579
736
                self.node.system_id, 'invalid-mac'))
580
737
 
581
738
        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
587
744
        # The api operation create_authorisation_token returns a json dict
588
745
        # with the consumer_key, the token_key and the token_secret in it.
589
746
        response = self.client.post(
590
 
            '/api/account/', {'op': 'create_authorisation_token'})
 
747
            self.get_uri('account/'), {'op': 'create_authorisation_token'})
591
748
        parsed_result = json.loads(response.content)
592
749
 
593
750
        self.assertEqual(
594
751
            ['consumer_key', 'token_key', 'token_secret'],
595
 
            sorted(parsed_result.keys()))
 
752
            sorted(parsed_result))
596
753
        self.assertIsInstance(parsed_result['consumer_key'], basestring)
597
754
        self.assertIsInstance(parsed_result['token_key'], basestring)
598
755
        self.assertIsInstance(parsed_result['token_secret'], basestring)
601
758
        # If the provided token_key does not exist (for the currently
602
759
        # logged-in user), the api returns a 'Not Found' (404) error.
603
760
        response = self.client.post(
604
 
            '/api/account/',
 
761
            self.get_uri('account/'),
605
762
            {'op': 'delete_authorisation_token', 'token_key': 'no-such-token'})
606
763
 
607
764
        self.assertEqual(httplib.NOT_FOUND, response.status_code)
611
768
        # delete_authorisation_token. It it is not present in the request's
612
769
        # parameters, the api returns a 'Bad Request' (400) error.
613
770
        response = self.client.post(
614
 
            '/api/account/', {'op': 'delete_authorisation_token'})
 
771
            self.get_uri('account/'), {'op': 'delete_authorisation_token'})
615
772
 
616
773
        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
617
774
 
648
805
    def make_API_POST_request(self, op=None, filename=None, fileObj=None):
649
806
        """Make an API POST request and return the response."""
650
807
        params = self._create_API_params(op, filename, fileObj)
651
 
        return self.client.post("/api/files/", params)
 
808
        return self.client.post(self.get_uri('files/'), params)
652
809
 
653
810
    def make_API_GET_request(self, op=None, filename=None, fileObj=None):
654
811
        """Make an API GET request and return the response."""
655
812
        params = self._create_API_params(op, filename, fileObj)
656
 
        return self.client.get("/api/files/", params)
 
813
        return self.client.get(self.get_uri('files/'), params)
657
814
 
658
815
    def test_add_file_succeeds(self):
659
816
        filepath = self.make_file()
686
843
 
687
844
        with open(filepath) as f, open(filepath2) as f2:
688
845
            response = self.client.post(
689
 
                "/api/files/",
 
846
                self.get_uri('files/'),
690
847
                {
691
848
                    "op": "add",
692
849
                    "filename": "foo",
735
892
        self.assertEqual(httplib.NOT_FOUND, response.status_code)
736
893
        self.assertIn('text/plain', response['Content-Type'])
737
894
        self.assertEqual("File not found", response.content)
 
895
 
 
896
 
 
897
class MaaSAPIAnonTest(APIv10TestMixin, TestCase):
 
898
    # The MaaS' handler is not accessible to anon users.
 
899
 
 
900
    def test_anon_get_config_forbidden(self):
 
901
        response = self.client.get(
 
902
            self.get_uri('maas/'),
 
903
            {'op': 'get_config'})
 
904
 
 
905
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
906
 
 
907
    def test_anon_set_config_forbidden(self):
 
908
        response = self.client.post(
 
909
            self.get_uri('maas/'),
 
910
            {'op': 'set_config'})
 
911
 
 
912
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
913
 
 
914
 
 
915
class MaaSAPITest(APITestCase):
 
916
 
 
917
    def test_simple_user_get_config_forbidden(self):
 
918
        response = self.client.get(
 
919
            self.get_uri('maas/'),
 
920
            {'op': 'get_config'})
 
921
 
 
922
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
923
 
 
924
    def test_simple_user_set_config_forbidden(self):
 
925
        response = self.client.post(
 
926
            self.get_uri('maas/'),
 
927
            {'op': 'set_config'})
 
928
 
 
929
        self.assertEqual(httplib.FORBIDDEN, response.status_code)
 
930
 
 
931
    def test_get_config_requires_name_param(self):
 
932
        self.become_admin()
 
933
        response = self.client.get(
 
934
            self.get_uri('maas/'),
 
935
            {
 
936
                'op': 'get_config',
 
937
            })
 
938
 
 
939
        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
 
940
        self.assertEqual("No provided name!", response.content)
 
941
 
 
942
    def test_get_config_returns_config(self):
 
943
        self.become_admin()
 
944
        name = factory.getRandomString()
 
945
        value = factory.getRandomString()
 
946
        Config.objects.set_config(name, value)
 
947
        response = self.client.get(
 
948
            self.get_uri('maas/'),
 
949
            {
 
950
                'op': 'get_config',
 
951
                'name': name,
 
952
            })
 
953
 
 
954
        self.assertEqual(httplib.OK, response.status_code)
 
955
        parsed_result = json.loads(response.content)
 
956
        self.assertIn('application/json', response['Content-Type'])
 
957
        self.assertEqual(value, parsed_result)
 
958
 
 
959
    def test_set_config_requires_name_param(self):
 
960
        self.become_admin()
 
961
        response = self.client.post(
 
962
            self.get_uri('maas/'),
 
963
            {
 
964
                'op': 'set_config',
 
965
                'value': factory.getRandomString(),
 
966
            })
 
967
 
 
968
        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
 
969
        self.assertEqual("No provided name!", response.content)
 
970
 
 
971
    def test_set_config_requires_string_name_param(self):
 
972
        self.become_admin()
 
973
        value = factory.getRandomString()
 
974
        response = self.client.post(
 
975
            self.get_uri('maas/'),
 
976
            {
 
977
                'op': 'set_config',
 
978
                'name': '',  # Invalid empty name.
 
979
                'value': value,
 
980
            })
 
981
 
 
982
        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
 
983
        self.assertEqual(
 
984
           "Invalid name: Please enter a value", response.content)
 
985
 
 
986
    def test_set_config_requires_value_param(self):
 
987
        self.become_admin()
 
988
        response = self.client.post(
 
989
            self.get_uri('maas/'),
 
990
            {
 
991
                'op': 'set_config',
 
992
                'name': factory.getRandomString(),
 
993
            })
 
994
 
 
995
        self.assertEqual(httplib.BAD_REQUEST, response.status_code)
 
996
        self.assertEqual("No provided value!", response.content)
 
997
 
 
998
    def test_admin_set_config(self):
 
999
        self.become_admin()
 
1000
        name = factory.getRandomString()
 
1001
        value = factory.getRandomString()
 
1002
        response = self.client.post(
 
1003
            self.get_uri('maas/'),
 
1004
            {
 
1005
                'op': 'set_config',
 
1006
                'name': name,
 
1007
                'value': value,
 
1008
            })
 
1009
 
 
1010
        self.assertEqual(httplib.OK, response.status_code)
 
1011
        stored_value = Config.objects.get_config(name)
 
1012
        self.assertEqual(stored_value, value)