Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / storage / test.py @ e3b8f796

History | View | Annotate | Download (17.5 kB)

1
# Copyright 2013 GRNET S.A. All rights reserved.
2
#
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
#
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
#
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
#
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from unittest import TestCase
35
from mock import patch, call
36
from tempfile import NamedTemporaryFile
37
from os import urandom
38

    
39
from kamaki.clients import ClientError
40
from kamaki.clients.storage import StorageClient as SC
41

    
42
client_pkg = 'kamaki.clients.Client'
43
storage_pkg = 'kamaki.clients.storage.StorageClient'
44

    
45
user_id = 'ac0un7-1d-5tr1ng'
46
obj = 'obj3c7N4m3'
47

    
48
account_info = {
49
    'content-language': 'en-us',
50
    'content-type': 'text/html; charset=utf-8',
51
    'date': 'Wed, 06 Mar 2013 13:25:51 GMT',
52
    'last-modified': 'Mon, 04 Mar 2013 18:22:31 GMT',
53
    'server': 'gunicorn/0.14.5',
54
    'vary': 'Accept-Language',
55
    'x-account-bytes-used': '751615526',
56
    'x-account-container-count': 7,
57
    'x-account-policy-quota': 53687091200,
58
    'x-account-policy-versioning': 'auto',
59
    'x-account-meta-k1': 'val1',
60
    'x-account-meta-k2': 'val2',
61
    'x-account-meta-k3': 'val3'}
62
container_info = {
63
    'content-language': 'en-us',
64
    'content-type': 'text/html; charset=utf-8',
65
    'date': 'Wed, 06 Mar 2013 15:11:05 GMT',
66
    'last-modified': 'Wed, 27 Feb 2013 15:56:13 GMT',
67
    'server': 'gunicorn/0.14.5',
68
    'vary': 'Accept-Language',
69
    'x-container-block-hash': 'sha256',
70
    'x-container-block-size': 4194304,
71
    'x-container-bytes-used': 309528938,
72
    'x-container-object-count': 14,
73
    'x-container-object-meta': '',
74
    'x-container-policy-quota': 53687091200,
75
    'x-container-policy-versioning': 'auto'}
76
object_info = {
77
    'content-language': 'en-us',
78
    'content-length': 254965,
79
    'content-type': 'application/octet-stream',
80
    'date': 'Thu, 07 Mar 2013 13:27:43 GMT',
81
    'etag': '',
82
    'last-modified': 'Mon, 04 Mar 2013 18:22:31 GMT',
83
    'server': 'gunicorn/0.14.5',
84
    'vary': 'Accept-Language',
85
    'x-object-hash': 'obj3c7h45h1s0bj3c7h45h411r34dY',
86
    'x-object-uuid': 'd0c747ca-34bd-49e0-8e98-1d07d8b0cbc7',
87
    'x-object-version': '525996',
88
    'x-object-version-timestamp': 'Mon, 04 Mar 2013 18:22:31 GMT',
89
    'x-object-meta-k1': 'v1',
90
    'x-object-meta-k2': 'v2'}
91
container_list = [
92
    dict(
93
        count=2,
94
        last_modified="2013-02-27T11:56:09.893033+00:00",
95
        bytes=677076979,
96
        name="pithos",
97
        x_container_policy=dict(quota="21474836480", versioning="auto")),
98
    dict(
99
        count=0,
100
        last_modified="2012-10-23T12:25:17.229187+00:00",
101
        bytes=0,
102
        name="trash",
103
        x_container_policy=dict(quota="21474836480", versioning="auto"))]
104
object_list = [
105
    dict(hash="",
106
        name="The_Secret_Garden.zip",
107
        x_object_public="/public/wdp9p",
108
        bytes=203304947,
109
        x_object_version_timestamp="1360237915.7027509",
110
        x_object_uuid="s0m3uu1df0r0bj0n3",
111
        last_modified="2013-02-07T11:51:55.702751+00:00",
112
        content_type="application/octet-stream",
113
        x_object_hash="0afdf29f71cd53126225c3f54ca",
114
        x_object_version=17737,
115
        x_object_modified_by=user_id),
116
    dict(hash="",
117
        name="The_Revealed_Garden.zip",
118
        x_object_public="/public/wpd7p",
119
        bytes=20330947,
120
        x_object_version_timestamp="13602915.7027509",
121
        x_object_uuid="s0m3uu1df0r0bj70w",
122
        last_modified="2013-02-07T11:51:55.702751+00:00",
123
        content_type="application/octet-stream",
124
        x_object_hash="0afdf29f71cd53126225c3f54ca",
125
        x_object_version=17737,
126
        x_object_modified_by=user_id)]
127
object_hashmap = dict(
128
    block_hash="sha256", block_size=4194304, bytes=33554432,
129
    hashes=[
130
        "4988438cc1c0292c085d289649b28cf547ba3db71c6efaac9f2df7e193d4d0af",
131
        "b214244aa56df7d1df7c6cac066e7cef268d9c2beb4dcf7ce68af667b0626f91",
132
        "17f365f25e0682565ded30576066bb13377a3d306967e4d74e06bb6bbc20f75f",
133
        "2524ae208932669fff89adf8a2fc0df3b67736ca0d3aadce7a2ce640f142af37",
134
        "5d807a2129d2fcd3c221c3da418ed52af3fc48d0817b62e0bb437acffccd3514",
135
        "609de22ce842d997f645fc49d5f14e0e3766dd51a6cbe66383b2bab82c8dfcd0",
136
        "3102851ac168c78be70e35ff5178c7b1ebebd589e5106d565ca1094d1ca8ff59",
137
        "bfe306dd24e92a8d85caf7055643f250fd319e8c4cdd4755ddabbf3ff97e83c7"])
138
sharers = [
139
    dict(last_modified="2013-01-29T16:50:06.084674+00:00", name="0b1a-82d5"),
140
    dict(last_modified="2013-01-29T16:50:06.084674+00:00", name="0b2a-f2d5"),
141
    dict(last_modified="2013-01-29T16:50:06.084674+00:00", name="2b1a-82d6")]
142

    
143

    
144
class FR(object):
145
    """FR stands for Fake Response"""
146
    json = dict()
147
    headers = dict()
148
    content = json
149
    status = None
150
    status_code = 200
151

    
152
    def release(self):
153
        pass
154

    
155

    
156
class Storage(TestCase):
157

    
158
    files = []
159

    
160
    def _create_temp_file(self, num_of_blocks):
161
        self.files.append(NamedTemporaryFile())
162
        tmpFile = self.files[-1]
163
        file_size = num_of_blocks * 4 * 1024 * 1024
164
        print('\n\tCreate tmp file')
165
        tmpFile.write(urandom(file_size))
166
        tmpFile.flush()
167
        tmpFile.seek(0)
168
        print('\t\tDone')
169
        return tmpFile
170

    
171
    def assert_dicts_are_equal(self, d1, d2):
172
        for k, v in d1.items():
173
            self.assertTrue(k in d2)
174
            if isinstance(v, dict):
175
                self.assert_dicts_are_equal(v, d2[k])
176
            else:
177
                self.assertEqual(unicode(v), unicode(d2[k]))
178

    
179
    def setUp(self):
180
        self.url = 'https://www.example.com/pithos'
181
        self.token = 'p17h0570k3n'
182
        self.client = SC(self.url, self.token)
183
        self.client.account = user_id
184
        self.client.container = 'c0nt@1n3r_i'
185

    
186
    def tearDown(self):
187
        FR.headers = dict()
188
        FR.status_code = 200
189
        FR.json = dict()
190
        FR.content = FR.json
191
        for f in self.files:
192
            f.close()
193

    
194
    #  Pithos+ methods that extend storage API
195

    
196
    @patch('%s.head' % client_pkg, return_value=FR())
197
    def test_get_account_info(self, head):
198
        FR.headers = account_info
199
        r = self.client.get_account_info()
200
        self.assert_dicts_are_equal(account_info, r)
201
        self.assertEqual(
202
            head.mock_calls[-1],
203
            call('/%s' % self.client.account, success=(204, 401)))
204
        FR.status_code = 401
205
        self.assertRaises(ClientError, self.client.get_account_info)
206

    
207
    @patch('%s.post' % storage_pkg, return_value=FR())
208
    @patch('%s.set_header' % storage_pkg)
209
    def test_replace_account_meta(self, SH, post):
210
        metas = dict(k1='v1', k2='v2', k3='v3')
211
        self.client.replace_account_meta(metas)
212
        prfx = 'X-Account-Meta-'
213
        expected = [call('%s%s' % (prfx, k), v) for k, v in metas.items()]
214
        self.assertEqual(SH.mock_calls, expected)
215
        self.assertEqual(
216
            post.mock_calls[-1],
217
            call('/%s' % self.client.account, success=202))
218

    
219
    @patch('%s.post' % storage_pkg, return_value=FR())
220
    @patch('%s.get_account_info' % storage_pkg, return_value=account_info)
221
    def test_del_account_meta(self, GAI, post):
222
        prfx = 'x-account-meta-'
223
        keys = [k[len(prfx):] for k in account_info if k.startswith(prfx)]
224
        for key in keys:
225
            self.client.del_account_meta(key)
226
            self.assertEqual(
227
                post.mock_calls[-1],
228
                call('/%s' % self.client.account, success=202))
229
        self.assertEqual(len(keys), len(post.mock_calls))
230
        self.assertRaises(ClientError, self.client.del_account_meta, 'k4')
231

    
232
    @patch('%s.put' % storage_pkg, return_value=FR())
233
    def test_create_container(self, put):
234
        cont = 's0m3c0n731n3r'
235
        self.client.create_container(cont)
236
        expected = call('/%s/%s' % (user_id, cont), success=(201, 202))
237
        self.assertEqual(put.mock_calls[-1], expected)
238
        FR.status_code = 202
239
        self.assertRaises(ClientError, self.client.create_container, cont)
240

    
241
    @patch('%s.head' % storage_pkg, return_value=FR())
242
    def test_get_container_info(self, head):
243
        FR.headers = container_info
244
        cont = self.client.container
245
        r = self.client.get_container_info(cont)
246
        self.assert_dicts_are_equal(r, container_info)
247
        path = '/%s/%s' % (self.client.account, cont)
248
        self.assertEqual(head.mock_calls[-1], call(path, success=(204, 404)))
249
        FR.status_code = 404
250
        self.assertRaises(ClientError, self.client.get_container_info, cont)
251

    
252
    @patch('%s.delete' % storage_pkg, return_value=FR())
253
    def test_delete_container(self, delete):
254
        FR.status_code = 204
255
        cont = 's0m3c0n731n3r'
256
        self.client.delete_container(cont)
257
        for err_code in (404, 409):
258
            FR.status_code = err_code
259
            self.assertRaises(ClientError, self.client.delete_container, cont)
260
        acall = call('/%s/%s' % (user_id, cont), success=(204, 404, 409))
261
        self.assertEqual(delete.mock_calls, [acall] * 3)
262

    
263
    @patch('%s.get' % storage_pkg, return_value=FR())
264
    @patch('%s.set_param' % storage_pkg)
265
    def test_list_containers(self, SP, get):
266
        FR.json = container_list
267
        r = self.client.list_containers()
268
        for i in range(len(r)):
269
            self.assert_dicts_are_equal(r[i], container_list[i])
270
        self.assertEqual(SP.mock_calls[-1], call('format', 'json'))
271
        self.assertEqual(
272
            get.mock_calls[-1],
273
            call('/%s' % self.client.account, success=(200., 204)))
274

    
275
    @patch('%s.put' % storage_pkg, return_value=FR())
276
    def test_upload_object(self, put):
277
        (acc, cont) = (self.client.account, self.client.container)
278
        num_of_blocks = 4
279
        tmpFile = self._create_temp_file(num_of_blocks)
280
        tmpFile.seek(0, 2)
281
        sizes = [None, (tmpFile.tell() / num_of_blocks) / 2]
282
        for size in sizes:
283
            tmpFile.seek(0)
284
            self.client.upload_object(obj, tmpFile, size)
285
            tmpFile.seek(0)
286
            self.assertEqual(put.mock_calls[-1], call(
287
                '/%s/%s/%s' % (acc, cont, obj),
288
                data=tmpFile.read(size) if size else tmpFile.read(),
289
                success=201))
290

    
291
    @patch('%s.put' % storage_pkg, return_value=FR())
292
    @patch('%s.set_header' % storage_pkg)
293
    def test_create_object(self, SH, put):
294
        cont = self.client.container
295
        ctype = 'c0n73n7/typ3'
296
        exp_shd = [
297
            call('Content-Type', 'application/octet-stream'),
298
            call('Content-length', '0'),
299
            call('Content-Type', ctype), call('Content-length', '42')]
300
        exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)] * 2
301
        self.client.create_object(obj)
302
        self.client.create_object(obj, content_type=ctype, content_length=42)
303
        self.assertEqual(SH.mock_calls, exp_shd)
304
        self.assertEqual(put.mock_calls, exp_put)
305

    
306
    @patch('%s.put' % storage_pkg, return_value=FR())
307
    @patch('%s.set_header' % client_pkg)
308
    def test_create_directory(self, SH, put):
309
        cont = self.client.container
310
        exp_shd = [
311
            call('Content-Type', 'application/directory'),
312
            call('Content-length', '0')]
313
        exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)]
314
        self.client.create_directory(obj)
315
        self.assertEqual(SH.mock_calls, exp_shd)
316
        self.assertEqual(put.mock_calls, exp_put)
317

    
318
    @patch('%s.head' % storage_pkg, return_value=FR())
319
    def test_get_object_info(self, head):
320
        FR.headers = object_info
321
        path = '/%s/%s/%s' % (self.client.account, self.client.container, obj)
322
        r = self.client.get_object_info(obj)
323
        self.assertEqual(r, object_info)
324
        self.assertEqual(head.mock_calls[-1], call(path, success=200))
325

    
326
    @patch('%s.get_object_info' % storage_pkg, return_value=object_info)
327
    def test_get_object_meta(self, GOI):
328
        r = self.client.get_object_meta(obj)
329
        prfx = 'x-object-meta-'
330
        for k in [k for k in object_info if k.startswith(prfx)]:
331
            self.assertEqual(r.pop(k[len(prfx):]), object_info[k])
332
        self.assertFalse(len(r))
333
        self.assertEqual(GOI.mock_calls[-1], call(obj))
334

    
335
    @patch('%s.post' % storage_pkg, return_value=FR())
336
    @patch('%s.set_header' % storage_pkg)
337
    def test_del_object_meta(self, SH, post):
338
        key = '50m3m3t4k3y'
339
        self.client.del_object_meta(obj, key)
340
        prfx = 'X-Object-Meta-'
341
        self.assertEqual(SH.mock_calls[-1], call('%s%s' % (prfx, key), ''))
342
        self.assertEqual(post.mock_calls[-1], call(
343
            '/%s/%s/%s' % (self.client.account, self.client.container, obj),
344
            success=202))
345

    
346
    @patch('%s.post' % client_pkg, return_value=FR())
347
    @patch('%s.set_header' % client_pkg)
348
    def test_replace_object_meta(self, SH, post):
349
        metas = dict(k1='new1', k2='new2', k3='new3')
350
        cont = self.client.container
351
        self.client.replace_object_meta(metas)
352
        expected = call('/%s/%s' % (user_id, cont), success=202)
353
        self.assertEqual(post.mock_calls[-1], expected)
354
        prfx = 'X-Object-Meta-'
355
        expected = [call('%s%s' % (prfx, k), v) for k, v in metas.items()]
356
        self.assertEqual(SH.mock_calls, expected)
357

    
358
    @patch('%s.put' % storage_pkg, return_value=FR())
359
    @patch('%s.set_header' % storage_pkg)
360
    def test_copy_object(self, SH, put):
361
        src_cont = 'src-c0nt41n3r'
362
        src_obj = 'src-0bj'
363
        dst_cont = 'dst-c0nt41n3r'
364
        for dst_obj in (None, 'dst-0bj'):
365
            dst_path = dst_obj or src_obj
366
            path = '/%s/%s/%s' % (self.client.account, dst_cont, dst_path)
367
            self.client.copy_object(src_cont, src_obj, dst_cont, dst_obj)
368
            self.assertEqual(put.mock_calls[-1], call(path, success=201))
369
            kwargs = {
370
                'X-Copy-From': '/%s/%s' % (src_cont, src_obj),
371
                'Content-Length': 0}
372
            self.assertEqual(
373
                SH.mock_calls[-2:],
374
                [call(k, v) for k, v in kwargs.items()])
375

    
376
    @patch('%s.put' % storage_pkg, return_value=FR())
377
    @patch('%s.set_header' % storage_pkg)
378
    def test_move_object(self, SH, put):
379
        src_cont = 'src-c0nt41n3r'
380
        src_obj = 'src-0bj'
381
        dst_cont = 'dst-c0nt41n3r'
382
        for dst_obj in (None, 'dst-0bj'):
383
            dst_path = dst_obj or src_obj
384
            path = '/%s/%s/%s' % (self.client.account, dst_cont, dst_path)
385
            self.client.move_object(src_cont, src_obj, dst_cont, dst_obj)
386
            self.assertEqual(put.mock_calls[-1], call(path, success=201))
387
            kwargs = {
388
                'X-Move-From': '/%s/%s' % (src_cont, src_obj),
389
                'Content-Length': 0}
390
            self.assertEqual(
391
                SH.mock_calls[-2:],
392
                [call(k, v) for k, v in kwargs.items()])
393

    
394
    @patch('%s.delete' % storage_pkg, return_value=FR())
395
    def test_delete_object(self, delete):
396
        cont = self.client.container
397
        self.client.delete_object(obj)
398
        self.assertEqual(
399
            delete.mock_calls[-1],
400
            call('/%s/%s/%s' % (user_id, cont, obj), success=(204, 404)))
401
        FR.status_code = 404
402
        self.assertRaises(ClientError, self.client.delete_object, obj)
403

    
404
    @patch('%s.get' % client_pkg, return_value=FR())
405
    @patch('%s.set_param' % client_pkg)
406
    def test_list_objects(self, SP, get):
407
        FR.json = object_list
408
        acc, cont = self.client.account, self.client.container
409
        r = self.client.list_objects()
410
        for i in range(len(r)):
411
            self.assert_dicts_are_equal(r[i], object_list[i])
412
        self.assertEqual(
413
            get.mock_calls[-1],
414
            call('/%s/%s' % (acc, cont), success=(200, 204, 304, 404)))
415
        self.assertEqual(SP.mock_calls[-1], call('format', 'json'))
416
        FR.status_code = 304
417
        self.assertEqual(self.client.list_objects(), [])
418
        FR.status_code = 404
419
        self.assertRaises(ClientError, self.client.list_objects)
420

    
421
    @patch('%s.get' % client_pkg, return_value=FR())
422
    @patch('%s.set_param' % client_pkg)
423
    def test_list_objects_in_path(self, SP, get):
424
        FR.json = object_list
425
        path = '/some/awsome/path'
426
        acc, cont = self.client.account, self.client.container
427
        self.client.list_objects_in_path(path)
428
        self.assertEqual(
429
            get.mock_calls[-1],
430
            call('/%s/%s' % (acc, cont), success=(200, 204, 404)))
431
        self.assertEqual(SP.mock_calls, [
432
            call('format', 'json'), call('path', path)])
433
        FR.status_code = 404
434
        self.assertRaises(ClientError, self.client.list_objects)
435

    
436
if __name__ == '__main__':
437
    from sys import argv
438
    from kamaki.clients.test import runTestCase
439
    runTestCase(Storage, 'Storage Client', argv[1:])