Revision 23808592 snf-cyclades-app/synnefo/plankton/tests.py

b/snf-cyclades-app/synnefo/plankton/tests.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
1
# Copyright 2012-2014 GRNET S.A. All rights reserved.
2 2
#
3 3
# Redistribution and use in source and binary forms, with or
4 4
# without modification, are permitted provided that the following
......
36 36
from mock import patch
37 37
from functools import wraps
38 38
from copy import deepcopy
39
from decimal import Decimal
39 40
from snf_django.utils.testing import BaseAPITest
40 41
from synnefo.cyclades_settings import cyclades_services
41 42
from synnefo.lib.services import get_service_path
42 43
from synnefo.lib import join_urls
43 44

  
44

  
45
class PlanktonAPITest(BaseAPITest):
46
    def setUp(self, *args, **kwargs):
47
        super(PlanktonAPITest, self).setUp(*args, **kwargs)
48
        self.api_path = get_service_path(cyclades_services, 'image',
49
                                             version='v1.0')
50
    def myget(self, path, *args, **kwargs):
51
        path = join_urls(self.api_path, path)
52
        return self.get(path, *args, **kwargs)
53

  
54
    def myput(self, path, *args, **kwargs):
55
        path = join_urls(self.api_path, path)
56
        return self.put(path, *args, **kwargs)
57

  
58
    def mypost(self, path, *args, **kwargs):
59
        path = join_urls(self.api_path, path)
60
        return self.post(path, *args, **kwargs)
61

  
62
    def mydelete(self, path, *args, **kwargs):
63
        path = join_urls(self.api_path, path)
64
        return self.delete(path, *args, **kwargs)
65

  
66

  
67
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
68
           'size_max')
69
PARAMS = ('sort_key', 'sort_dir')
70
SORT_KEY_OPTIONS = ('id', 'name', 'status', 'size', 'disk_format',
71
                    'container_format', 'created_at', 'updated_at')
72
SORT_DIR_OPTIONS = ('asc', 'desc')
73
LIST_FIELDS = ('status', 'name', 'disk_format', 'container_format', 'size',
74
               'id')
75
DETAIL_FIELDS = ('name', 'disk_format', 'container_format', 'size', 'checksum',
76
                 'location', 'created_at', 'updated_at', 'deleted_at',
77
                 'status', 'is_public', 'owner', 'properties', 'id')
78
ADD_FIELDS = ('name', 'id', 'store', 'disk_format', 'container_format', 'size',
79
              'checksum', 'is_public', 'owner', 'properties', 'location')
80
UPDATE_FIELDS = ('name', 'disk_format', 'container_format', 'is_public',
81
                 'owner', 'properties', 'status')
82

  
83

  
84
DummyImages = {
85
 '0786a349-9725-48ec-8b86-8598eefc4043':
86
 {'checksum': u'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
87
  u'container_format': u'bare',
88
  'created_at': '2012-12-04 09:50:20',
89
  'deleted_at': '',
90
  u'disk_format': u'diskdump',
91
  'id': u'0786a349-9725-48ec-8b86-8598eefc4043',
92
  'is_public': True,
93
  'location': u'pithos://foo@example.com/container/foo3',
94
  u'name': u'dummyname',
95
  'owner': u'foo@example.com',
96
  'properties': {},
97
  'size': 500L,
98
  u'status': u'available',
99
  'store': 'pithos',
100
  'updated_at': '2012-12-04 09:50:54'},
101

  
102
 'd8aa85b8-410b-4550-953d-6797572534e6':
103
 {'checksum': u'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
104
  u'container_format': u'bare',
105
  'created_at': '2012-11-26 11:56:42',
106
  'deleted_at': '',
107
  u'disk_format': u'diskdump',
108
  'id': u'd8aa85b8-410b-4550-953d-6797572534e6',
109
  'is_public': False,
110
  'location': u'pithos://foo@example.com/container/private',
111
  u'name': u'dummyname2',
112
  'owner': u'foo@example.com',
113
  'properties': {},
114
  'size': 10000L,
115
  u'status': u'available',
116
  'store': 'pithos',
117
  'updated_at': '2012-11-26 11:57:09'},
118

  
119
 '264fb9ac-2458-421c-b460-6a765a92825c':
120
 {'checksum': u'0c6d0586744781218672fff2d7ed94cc32efb02a6a8eb589a0628f0e22bd5a7f',
121
  u'container_format': u'bare',
122
  'created_at': '2012-11-26 11:52:54',
123
  'deleted_at': '',
124
  u'disk_format': u'diskdump',
125
  'id': u'264fb9ac-2458-421c-b460-6a765a92825c',
126
  'is_public': True,
127
  'location': u'pithos://foo@example.com/container/baz.diskdump',
128
  u'name': u'"dummyname3"',
129
  'owner': u'foo@example.com',
130
  'properties': {u'description': u'Debian Squeeze Base System',
131
                 u'gui': u'No GUI',
132
                 u'kernel': u'2.6.32',
133
                 u'os': u'debian',
134
                 u'osfamily': u'linux',
135
                 u'root_partition': u'1',
136
                 u'size': u'451',
137
                 u'sortorder': u'1',
138
                 u'users': u'root'},
139
  'size': 473772032L,
140
  u'status': u'available',
141
  'store': 'pithos',
142
  'updated_at': '2012-11-26 11:55:40'}}
45
PLANKTON_URL = get_service_path(cyclades_services, 'image',
46
                                version='v1.0')
47
IMAGES_URL = join_urls(PLANKTON_URL, "images/")
143 48

  
144 49

  
145 50
def assert_backend_closed(func):
......
152 57
    return wrapper
153 58

  
154 59

  
155
@patch("synnefo.plankton.backend.ImageBackend")
156
class PlanktonTest(PlanktonAPITest):
157
    @assert_backend_closed
158
    def test_list_images(self, backend):
159
        backend.return_value.list_images.return_value =\
160
                deepcopy(DummyImages).values()
161
        response = self.myget("images/")
162
        self.assertSuccess(response)
163
        images = json.loads(response.content)
164
        for api_image in images:
165
            id = api_image['id']
166
            pithos_image = dict([(key, val)\
167
                                for key, val in DummyImages[id].items()\
168
                                if key in LIST_FIELDS])
169
            self.assertEqual(api_image, pithos_image)
170
        backend.return_value\
171
                .list_images.assert_called_once_with({}, {'sort_key': 'created_at',
172
                                                   'sort_dir': 'desc'})
60
@patch("synnefo.plankton.backend.get_pithos_backend")
61
class PlanktonTest(BaseAPITest):
62
    def test_register_image(self, backend):
63
        required = {
64
            "HTTP_X_IMAGE_META_NAME": u"TestImage\u2602",
65
            "HTTP_X_IMAGE_META_LOCATION": "pithos://4321-4321/images/foo"}
66
        # Check valid name
67
        headers = deepcopy(required)
68
        headers.pop("HTTP_X_IMAGE_META_NAME")
69
        response = self.post(IMAGES_URL, **headers)
70
        self.assertBadRequest(response)
71
        self.assertTrue("name" in response.content)
72
        headers["HTTP_X_IMAGE_META_NAME"] = ""
73
        response = self.post(IMAGES_URL, **headers)
74
        self.assertBadRequest(response)
75
        self.assertTrue("name" in response.content)
76
        # Check valid location
77
        headers = deepcopy(required)
78
        headers.pop("HTTP_X_IMAGE_META_LOCATION")
79
        response = self.post(IMAGES_URL, **headers)
80
        self.assertBadRequest(response)
81
        self.assertTrue("location" in response.content)
82
        headers["HTTP_X_IMAGE_META_LOCATION"] = ""
83
        response = self.post(IMAGES_URL, **headers)
84
        self.assertBadRequest(response)
85
        self.assertTrue("location" in response.content)
86
        headers["HTTP_X_IMAGE_META_LOCATION"] = "pitho://4321-4321/images/foo"
87
        response = self.post(IMAGES_URL, **headers)
88
        self.assertBadRequest(response)
89
        self.assertTrue("location" in response.content)
90
        headers["HTTP_X_IMAGE_META_LOCATION"] = "pithos://4321-4321/foo"
91
        response = self.post(IMAGES_URL, **headers)
92
        self.assertBadRequest(response)
93
        self.assertTrue("location" in response.content)
94
        # ID not supported
95
        headers = deepcopy(required)
96
        headers["HTTP_X_IMAGE_META_ID"] = "1234"
97
        response = self.post(IMAGES_URL, **headers)
98
        self.assertBadRequest(response)
99
        headers = deepcopy(required)
100
        # ID not supported
101
        headers = deepcopy(required)
102
        headers["HTTP_X_IMAGE_META_LOLO"] = "1234"
103
        response = self.post(IMAGES_URL, **headers)
104
        self.assertBadRequest(response)
105
        headers = deepcopy(required)
106
        headers["HTTP_X_IMAGE_META_STORE"] = "pitho"
107
        response = self.post(IMAGES_URL, **headers)
108
        self.assertBadRequest(response)
109
        self.assertTrue("store " in response.content)
110
        headers = deepcopy(required)
111
        headers["HTTP_X_IMAGE_META_DISK_FORMAT"] = "diskdumpp"
112
        response = self.post(IMAGES_URL, **headers)
113
        self.assertBadRequest(response)
114
        self.assertTrue("disk format" in response.content)
115
        headers = deepcopy(required)
116
        headers["HTTP_X_IMAGE_META_CONTAINER_FORMAT"] = "baree"
117
        response = self.post(IMAGES_URL, **headers)
118
        self.assertBadRequest(response)
119
        self.assertTrue("container format" in response.content)
173 120

  
174
    @assert_backend_closed
175
    def test_list_images_detail(self, backend):
176
        backend.return_value.list_images.return_value =\
177
                deepcopy(DummyImages).values()
178
        response = self.myget("images/detail")
179
        self.assertSuccess(response)
180
        images = json.loads(response.content)
181
        for api_image in images:
182
            id = api_image['id']
183
            pithos_image = dict([(key, val)\
184
                                for key, val in DummyImages[id].items()\
185
                                if key in DETAIL_FIELDS])
186
            self.assertEqual(api_image, pithos_image)
187
        backend.return_value\
188
                .list_images.assert_called_once_with({}, {'sort_key': 'created_at',
189
                                                   'sort_dir': 'desc'})
121
        backend().get_object_meta.return_value = {"uuid": "1234-1234-1234",
122
                                                  "bytes": 42,
123
                                                  "hash": "unique_hash"}
124
        headers = deepcopy(required)
125
        headers["HTTP_X_IMAGE_META_SIZE"] = "foo"
126
        response = self.post(IMAGES_URL, **headers)
127
        self.assertBadRequest(response)
128
        self.assertTrue("size" in response.content)
129
        headers["HTTP_X_IMAGE_META_SIZE"] = "43"
130
        response = self.post(IMAGES_URL, **headers)
131
        self.assertBadRequest(response)
132
        self.assertTrue("size" in response.content)
133

  
134
        headers["HTTP_X_IMAGE_META_SIZE"] = 42
135
        headers["HTTP_X_IMAGE_META_CHECKSUM"] = "wrong_checksum"
136
        response = self.post(IMAGES_URL, **headers)
137
        self.assertBadRequest(response)
190 138

  
191
    @assert_backend_closed
192
    def test_list_images_filters(self, backend):
193
        backend.return_value.list_images.return_value =\
194
                deepcopy(DummyImages).values()
195
        response = self.myget("images/?size_max=1000")
139
        backend().get_uuid.return_value =\
140
            ("4321-4321", "images", "foo")
141
        backend().get_object_permissions.return_value = \
142
            ("foo", "foo", {"read": []})
143
        backend().get_object_meta.side_effect = \
144
            [{"uuid": "1234-1234-1234",
145
              "bytes": 42,
146
              "hash": "unique_hash"},
147
             {"uuid": "1234-1234-1234",
148
              "bytes": 42,
149
              "hash": "unique_hash",
150
              'version_timestamp': Decimal('1392487853.863673'),
151
              "plankton:name": u"TestImage\u2602",
152
              "plankton:container_format": "bare",
153
              "plankton:disk_format": "diskdump",
154
              "plankton:status": u"AVAILABLE"}]
155
        headers = deepcopy(required)
156
        response = self.post(IMAGES_URL, **headers)
196 157
        self.assertSuccess(response)
197
        backend.return_value\
198
                .list_images.assert_called_once_with({'size_max': 1000},
199
                                                     {'sort_key': 'created_at',
200
                                                     'sort_dir': 'desc'})
158
        self.assertEqual(response["x-image-meta-location"],
159
                         "pithos://4321-4321/images/foo")
160
        self.assertEqual(response["x-image-meta-id"], "1234-1234-1234")
161
        self.assertEqual(response["x-image-meta-status"], "AVAILABLE")
162
        self.assertEqual(response["x-image-meta-deleted-at"], "")
163
        self.assertEqual(response["x-image-meta-is-public"], "False")
164
        self.assertEqual(response["x-image-meta-owner"], "4321-4321")
165
        self.assertEqual(response["x-image-meta-size"], "42")
166
        self.assertEqual(response["x-image-meta-checksum"], "unique_hash")
167
        self.assertEqual(response["x-image-meta-name"],
168
                         u"TestImage\u2602".encode("utf-8"))
169
        self.assertEqual(response["x-image-meta-container-format"], "bare")
170
        self.assertEqual(response["x-image-meta-disk-format"], "diskdump")
171
        self.assertEqual(response["x-image-meta-created-at"],
172
                         "2014-02-15 18:10:53")
173
        self.assertEqual(response["x-image-meta-updated-at"],
174
                         "2014-02-15 18:10:53")
201 175

  
202
    @assert_backend_closed
203
    def test_list_images_filters_error_1(self, backend):
204
        response = self.myget("images/?size_max=")
205
        self.assertBadRequest(response)
176
        # Extra headers,properties
177
        backend().get_object_meta.side_effect = \
178
            [{"uuid": "1234-1234-1234",
179
              "bytes": 42,
180
              "hash": "unique_hash"},
181
             {"uuid": "1234-1234-1234",
182
              "bytes": 42,
183
              "hash": "unique_hash",
184
              'version_timestamp': Decimal('1392487853.863673'),
185
              "plankton:name": u"TestImage\u2602",
186
              "plankton:container_format": "bare",
187
              "plankton:disk_format": "diskdump",
188
              "plankton:status": u"AVAILABLE"}]
189
        headers = deepcopy(required)
190
        headers["HTTP_X_IMAGE_META_IS_PUBLIC"] = True
191
        headers["HTTP_X_IMAGE_META_PROPERTY_KEY1"] = "val1"
192
        headers["HTTP_X_IMAGE_META_PROPERTY_KEY2"] = u"\u2601"
193
        response = self.post(IMAGES_URL, **headers)
194
        name, args, kwargs = backend().update_object_meta.mock_calls[-1]
195
        metadata = args[5]
196
        self.assertEqual(metadata["plankton:property:key1"], "val1")
197
        self.assertEqual(metadata["plankton:property:key2"],
198
                         u"\u2601".encode("utf-8"))
199
        self.assertSuccess(response)
206 200

  
207
    @assert_backend_closed
208
    def test_list_images_filters_error_2(self, backend):
209
        response = self.myget("images/?size_min=foo")
210
        self.assertBadRequest(response)
201
    def test_unregister_image(self, backend):
202
        backend().get_uuid.return_value = ("img_owner", "images", "foo")
203
        backend().get_object_meta.return_value = {"uuid": "img_uuid",
204
                                                  "bytes": 42,
205
                                                  "plankton:name": "test"}
206
        response = self.delete(join_urls(IMAGES_URL, "img_uuid"))
207
        self.assertEqual(response.status_code, 204)
208
        backend().update_object_meta.assert_called_once_with(
209
            "user", "img_owner", "images", "foo", "plankton", {}, True)
211 210

  
212
    @assert_backend_closed
213
    def test_update_image(self, backend):
214
        db_image = DummyImages.values()[0]
215
        response = self.myput("images/%s" % db_image['id'],
216
                              json.dumps({}),
217
                              'json', HTTP_X_IMAGE_META_OWNER='user2')
211
    def test_users(self, backend):
212
        """Test adding/removing and replacing image members"""
213
        # Add user
214
        backend.reset_mock()
215
        backend().get_uuid.return_value = ("img_owner", "images", "foo")
216
        backend().get_object_permissions.return_value = \
217
            ("foo", "foo", {"read": []})
218
        backend().get_object_meta.return_value = {"uuid": "img_uuid",
219
                                                  "bytes": 42,
220
                                                  "plankton:name": "test"}
221
        response = self.put(join_urls(IMAGES_URL, "img_uuid/members/user1"),
222
                            user="user1")
218 223
        self.assertSuccess(response)
219
        backend.return_value.update_metadata.assert_called_once_with(db_image['id'],
220
                                                                     {"owner": "user2"})
224
        backend().update_object_permissions.assert_called_once_with(
225
            "user1", "img_owner", "images", "foo", {"read": ["user1"]})
221 226

  
222
    @assert_backend_closed
223
    def test_add_image_member(self, backend):
224
        image_id = DummyImages.values()[0]['id']
225
        response = self.myput("images/%s/members/user3" % image_id,
226
                              json.dumps({}), 'json')
227
        # Remove user
228
        backend().update_object_permissions.reset_mock()
229
        backend().get_object_permissions.return_value = \
230
            ("foo", "foo", {"read": ["user1"]})
231
        response = self.delete(join_urls(IMAGES_URL, "img_uuid/members/user1"),
232
                               user="user1")
227 233
        self.assertSuccess(response)
228
        backend.return_value.add_user.assert_called_once_with(image_id,
229
                                                             'user3')
234
        backend().update_object_permissions.assert_called_once_with(
235
            "user1", "img_owner", "images", "foo", {"read": []})
230 236

  
231
    @assert_backend_closed
232
    def test_remove_image_member(self, backend):
233
        image_id = DummyImages.values()[0]['id']
234
        response = self.mydelete("images/%s/members/user3" % image_id)
237
        # Update users
238
        backend().get_object_permissions.return_value = \
239
            ("foo", "foo", {"read": ["user1", "user2", "user3"]})
240
        backend().update_object_permissions.reset_mock()
241
        response = self.put(join_urls(IMAGES_URL, "img_uuid/members"),
242
                            params=json.dumps({"memberships":
243
                                               [{"member_id": "foo1"},
244
                                                {"member_id": "foo2"}]}),
245
                            ctype="json",
246
                            user="user1")
235 247
        self.assertSuccess(response)
236
        backend.return_value.remove_user.assert_called_once_with(image_id,
237
                                                                'user3')
248
        backend().update_object_permissions.assert_called_once_with(
249
            "user1", "img_owner", "images", "foo", {"read": ["foo1", "foo2"]})
238 250

  
239
    @assert_backend_closed
240
    def test_add_image(self, backend):
241
        location = "pithos://uuid/container/name/"
242
        response = self.mypost("images/",
243
                               json.dumps({}),
244
                               'json',
245
                               HTTP_X_IMAGE_META_NAME='dummy_name',
246
                               HTTP_X_IMAGE_META_OWNER='dummy_owner',
247
                               HTTP_X_IMAGE_META_LOCATION=location)
251
        # List users
252
        backend().get_object_permissions.return_value = \
253
            ("foo", "foo", {"read": ["user1", "user2", "user3"]})
254
        response = self.get(join_urls(IMAGES_URL, "img_uuid/members"))
248 255
        self.assertSuccess(response)
249
        backend.return_value.register.assert_called_once_with('dummy_name',
250
                                                              location,
251
                                                      {'owner': 'dummy_owner'})
256
        res_members = [{"member_id": m, "can_share": False}
257
                       for m in ["user1", "user2", "user3"]]
258
        self.assertEqual(json.loads(response.content)["members"], res_members)
252 259

  
253
    @assert_backend_closed
254
    def test_get_image(self, backend):
255
        response = self.myget("images/123")
256
        self.assertEqual(response.status_code, 501)
260
    def test_metadata(self, backend):
261
        backend().get_uuid.return_value = ("img_owner", "images", "foo")
262
        backend().get_object_meta.return_value = \
263
            {"uuid": "img_uuid",
264
             "bytes": 42,
265
             "hash": "unique_hash",
266
             'version_timestamp': Decimal('1392487853.863673'),
267
             "plankton:name": u"TestImage\u2602",
268
             "plankton:container_format": "bare",
269
             "plankton:disk_format": "diskdump",
270
             "plankton:status": u"AVAILABLE"}
271
        backend().get_object_permissions.return_value = \
272
            ("foo", "foo", {"read": ["*", "user1"]})
273
        response = self.head(join_urls(IMAGES_URL, "img_uuid2"))
274
        self.assertSuccess(response)
275
        self.assertEqual(response["x-image-meta-location"],
276
                         "pithos://img_owner/images/foo")
277
        self.assertEqual(response["x-image-meta-id"], "img_uuid")
278
        self.assertEqual(response["x-image-meta-status"], "AVAILABLE")
279
        self.assertEqual(response["x-image-meta-deleted-at"], "")
280
        self.assertEqual(response["x-image-meta-is-public"], "True")
281
        self.assertEqual(response["x-image-meta-owner"], "img_owner")
282
        self.assertEqual(response["x-image-meta-size"], "42")
283
        self.assertEqual(response["x-image-meta-checksum"], "unique_hash")
284
        self.assertEqual(response["x-image-meta-name"],
285
                         u"TestImage\u2602".encode("utf-8"))
286
        self.assertEqual(response["x-image-meta-container-format"], "bare")
287
        self.assertEqual(response["x-image-meta-disk-format"], "diskdump")
288
        self.assertEqual(response["x-image-meta-created-at"],
289
                         "2014-02-15 18:10:53")
290
        self.assertEqual(response["x-image-meta-updated-at"],
291
                         "2014-02-15 18:10:53")
292
        response = self.head(join_urls(IMAGES_URL, "img_uuid2"))
257 293

  
258
    @assert_backend_closed
259
    def test_delete_image(self, backend):
260
        response = self.mydelete("images/123")
261
        self.assertEqual(response.status_code, 204)
262
        backend.return_value.unregister.assert_called_once_with('123')
263
        backend.return_value._delete.assert_not_called()
294
        headers = {"HTTP_X_IMAGE_META_IS_PUBLIC": False,
295
                   "HTTP_X_IMAGE_META_PROPERTY_KEY1": "Val1"}
296
        response = self.put(join_urls(IMAGES_URL, "img_uuid"), **headers)
297
        self.assertSuccess(response)
298
        backend().update_object_permissions.assert_called_once_with(
299
            "user", "img_owner", "images", "foo", {"read": ["user1"]})
264 300

  
265
    @assert_backend_closed
266 301
    def test_catch_wrong_api_paths(self, *args):
267
        response = self.myget('nonexistent')
302
        response = self.get(join_urls(PLANKTON_URL, 'nonexistent'))
268 303
        self.assertEqual(response.status_code, 400)
269 304
        try:
270
            error = json.loads(response.content)
305
            json.loads(response.content)
271 306
        except ValueError:
272 307
            self.assertTrue(False)
308

  
309
    def test_list_images_filters_error_1(self, backend):
310
        response = self.get(join_urls(IMAGES_URL, "?size_max="))
311
        self.assertBadRequest(response)

Also available in: Unified diff