Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (20.2 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 prepate_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
        prepate_db_connection()
145
        return old_names, mirrors
146

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

    
153

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

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

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

    
169
        #delete additionally created groups
170
        groups = self.get_account_groups()
171
        self.delete_account_groups(groups)
172

    
173
        self._clean_account()
174

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

    
180
    def head(self, url, user='user', data={}, follow=False, **extra):
181
        with astakos_user(user):
182
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
183
            extra.setdefault('HTTP_X_AUTH_TOKEN', 'token')
184
            response = self.client.head(url, data, follow, **extra)
185
        return response
186

    
187
    def get(self, url, user='user', data={}, follow=False, **extra):
188
        with astakos_user(user):
189
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
190
            extra.setdefault('HTTP_X_AUTH_TOKEN', 'token')
191
            response = self.client.get(url, data, follow, **extra)
192
        return response
193

    
194
    def delete(self, url, user='user', data={}, follow=False, **extra):
195
        with astakos_user(user):
196
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
197
            extra.setdefault('HTTP_X_AUTH_TOKEN', 'token')
198
            response = self.client.delete(url, data, follow, **extra)
199
        return response
200

    
201
    def post(self, url, user='user', data={},
202
             content_type='application/octet-stream', follow=False, **extra):
203
        with astakos_user(user):
204
            extra = dict((quote(k), quote(v)) for k, v in extra.items())
205
            extra.setdefault('HTTP_X_AUTH_TOKEN', 'token')
206
            response = self.client.post(url, data, content_type, follow,
207
                                        **extra)
208
        return response
209

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

    
219
    def update_account_meta(self, meta, user=None):
220
        user = user or self.user
221
        kwargs = dict(
222
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
223
        url = join_urls(self.pithos_path, user)
224
        r = self.post('%s?update=' % url, user=user, **kwargs)
225
        self.assertEqual(r.status_code, 202)
226
        account_meta = self.get_account_meta()
227
        (self.assertTrue(k in account_meta) for k in meta.keys())
228
        (self.assertEqual(account_meta[k], v) for k, v in meta.items())
229

    
230
    def delete_account_meta(self, meta, user=None):
231
        user = user or self.user
232
        transform = (lambda k: 'HTTP_X_ACCOUNT_META_%s' %
233
                     k.replace('-', '_').upper())
234
        kwargs = dict((transform(k), '') for k, v in meta.items())
235
        url = join_urls(self.pithos_path, user)
236
        r = self.post('%s?update=' % url, user=user, **kwargs)
237
        self.assertEqual(r.status_code, 202)
238
        account_meta = self.get_account_meta()
239
        (self.assertTrue(k not in account_meta) for k in meta.keys())
240
        return r
241

    
242
    def delete_account_groups(self, groups, user=None):
243
        user = user or self.user
244
        url = join_urls(self.pithos_path, user)
245
        transform = (lambda k: 'HTTP_X_ACCOUNT_GROUP_%s' %
246
                     k.replace('-', '_').upper())
247
        kwargs = dict((transform(k), '') for k, v in groups.items())
248
        r = self.post('%s?update=' % url, user=user, **kwargs)
249
        self.assertEqual(r.status_code, 202)
250
        account_groups = self.get_account_groups()
251
        (self.assertTrue(k not in account_groups) for k in groups.keys())
252
        return r
253

    
254
    def get_account_info(self, until=None, user=None):
255
        user = user or self.user
256
        url = join_urls(self.pithos_path, user)
257
        if until is not None:
258
            parts = list(urlsplit(url))
259
            parts[3] = urlencode({
260
                'until': until
261
            })
262
            url = urlunsplit(parts)
263
        r = self.head(url, user=user)
264
        self.assertEqual(r.status_code, 204)
265
        return r
266

    
267
    def get_account_meta(self, until=None, user=None):
268
        prefix = 'X-Account-Meta-'
269
        r = self.get_account_info(until=until, user=user)
270
        headers = dict(r._headers.values())
271
        return filter_headers(headers, prefix)
272

    
273
    def get_account_groups(self, until=None, user=None):
274
        prefix = 'X-Account-Group-'
275
        r = self.get_account_info(until=until, user=user)
276
        headers = dict(r._headers.values())
277
        return filter_headers(headers, prefix)
278

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

    
292
    def get_container_meta(self, container, until=None, user=None):
293
        prefix = 'X-Container-Meta-'
294
        r = self.get_container_info(container, until=until, user=user)
295
        headers = dict(r._headers.values())
296
        return filter_headers(headers, prefix)
297

    
298
    def update_container_meta(self, container, meta, user=None):
299
        user = user or self.user
300
        kwargs = dict(
301
            ('HTTP_X_CONTAINER_META_%s' % k, str(v)) for k, v in meta.items())
302
        url = join_urls(self.pithos_path, user, container)
303
        r = self.post('%s?update=' % url, user=user, **kwargs)
304
        self.assertEqual(r.status_code, 202)
305
        container_meta = self.get_container_meta(container, user=user)
306
        (self.assertTrue('X-Container-Meta-%s' % k in container_meta) for
307
            k in meta.keys())
308
        (self.assertEqual(container_meta['X-Container-Meta-%s' % k], v) for
309
            k, v in meta.items())
310

    
311
    def list_containers(self, format='json', headers={}, user=None, **params):
312
        user = user or self.user
313
        _url = join_urls(self.pithos_path, user)
314
        parts = list(urlsplit(_url))
315
        params['format'] = format
316
        parts[3] = urlencode(params)
317
        url = urlunsplit(parts)
318
        _headers = dict(('HTTP_%s' % k.upper(), str(v))
319
                        for k, v in headers.items())
320
        r = self.get(url, user=user, **_headers)
321

    
322
        if format is None:
323
            containers = r.content.split('\n')
324
            if '' in containers:
325
                containers.remove('')
326
            return containers
327
        elif format == 'json':
328
            try:
329
                containers = json.loads(r.content)
330
            except:
331
                self.fail('json format expected')
332
            return containers
333
        elif format == 'xml':
334
            return minidom.parseString(r.content)
335

    
336
    def delete_container_content(self, cname, user=None):
337
        user = user or self.user
338
        url = join_urls(self.pithos_path, user, cname)
339
        r = self.delete('%s?delimiter=/' % url, user=user)
340
        self.assertEqual(r.status_code, 204)
341
        return r
342

    
343
    def delete_container(self, cname, user=None):
344
        user = user or self.user
345
        url = join_urls(self.pithos_path, user, cname)
346
        r = self.delete(url, user=user)
347
        self.assertEqual(r.status_code, 204)
348
        return r
349

    
350
    def create_container(self, cname=None, user=None):
351
        cname = cname or get_random_name()
352
        user = user or self.user
353
        url = join_urls(self.pithos_path, user, cname)
354
        r = self.put(url, user=user, data='')
355
        self.assertTrue(r.status_code in (202, 201))
356
        return cname, r
357

    
358
    def upload_object(self, cname, oname=None, length=None, verify=True,
359
                      user=None, **meta):
360
        oname = oname or get_random_name()
361
        length = length or random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE)
362
        user = user or self.user
363
        data = get_random_data(length=length)
364
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
365
                       for k, v in meta.iteritems())
366
        url = join_urls(self.pithos_path, user, cname, oname)
367
        r = self.put(url, user=user, data=data, **headers)
368
        if verify:
369
            self.assertEqual(r.status_code, 201)
370
        return oname, data, r
371

    
372
    def update_object_data(self, cname, oname=None, length=None,
373
                           content_type=None, content_range=None,
374
                           verify=True, user=None, **meta):
375
        oname = oname or get_random_name()
376
        length = length or random.randint(TEST_BLOCK_SIZE, 2 * TEST_BLOCK_SIZE)
377
        content_type = content_type or 'application/octet-stream'
378
        user = user or self.user
379
        data = get_random_data(length=length)
380
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
381
                       for k, v in meta.iteritems())
382
        if content_range:
383
            headers['HTTP_CONTENT_RANGE'] = content_range
384
        url = join_urls(self.pithos_path, user, cname, oname)
385
        r = self.post(url, user=user, data=data, content_type=content_type,
386
                      **headers)
387
        if verify:
388
            self.assertEqual(r.status_code, 204)
389
        return oname, data, r
390

    
391
    def append_object_data(self, cname, oname=None, length=None,
392
                           content_type=None, user=None):
393
        return self.update_object_data(cname, oname=oname,
394
                                       length=length,
395
                                       content_type=content_type,
396
                                       content_range='bytes */*',
397
                                       user=user)
398

    
399
    def create_folder(self, cname, oname=None, user=None, **headers):
400
        user = user or self.user
401
        oname = oname or get_random_name()
402
        url = join_urls(self.pithos_path, user, cname, oname)
403
        r = self.put(url, user=user, data='',
404
                     content_type='application/directory', **headers)
405
        self.assertEqual(r.status_code, 201)
406
        return oname, r
407

    
408
    def list_objects(self, cname, prefix=None, user=None):
409
        user = user or self.user
410
        url = join_urls(self.pithos_path, user, cname)
411
        path = '%s?format=json' % url
412
        if prefix is not None:
413
            path = '%s&prefix=%s' % (path, prefix)
414
        r = self.get(path, user=user)
415
        self.assertTrue(r.status_code in (200, 204))
416
        try:
417
            objects = json.loads(r.content)
418
        except:
419
            self.fail('json format expected')
420
        return objects
421

    
422
    def get_object_info(self, container, object, version=None, until=None,
423
                        user=None):
424
        user = user or self.user
425
        url = join_urls(self.pithos_path, user, container, object)
426
        if until is not None:
427
            parts = list(urlsplit(url))
428
            parts[3] = urlencode({
429
                'until': until
430
            })
431
            url = urlunsplit(parts)
432
        if version:
433
            url = '%s?version=%s' % (url, version)
434
        r = self.head(url, user=user)
435
        self.assertEqual(r.status_code, 200)
436
        return r
437

    
438
    def get_object_meta(self, container, object, version=None, until=None,
439
                        user=None):
440
        prefix = 'X-Object-Meta-'
441
        r = self.get_object_info(container, object, version, until=until,
442
                                 user=user)
443
        headers = dict(r._headers.values())
444
        return filter_headers(headers, prefix)
445

    
446
    def update_object_meta(self, container, object, meta, user=None):
447
        user = user or self.user
448
        kwargs = dict(
449
            ('HTTP_X_OBJECT_META_%s' % k, str(v)) for k, v in meta.items())
450
        url = join_urls(self.pithos_path, user, container, object)
451
        r = self.post('%s?update=' % url, user=user, content_type='', **kwargs)
452
        self.assertEqual(r.status_code, 202)
453
        object_meta = self.get_object_meta(container, object, user=user)
454
        (self.assertTrue('X-Objecr-Meta-%s' % k in object_meta) for
455
            k in meta.keys())
456
        (self.assertEqual(object_meta['X-Object-Meta-%s' % k], v) for
457
            k, v in meta.items())
458

    
459
    def assert_extended(self, data, format, type, size=10000):
460
        if format == 'xml':
461
            self._assert_xml(data, type, size)
462
        elif format == 'json':
463
            self._assert_json(data, type, size)
464

    
465
    def _assert_json(self, data, type, size):
466
        convert = lambda s: s.lower()
467
        info = [convert(elem) for elem in details[type]]
468
        self.assertTrue(len(data) <= size)
469
        for item in info:
470
            for i in data:
471
                if 'subdir' in i.keys():
472
                    continue
473
                self.assertTrue(item in i.keys())
474

    
475
    def _assert_xml(self, data, type, size):
476
        convert = lambda s: s.lower()
477
        info = [convert(elem) for elem in details[type]]
478
        try:
479
            info.remove('content_encoding')
480
        except ValueError:
481
            pass
482
        xml = data
483
        entities = xml.getElementsByTagName(type)
484
        self.assertTrue(len(entities) <= size)
485
        for e in entities:
486
            for item in info:
487
                self.assertTrue(e.getElementsByTagName(item))
488

    
489

    
490
class AssertMappingInvariant(object):
491
    def __init__(self, callable, *args, **kwargs):
492
        self.callable = callable
493
        self.args = args
494
        self.kwargs = kwargs
495

    
496
    def __enter__(self):
497
        self.map = self.callable(*self.args, **self.kwargs)
498
        return self.map
499

    
500
    def __exit__(self, type, value, tb):
501
        map = self.callable(*self.args, **self.kwargs)
502
        for k, v in self.map.items():
503
            if is_date(v):
504
                continue
505

    
506
            assert(k in map), '%s not in map' % k
507
            assert v == map[k]
508

    
509

    
510
class AssertUUidInvariant(object):
511
    def __init__(self, callable, *args, **kwargs):
512
        self.callable = callable
513
        self.args = args
514
        self.kwargs = kwargs
515

    
516
    def __enter__(self):
517
        self.map = self.callable(*self.args, **self.kwargs)
518
        assert('x-object-uuid' in self.map)
519
        self.uuid = self.map['x-object-uuid']
520
        return self.map
521

    
522
    def __exit__(self, type, value, tb):
523
        map = self.callable(*self.args, **self.kwargs)
524
        assert('x-object-uuid' in self.map)
525
        uuid = map['x-object-uuid']
526
        assert(uuid == self.uuid)