Statistics
| Branch: | Tag: | Revision:

root / kamaki / clients / storage / test.py @ 3f7e4e14

History | View | Annotate | Download (17.1 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

    
128

    
129
class FR(object):
130
    """FR stands for Fake Response"""
131
    json = dict()
132
    headers = dict()
133
    content = json
134
    status = None
135
    status_code = 200
136

    
137

    
138
class StorageClient(TestCase):
139

    
140
    files = []
141

    
142
    def _create_temp_file(self, num_of_blocks):
143
        self.files.append(NamedTemporaryFile())
144
        tmpFile = self.files[-1]
145
        file_size = num_of_blocks * 4 * 1024 * 1024
146
        print('\n\tCreate tmp file')
147
        tmpFile.write(urandom(file_size))
148
        tmpFile.flush()
149
        tmpFile.seek(0)
150
        print('\t\tDone')
151
        return tmpFile
152

    
153
    def assert_dicts_are_equal(self, d1, d2):
154
        for k, v in d1.items():
155
            self.assertTrue(k in d2)
156
            if isinstance(v, dict):
157
                self.assert_dicts_are_equal(v, d2[k])
158
            else:
159
                self.assertEqual(unicode(v), unicode(d2[k]))
160

    
161
    def setUp(self):
162
        self.url = 'https://www.example.com/pithos'
163
        self.token = 'p17h0570k3n'
164
        self.client = SC(self.url, self.token)
165
        self.client.account = user_id
166
        self.client.container = 'c0nt@1n3r_i'
167

    
168
    def tearDown(self):
169
        FR.headers = dict()
170
        FR.status_code = 200
171
        FR.json = dict()
172
        FR.content = FR.json
173
        for f in self.files:
174
            f.close()
175

    
176
    #  Pithos+ methods that extend storage API
177

    
178
    @patch('%s.head' % client_pkg, return_value=FR())
179
    def test_get_account_info(self, head):
180
        FR.headers = account_info
181
        r = self.client.get_account_info()
182
        self.assert_dicts_are_equal(account_info, r)
183
        head.assert_called_once_with(
184
            '/%s' % self.client.account,
185
            success=(204, 401))
186
        FR.status_code = 401
187
        self.assertRaises(ClientError, self.client.get_account_info)
188

    
189
    @patch('%s.post' % storage_pkg, return_value=FR())
190
    @patch('%s.set_header' % storage_pkg)
191
    def test_replace_account_meta(self, SH, post):
192
        metas = dict(k1='v1', k2='v2', k3='v3')
193
        self.client.replace_account_meta(metas)
194
        prfx = 'X-Account-Meta-'
195
        expected = [call('%s%s' % (prfx, k), v) for k, v in metas.items()]
196
        self.assertEqual(SH.mock_calls, expected)
197
        post.assert_called_once_with('/%s' % self.client.account, success=202)
198

    
199
    @patch('%s.post' % storage_pkg, return_value=FR())
200
    @patch('%s.get_account_info' % storage_pkg, return_value=account_info)
201
    def test_del_account_meta(self, GAI, post):
202
        prfx = 'x-account-meta-'
203
        keys = [k[len(prfx):] for k in account_info if k.startswith(prfx)]
204
        for key in keys:
205
            self.client.del_account_meta(key)
206
            self.assertEqual(
207
                post.mock_calls[-1],
208
                call('/%s' % self.client.account, success=202))
209
        self.assertEqual(len(keys), len(post.mock_calls))
210
        self.assertRaises(ClientError, self.client.del_account_meta, 'k4')
211

    
212
    @patch('%s.put' % storage_pkg, return_value=FR())
213
    def test_create_container(self, put):
214
        cont = 's0m3c0n731n3r'
215
        self.client.create_container(cont)
216
        args = (user_id, cont)
217
        put.assert_called_once_with('/%s/%s' % args, success=(201, 202))
218
        FR.status_code = 202
219
        self.assertRaises(ClientError, self.client.create_container, cont)
220

    
221
    @patch('%s.head' % storage_pkg, return_value=FR())
222
    def test_get_container_info(self, head):
223
        FR.headers = container_info
224
        cont = self.client.container
225
        r = self.client.get_container_info(cont)
226
        self.assert_dicts_are_equal(r, container_info)
227
        path = '/%s/%s' % (self.client.account, cont)
228
        head.assert_called_once_with(path, success=(204, 404))
229
        FR.status_code = 404
230
        self.assertRaises(ClientError, self.client.get_container_info, cont)
231

    
232
    @patch('%s.delete' % storage_pkg, return_value=FR())
233
    def test_delete_container(self, delete):
234
        FR.status_code = 204
235
        cont = 's0m3c0n731n3r'
236
        self.client.delete_container(cont)
237
        for err_code in (404, 409):
238
            FR.status_code = err_code
239
            self.assertRaises(ClientError, self.client.delete_container, cont)
240
        acall = call('/%s/%s' % (user_id, cont), success=(204, 404, 409))
241
        self.assertEqual(delete.mock_calls, [acall] * 3)
242

    
243
    @patch('%s.get' % storage_pkg, return_value=FR())
244
    @patch('%s.set_param' % storage_pkg)
245
    def test_list_containers(self, SP, get):
246
        FR.json, acc = container_list, self.client.account
247
        r = self.client.list_containers()
248
        SP.assert_called_once_with('format', 'json')
249
        get.assert_called_once_with('/%s' % acc, success=(200, 204))
250
        for i in range(len(r)):
251
            self.assert_dicts_are_equal(r[i], container_list[i])
252

    
253
    @patch('%s.put' % storage_pkg, return_value=FR())
254
    def test_upload_object(self, put):
255
        (acc, cont) = (self.client.account, self.client.container)
256
        num_of_blocks = 4
257
        tmpFile = self._create_temp_file(num_of_blocks)
258
        tmpFile.seek(0, 2)
259
        sizes = [None, (tmpFile.tell() / num_of_blocks) / 2]
260
        for size in sizes:
261
            tmpFile.seek(0)
262
            self.client.upload_object(obj, tmpFile, size)
263
            tmpFile.seek(0)
264
            self.assertEqual(put.mock_calls[-1], call(
265
                '/%s/%s/%s' % (acc, cont, obj),
266
                data=tmpFile.read(size) if size else tmpFile.read(),
267
                success=201))
268

    
269
    @patch('%s.put' % storage_pkg, return_value=FR())
270
    @patch('%s.set_header' % storage_pkg)
271
    def test_create_object(self, SH, put):
272
        cont = self.client.container
273
        ctype = 'c0n73n7/typ3'
274
        exp_shd = [
275
            call('Content-Type', 'application/octet-stream'),
276
            call('Content-length', '0'),
277
            call('Content-Type', ctype), call('Content-length', '42')]
278
        exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)] * 2
279
        self.client.create_object(obj)
280
        self.client.create_object(obj, content_type=ctype, content_length=42)
281
        self.assertEqual(SH.mock_calls, exp_shd)
282
        self.assertEqual(put.mock_calls, exp_put)
283

    
284
    @patch('%s.put' % storage_pkg, return_value=FR())
285
    @patch('%s.set_header' % client_pkg)
286
    def test_create_directory(self, SH, put):
287
        cont = self.client.container
288
        exp_shd = [
289
            call('Content-Type', 'application/directory'),
290
            call('Content-length', '0')]
291
        exp_put = [call('/%s/%s/%s' % (user_id, cont, obj), success=201)]
292
        self.client.create_directory(obj)
293
        self.assertEqual(SH.mock_calls, exp_shd)
294
        self.assertEqual(put.mock_calls, exp_put)
295

    
296
    @patch('%s.head' % storage_pkg, return_value=FR())
297
    def test_get_object_info(self, head):
298
        FR.headers = object_info
299
        path = '/%s/%s/%s' % (self.client.account, self.client.container, obj)
300
        r = self.client.get_object_info(obj)
301
        head.assert_called_once_with(path, success=200)
302
        self.assertEqual(r, object_info)
303

    
304
    @patch('%s.get_object_info' % storage_pkg, return_value=object_info)
305
    def test_get_object_meta(self, GOI):
306
        r = self.client.get_object_meta(obj)
307
        GOI.assert_called_once_with(obj)
308
        prfx = 'x-object-meta-'
309
        for k in [k for k in object_info if k.startswith(prfx)]:
310
            self.assertEqual(r.pop(k[len(prfx):]), object_info[k])
311
        self.assertFalse(len(r))
312

    
313
    @patch('%s.post' % storage_pkg, return_value=FR())
314
    @patch('%s.set_header' % storage_pkg)
315
    def test_del_object_meta(self, SH, post):
316
        key = '50m3m3t4k3y'
317
        self.client.del_object_meta(obj, key)
318
        prfx = 'X-Object-Meta-'
319
        SH.assert_called_once_with('%s%s' % (prfx, key), '')
320
        exp = '/%s/%s/%s' % (self.client.account, self.client.container, obj)
321
        post.assert_called_once_with(exp, success=202)
322

    
323
    @patch('%s.post' % client_pkg, return_value=FR())
324
    @patch('%s.set_header' % client_pkg)
325
    def test_replace_object_meta(self, SH, post):
326
        metas = dict(k1='new1', k2='new2', k3='new3')
327
        cont = self.client.container
328
        self.client.replace_object_meta(metas)
329
        post.assert_called_once_with('/%s/%s' % (user_id, cont), success=202)
330
        prfx = 'X-Object-Meta-'
331
        expected = [call('%s%s' % (prfx, k), v) for k, v in metas.items()]
332
        self.assertEqual(SH.mock_calls, expected)
333

    
334
    @patch('%s.put' % storage_pkg, return_value=FR())
335
    @patch('%s.set_header' % storage_pkg)
336
    def test_copy_object(self, SH, put):
337
        src_cont = 'src-c0nt41n3r'
338
        src_obj = 'src-0bj'
339
        dst_cont = 'dst-c0nt41n3r'
340
        for dst_obj in (None, 'dst-0bj'):
341
            dst_path = dst_obj or src_obj
342
            path = '/%s/%s/%s' % (self.client.account, dst_cont, dst_path)
343
            self.client.copy_object(src_cont, src_obj, dst_cont, dst_obj)
344
            self.assertEqual(put.mock_calls[-1], call(path, success=201))
345
            kwargs = {
346
                'X-Copy-From': '/%s/%s' % (src_cont, src_obj),
347
                'Content-Length': 0}
348
            self.assertEqual(
349
                SH.mock_calls[-2:],
350
                [call(k, v) for k, v in kwargs.items()])
351

    
352
    @patch('%s.put' % storage_pkg, return_value=FR())
353
    @patch('%s.set_header' % storage_pkg)
354
    def test_move_object(self, SH, put):
355
        src_cont = 'src-c0nt41n3r'
356
        src_obj = 'src-0bj'
357
        dst_cont = 'dst-c0nt41n3r'
358
        for dst_obj in (None, 'dst-0bj'):
359
            dst_path = dst_obj or src_obj
360
            path = '/%s/%s/%s' % (self.client.account, dst_cont, dst_path)
361
            self.client.move_object(src_cont, src_obj, dst_cont, dst_obj)
362
            self.assertEqual(put.mock_calls[-1], call(path, success=201))
363
            kwargs = {
364
                'X-Move-From': '/%s/%s' % (src_cont, src_obj),
365
                'Content-Length': 0}
366
            self.assertEqual(
367
                SH.mock_calls[-2:],
368
                [call(k, v) for k, v in kwargs.items()])
369

    
370
    @patch('%s.delete' % storage_pkg, return_value=FR())
371
    def test_delete_object(self, delete):
372
        cont = self.client.container
373
        self.client.delete_object(obj)
374
        exp = '/%s/%s/%s' % (user_id, cont, obj)
375
        delete.assert_called_once_with(exp, success=(204, 404))
376
        FR.status_code = 404
377
        self.assertRaises(ClientError, self.client.delete_object, obj)
378

    
379
    @patch('%s.get' % client_pkg, return_value=FR())
380
    @patch('%s.set_param' % client_pkg)
381
    def test_list_objects(self, SP, get):
382
        FR.json = object_list
383
        acc, cont = self.client.account, self.client.container
384
        r = self.client.list_objects()
385
        for i in range(len(r)):
386
            self.assert_dicts_are_equal(r[i], object_list[i])
387
        exp = '/%s/%s' % (acc, cont)
388
        get.assert_called_once_with(exp, success=(200, 204, 304, 404))
389
        self.assertEqual(SP.mock_calls, [
390
            call('format', 'json'),
391
            call('limit', None, iff=None),
392
            call('marker', None, iff=None),
393
            call('prefix', None, iff=None),
394
            call('delimiter', None, iff=None)])
395
        self.client.list_objects(
396
            format='xml', limit=10, marker='X', path='/lala')
397
        self.assertEqual(SP.mock_calls[-4:], [
398
            call('format', 'xml'),
399
            call('limit', 10, iff=10),
400
            call('marker', 'X', iff='X'),
401
            call('path', '/lala')])
402
        self.client.list_objects(delimiter='X', prefix='/lala')
403
        self.assertEqual(SP.mock_calls[-5:], [
404
            call('format', 'json'),
405
            call('limit', None, iff=None),
406
            call('marker', None, iff=None),
407
            call('prefix', '/lala', iff='/lala'),
408
            call('delimiter', 'X', iff='X'),
409
            ])
410
        FR.status_code = 304
411
        self.assertEqual(self.client.list_objects(), [])
412
        FR.status_code = 404
413
        self.assertRaises(ClientError, self.client.list_objects)
414

    
415
    @patch('%s.get' % client_pkg, return_value=FR())
416
    @patch('%s.set_param' % client_pkg)
417
    def test_list_objects_in_path(self, SP, get):
418
        FR.json = object_list
419
        path = '/some/awsome/path'
420
        acc, cont = self.client.account, self.client.container
421
        self.client.list_objects_in_path(path)
422
        exp = '/%s/%s' % (acc, cont)
423
        get.assert_called_once_with(exp, success=(200, 204, 404))
424
        self.assertEqual(
425
            SP.mock_calls,
426
            [call('format', 'json'), call('path', path)])
427
        FR.status_code = 404
428
        self.assertRaises(ClientError, self.client.list_objects)
429

    
430
if __name__ == '__main__':
431
    from sys import argv
432
    from kamaki.clients.test import runTestCase
433
    runTestCase(StorageClient, 'Storage Client', argv[1:])