Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / test / __init__.py @ 6e7e6b21

History | View | Annotate | Download (21.1 kB)

1
#!/usr/bin/env python
2
#coding=utf8
3

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

    
37
from urlparse import urlunsplit, urlsplit
38
from xml.dom import minidom
39
from urllib import quote, unquote
40

    
41
from snf_django.utils.testing import with_settings, astakos_user
42

    
43
from pithos.api import settings as pithos_settings
44
from pithos.api.test.util import is_date, get_random_data, get_random_name
45
from pithos.backends.migrate import initialize_db
46

    
47
from synnefo.lib.services import get_service_path
48
from synnefo.lib import join_urls
49

    
50
from django.test import TestCase
51
from django.test.simple import DjangoTestSuiteRunner
52
from django.conf import settings
53
from django.utils.http import urlencode
54
from django.db.backends.creation import TEST_DATABASE_PREFIX
55

    
56
import django.utils.simplejson as json
57

    
58
import random
59
import functools
60

    
61

    
62
pithos_test_settings = functools.partial(with_settings, pithos_settings)
63

    
64
DATE_FORMATS = ["%a %b %d %H:%M:%S %Y",
65
                "%A, %d-%b-%y %H:%M:%S GMT",
66
                "%a, %d %b %Y %H:%M:%S GMT"]
67

    
68
o_names = ['kate.jpg',
69
           'kate_beckinsale.jpg',
70
           'How To Win Friends And Influence People.pdf',
71
           'moms_birthday.jpg',
72
           'poodle_strut.mov',
73
           'Disturbed - Down With The Sickness.mp3',
74
           'army_of_darkness.avi',
75
           'the_mad.avi',
76
           'photos/animals/dogs/poodle.jpg',
77
           'photos/animals/dogs/terrier.jpg',
78
           'photos/animals/cats/persian.jpg',
79
           'photos/animals/cats/siamese.jpg',
80
           'photos/plants/fern.jpg',
81
           'photos/plants/rose.jpg',
82
           'photos/me.jpg']
83

    
84
details = {'container': ('name', 'count', 'bytes', 'last_modified',
85
                         'x_container_policy'),
86
           'object': ('name', 'hash', 'bytes', 'content_type',
87
                      'content_encoding', 'last_modified',)}
88

    
89
TEST_BLOCK_SIZE = 1024
90
TEST_HASH_ALGORITHM = 'sha256'
91

    
92
print 'backend module:', pithos_settings.BACKEND_DB_MODULE
93
print 'backend database engine:', settings.DATABASES['default']['ENGINE']
94
print 'update md5:', pithos_settings.UPDATE_MD5
95

    
96

    
97
django_sqlalchemy_engines = {
98
    'django.db.backends.postgresql_psycopg2': 'postgresql+psycopg2',
99
    'django.db.backends.postgresql': 'postgresql',
100
    'django.db.backends.mysql': '',
101
    'django.db.backends.sqlite3': 'mssql',
102
    'django.db.backends.oracle': 'oracle'}
103

    
104

    
105
def prepare_db_connection():
106
    """Build pithos backend connection string from django default database"""
107

    
108
    db = settings.DATABASES['default']
109
    name = db.get('TEST_NAME', TEST_DATABASE_PREFIX + db['NAME'])
110

    
111
    if (pithos_settings.BACKEND_DB_MODULE == 'pithos.backends.lib.sqlalchemy'):
112
        if db['ENGINE'] == 'django.db.backends.sqlite3':
113
            db_connection = 'sqlite:///%s' % name
114
        else:
115
            d = dict(scheme=django_sqlalchemy_engines.get(db['ENGINE']),
116
                     user=db['USER'],
117
                     pwd=db['PASSWORD'],
118
                     host=db['HOST'].lower(),
119
                     port=int(db['PORT']) if db['PORT'] != '' else '',
120
                     name=name)
121
            db_connection = (
122
                '%(scheme)s://%(user)s:%(pwd)s@%(host)s:%(port)s/%(name)s' % d)
123

    
124
            # initialize pithos database
125
            initialize_db(db_connection)
126
    else:
127
        db_connection = name
128
    pithos_settings.BACKEND_DB_CONNECTION = db_connection
129

    
130

    
131
def filter_headers(headers, prefix):
132
    meta = {}
133
    for k, v in headers.iteritems():
134
        if not k.startswith(prefix):
135
            continue
136
        meta[unquote(k[len(prefix):])] = unquote(v)
137
    return meta
138

    
139

    
140
class PithosTestSuiteRunner(DjangoTestSuiteRunner):
141
    def setup_databases(self, **kwargs):
142
        old_names, mirrors = super(PithosTestSuiteRunner,
143
                                   self).setup_databases(**kwargs)
144
        prepare_db_connection()
145
        return old_names, mirrors
146

    
147
    def teardown_databases(self, old_config, **kwargs):
148
        from pithos.api.util import (_pithos_storage_backend_pool,
149
                                     _pithos_public_backend_pool)
150
        _pithos_storage_backend_pool.shutdown()
151
        _pithos_public_backend_pool.shutdown()
152
        super(PithosTestSuiteRunner, self).teardown_databases(old_config,
153
                                                              **kwargs)
154

    
155

    
156
class PithosAPITest(TestCase):
157
    def setUp(self):
158
        # Override default block size to spead up tests
159
        pithos_settings.BACKEND_BLOCK_SIZE = TEST_BLOCK_SIZE
160
        pithos_settings.BACKEND_HASH_ALGORITHM = TEST_HASH_ALGORITHM
161

    
162
        self.user = 'user'
163
        self.pithos_path = join_urls(get_service_path(
164
            pithos_settings.pithos_services, 'object-store'))
165

    
166
    def tearDown(self):
167
        #delete additionally created metadata
168
        meta = self.get_account_meta()
169
        self.delete_account_meta(meta)
170

    
171
        #delete additionally created groups
172
        groups = self.get_account_groups()
173
        self.delete_account_groups(groups)
174

    
175
        self._clean_account()
176

    
177
    def _clean_account(self):
178
        for c in self.list_containers():
179
            self.delete_container_content(c['name'])
180
            self.delete_container(c['name'])
181

    
182
    def head(self, url, user='user', token='DummyToken', data={}, follow=False,
183
             **extra):
184
        with astakos_user(user):
185
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
186
            if token:
187
                extra['HTTP_X_AUTH_TOKEN'] = token
188
            response = self.client.head(url, data, follow, **extra)
189
        return response
190

    
191
    def get(self, url, user='user', token='DummyToken', data={}, follow=False,
192
            **extra):
193
        with astakos_user(user):
194
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
195
            if token:
196
                extra['HTTP_X_AUTH_TOKEN'] = token
197
            response = self.client.get(url, data, follow, **extra)
198
        return response
199

    
200
    def delete(self, url, user='user', token='DummyToken', data={},
201
               follow=False, **extra):
202
        with astakos_user(user):
203
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
204
            if token:
205
                extra['HTTP_X_AUTH_TOKEN'] = token
206
            response = self.client.delete(url, data, follow, **extra)
207
        return response
208

    
209
    def post(self, url, user='user', token='DummyToken', data={},
210
             content_type='application/octet-stream', follow=False, **extra):
211
        with astakos_user(user):
212
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
213
            if token:
214
                extra['HTTP_X_AUTH_TOKEN'] = token
215
            response = self.client.post(url, data, content_type, follow,
216
                                        **extra)
217
        return response
218

    
219
    def put(self, url, user='user', token='DummyToken', data={},
220
            content_type='application/octet-stream', follow=False, **extra):
221
        with astakos_user(user):
222
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
223
            if token:
224
                extra['HTTP_X_AUTH_TOKEN'] = token
225
            response = self.client.put(url, data, content_type, follow,
226
                                       **extra)
227
        return response
228

    
229
    def update_account_meta(self, meta, user=None):
230
        user = user or self.user
231
        kwargs = dict(
232
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
233
        url = join_urls(self.pithos_path, user)
234
        r = self.post('%s?update=' % url, user=user, **kwargs)
235
        self.assertEqual(r.status_code, 202)
236
        account_meta = self.get_account_meta(user=user)
237
        (self.assertTrue('X-Account-Meta-%s' % k in account_meta) for
238
            k in meta.keys())
239
        (self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for
240
            k, v in meta.items())
241

    
242
    def reset_account_meta(self, meta, user=None):
243
        user = user or self.user
244
        kwargs = dict(
245
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
246
        url = join_urls(self.pithos_path, user)
247
        r = self.post(url, user=user, **kwargs)
248
        self.assertEqual(r.status_code, 202)
249
        account_meta = self.get_account_meta(user=user)
250
        (self.assertTrue('X-Account-Meta-%s' % k in account_meta) for
251
            k in meta.keys())
252
        (self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for
253
            k, v in meta.items())
254

    
255
    def delete_account_meta(self, meta, user=None):
256
        user = user or self.user
257
        transform = lambda k: 'HTTP_%s' % k.replace('-', '_').upper()
258
        kwargs = dict((transform(k), '') for k, v in meta.items())
259
        url = join_urls(self.pithos_path, user)
260
        r = self.post('%s?update=' % url, user=user, **kwargs)
261
        self.assertEqual(r.status_code, 202)
262
        account_meta = self.get_account_meta(user=user)
263
        (self.assertTrue('X-Account-Meta-%s' % k not in account_meta) for
264
            k in meta.keys())
265
        return r
266

    
267
    def delete_account_groups(self, groups, user=None):
268
        user = user or self.user
269
        url = join_urls(self.pithos_path, user)
270
        r = self.post('%s?update=' % url, user=user, **groups)
271
        self.assertEqual(r.status_code, 202)
272
        account_groups = self.get_account_groups()
273
        (self.assertTrue(k not in account_groups) for k in groups.keys())
274
        return r
275

    
276
    def get_account_info(self, until=None, user=None):
277
        user = user or self.user
278
        url = join_urls(self.pithos_path, user)
279
        if until is not None:
280
            parts = list(urlsplit(url))
281
            parts[3] = urlencode({
282
                'until': until
283
            })
284
            url = urlunsplit(parts)
285
        r = self.head(url, user=user)
286
        self.assertEqual(r.status_code, 204)
287
        return r
288

    
289
    def get_account_meta(self, until=None, user=None):
290
        prefix = 'X-Account-Meta-'
291
        r = self.get_account_info(until=until, user=user)
292
        headers = dict(r._headers.values())
293
        return filter_headers(headers, prefix)
294

    
295
    def get_account_groups(self, until=None, user=None):
296
        prefix = 'X-Account-Group-'
297
        r = self.get_account_info(until=until, user=user)
298
        headers = dict(r._headers.values())
299
        return filter_headers(headers, prefix)
300

    
301
    def get_container_info(self, container, until=None, user=None):
302
        user = user or self.user
303
        url = join_urls(self.pithos_path, user, container)
304
        if until is not None:
305
            parts = list(urlsplit(url))
306
            parts[3] = urlencode({
307
                'until': until
308
            })
309
            url = urlunsplit(parts)
310
        r = self.head(url, user=user)
311
        self.assertEqual(r.status_code, 204)
312
        return r
313

    
314
    def get_container_meta(self, container, until=None, user=None):
315
        prefix = 'X-Container-Meta-'
316
        r = self.get_container_info(container, until=until, user=user)
317
        headers = dict(r._headers.values())
318
        return filter_headers(headers, prefix)
319

    
320
    def update_container_meta(self, container, meta, user=None):
321
        user = user or self.user
322
        kwargs = dict(
323
            ('HTTP_X_CONTAINER_META_%s' % k, str(v)) for k, v in meta.items())
324
        url = join_urls(self.pithos_path, user, container)
325
        r = self.post('%s?update=' % url, user=user, **kwargs)
326
        self.assertEqual(r.status_code, 202)
327
        container_meta = self.get_container_meta(container, user=user)
328
        (self.assertTrue('X-Container-Meta-%s' % k in container_meta) for
329
            k in meta.keys())
330
        (self.assertEqual(container_meta['X-Container-Meta-%s' % k], v) for
331
            k, v in meta.items())
332

    
333
    def list_containers(self, format='json', headers={}, user=None, **params):
334
        user = user or self.user
335
        _url = join_urls(self.pithos_path, user)
336
        parts = list(urlsplit(_url))
337
        params['format'] = format
338
        parts[3] = urlencode(params)
339
        url = urlunsplit(parts)
340
        _headers = dict(('HTTP_%s' % k.upper(), str(v))
341
                        for k, v in headers.items())
342
        r = self.get(url, user=user, **_headers)
343

    
344
        if format is None:
345
            containers = r.content.split('\n')
346
            if '' in containers:
347
                containers.remove('')
348
            return containers
349
        elif format == 'json':
350
            try:
351
                containers = json.loads(r.content)
352
            except:
353
                self.fail('json format expected')
354
            return containers
355
        elif format == 'xml':
356
            return minidom.parseString(r.content)
357

    
358
    def delete_container_content(self, cname, user=None):
359
        user = user or self.user
360
        url = join_urls(self.pithos_path, user, cname)
361
        r = self.delete('%s?delimiter=/' % url, user=user)
362
        self.assertEqual(r.status_code, 204)
363
        return r
364

    
365
    def delete_container(self, cname, user=None):
366
        user = user or self.user
367
        url = join_urls(self.pithos_path, user, cname)
368
        r = self.delete(url, user=user)
369
        self.assertEqual(r.status_code, 204)
370
        return r
371

    
372
    def create_container(self, cname=None, user=None):
373
        cname = cname or get_random_name()
374
        user = user or self.user
375
        url = join_urls(self.pithos_path, user, cname)
376
        r = self.put(url, user=user, data='')
377
        self.assertTrue(r.status_code in (202, 201))
378
        return cname, r
379

    
380
    def upload_object(self, cname, oname=None, length=None, verify=True,
381
                      user=None, **meta):
382
        oname = oname or get_random_name()
383
        length = length or random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE)
384
        user = user or self.user
385
        data = get_random_data(length=length)
386
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
387
                       for k, v in meta.iteritems())
388
        url = join_urls(self.pithos_path, user, cname, oname)
389
        r = self.put(url, user=user, data=data, **headers)
390
        if verify:
391
            self.assertEqual(r.status_code, 201)
392
        return oname, data, r
393

    
394
    def update_object_data(self, cname, oname=None, length=None,
395
                           content_type=None, content_range=None,
396
                           verify=True, user=None, **meta):
397
        oname = oname or get_random_name()
398
        length = length or random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE)
399
        content_type = content_type or 'application/octet-stream'
400
        user = user or self.user
401
        data = get_random_data(length=length)
402
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
403
                       for k, v in meta.iteritems())
404
        if content_range:
405
            headers['HTTP_CONTENT_RANGE'] = content_range
406
        url = join_urls(self.pithos_path, user, cname, oname)
407
        r = self.post(url, user=user, data=data, content_type=content_type,
408
                      **headers)
409
        if verify:
410
            self.assertEqual(r.status_code, 204)
411
        return oname, data, r
412

    
413
    def append_object_data(self, cname, oname=None, length=None,
414
                           content_type=None, user=None):
415
        return self.update_object_data(cname, oname=oname,
416
                                       length=length,
417
                                       content_type=content_type,
418
                                       content_range='bytes */*',
419
                                       user=user)
420

    
421
    def create_folder(self, cname, oname=None, user=None, **headers):
422
        user = user or self.user
423
        oname = oname or get_random_name()
424
        url = join_urls(self.pithos_path, user, cname, oname)
425
        r = self.put(url, user=user, data='',
426
                     content_type='application/directory', **headers)
427
        self.assertEqual(r.status_code, 201)
428
        return oname, r
429

    
430
    def list_objects(self, cname, prefix=None, user=None):
431
        user = user or self.user
432
        url = join_urls(self.pithos_path, user, cname)
433
        path = '%s?format=json' % url
434
        if prefix is not None:
435
            path = '%s&prefix=%s' % (path, prefix)
436
        r = self.get(path, user=user)
437
        self.assertTrue(r.status_code in (200, 204))
438
        try:
439
            objects = json.loads(r.content)
440
        except:
441
            self.fail('json format expected')
442
        return objects
443

    
444
    def get_object_info(self, container, object, version=None, until=None,
445
                        user=None):
446
        user = user or self.user
447
        url = join_urls(self.pithos_path, user, container, object)
448
        if until is not None:
449
            parts = list(urlsplit(url))
450
            parts[3] = urlencode({
451
                'until': until
452
            })
453
            url = urlunsplit(parts)
454
        if version:
455
            url = '%s?version=%s' % (url, version)
456
        r = self.head(url, user=user)
457
        self.assertEqual(r.status_code, 200)
458
        return r
459

    
460
    def get_object_meta(self, container, object, version=None, until=None,
461
                        user=None):
462
        prefix = 'X-Object-Meta-'
463
        user = user or self.user
464
        r = self.get_object_info(container, object, version, until=until,
465
                                 user=user)
466
        headers = dict(r._headers.values())
467
        return filter_headers(headers, prefix)
468

    
469
    def update_object_meta(self, container, object, meta, user=None):
470
        user = user or self.user
471
        kwargs = dict(
472
            ('HTTP_X_OBJECT_META_%s' % k, str(v)) for k, v in meta.items())
473
        url = join_urls(self.pithos_path, user, container, object)
474
        r = self.post('%s?update=' % url, user=user, content_type='', **kwargs)
475
        self.assertEqual(r.status_code, 202)
476
        object_meta = self.get_object_meta(container, object, user=user)
477
        (self.assertTrue('X-Objecr-Meta-%s' % k in object_meta) for
478
            k in meta.keys())
479
        (self.assertEqual(object_meta['X-Object-Meta-%s' % k], v) for
480
            k, v in meta.items())
481

    
482
    def assert_extended(self, data, format, type, size=10000):
483
        if format == 'xml':
484
            self._assert_xml(data, type, size)
485
        elif format == 'json':
486
            self._assert_json(data, type, size)
487

    
488
    def _assert_json(self, data, type, size):
489
        convert = lambda s: s.lower()
490
        info = [convert(elem) for elem in details[type]]
491
        self.assertTrue(len(data) <= size)
492
        for item in info:
493
            for i in data:
494
                if 'subdir' in i.keys():
495
                    continue
496
                self.assertTrue(item in i.keys())
497

    
498
    def _assert_xml(self, data, type, size):
499
        convert = lambda s: s.lower()
500
        info = [convert(elem) for elem in details[type]]
501
        try:
502
            info.remove('content_encoding')
503
        except ValueError:
504
            pass
505
        xml = data
506
        entities = xml.getElementsByTagName(type)
507
        self.assertTrue(len(entities) <= size)
508
        for e in entities:
509
            for item in info:
510
                self.assertTrue(e.getElementsByTagName(item))
511

    
512

    
513
class AssertMappingInvariant(object):
514
    def __init__(self, callable, *args, **kwargs):
515
        self.callable = callable
516
        self.args = args
517
        self.kwargs = kwargs
518

    
519
    def __enter__(self):
520
        self.map = self.callable(*self.args, **self.kwargs)
521
        return self.map
522

    
523
    def __exit__(self, type, value, tb):
524
        map = self.callable(*self.args, **self.kwargs)
525
        for k, v in self.map.items():
526
            if is_date(v):
527
                continue
528

    
529
            assert(k in map), '%s not in map' % k
530
            assert v == map[k]
531

    
532

    
533
class AssertUUidInvariant(object):
534
    def __init__(self, callable, *args, **kwargs):
535
        self.callable = callable
536
        self.args = args
537
        self.kwargs = kwargs
538

    
539
    def __enter__(self):
540
        self.map = self.callable(*self.args, **self.kwargs)
541
        assert('x-object-uuid' in self.map)
542
        self.uuid = self.map['x-object-uuid']
543
        return self.map
544

    
545
    def __exit__(self, type, value, tb):
546
        map = self.callable(*self.args, **self.kwargs)
547
        assert('x-object-uuid' in self.map)
548
        uuid = map['x-object-uuid']
549
        assert(uuid == self.uuid)