Statistics
| Branch: | Tag: | Revision:

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

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

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

    
42
from pithos.backends.random_word import get_random_word
43
from pithos.api import settings as pithos_settings
44

    
45
from synnefo.lib.services import get_service_path
46
from synnefo.lib import join_urls
47

    
48
from django.test import TestCase
49
from django.utils.http import urlencode
50
from django.conf import settings
51

    
52
import django.utils.simplejson as json
53

    
54
import re
55
import random
56
import threading
57
import functools
58

    
59
pithos_test_settings = functools.partial(with_settings, pithos_settings)
60

    
61
DATE_FORMATS = ["%a %b %d %H:%M:%S %Y",
62
                "%A, %d-%b-%y %H:%M:%S GMT",
63
                "%a, %d %b %Y %H:%M:%S GMT"]
64

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

    
81
details = {'container': ('name', 'count', 'bytes', 'last_modified',
82
                         'x_container_policy'),
83
           'object': ('name', 'hash', 'bytes', 'content_type',
84
                      'content_encoding', 'last_modified',)}
85

    
86
return_codes = (400, 401, 403, 404, 503)
87

    
88

    
89
class PithosAPITest(TestCase):
90
    #TODO unauthorized request
91
    def setUp(self):
92
        pithos_settings.BACKEND_DB_MODULE = 'pithos.backends.lib.sqlalchemy'
93
        pithos_settings.BACKEND_DB_CONNECTION = django_to_sqlalchemy()
94
        pithos_settings.BACKEND_POOL_SIZE = 1
95

    
96
        # Override default block size to spead up tests
97
        pithos_settings.BACKEND_BLOCK_SIZE = 1024
98

    
99
        self.user = 'user'
100
        self.pithos_path = join_urls(get_service_path(
101
            pithos_settings.pithos_services, 'object-store'))
102

    
103
    def tearDown(self):
104
        #delete additionally created metadata
105
        meta = self.get_account_meta()
106
        self.delete_account_meta(meta)
107

    
108
        #delete additionally created groups
109
        groups = self.get_account_groups()
110
        self.delete_account_groups(groups)
111

    
112
        self._clean_account()
113

    
114
    def head(self, url, user='user', *args, **kwargs):
115
        with astakos_user(user):
116
            response = self.client.head(url, *args, **kwargs)
117
        return response
118

    
119
    def get(self, url, user='user', *args, **kwargs):
120
        with astakos_user(user):
121
            response = self.client.get(url, *args, **kwargs)
122
        return response
123

    
124
    def delete(self, url, user='user', *args, **kwargs):
125
        with astakos_user(user):
126
            response = self.client.delete(url, *args, **kwargs)
127
        return response
128

    
129
    def post(self, url, user='user', *args, **kwargs):
130
        with astakos_user(user):
131
            kwargs.setdefault('content_type', 'application/octet-stream')
132
            response = self.client.post(url, *args, **kwargs)
133
        return response
134

    
135
    def put(self, url, user='user', *args, **kwargs):
136
        with astakos_user(user):
137
            kwargs.setdefault('content_type', 'application/octet-stream')
138
            response = self.client.put(url, *args, **kwargs)
139
        return response
140

    
141
    def _clean_account(self):
142
        for c in self.list_containers():
143
            self.delete_container_content(c['name'])
144
            self.delete_container(c['name'])
145

    
146
    def update_account_meta(self, meta):
147
        kwargs = dict(
148
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
149
        url = join_urls(self.pithos_path, self.user)
150
        r = self.post('%s?update=' % url, **kwargs)
151
        self.assertEqual(r.status_code, 202)
152
        account_meta = self.get_account_meta()
153
        (self.assertTrue('X-Account-Meta-%s' % k in account_meta) for
154
            k in meta.keys())
155
        (self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for
156
            k, v in meta.items())
157

    
158
    def reset_account_meta(self, meta):
159
        kwargs = dict(
160
            ('HTTP_X_ACCOUNT_META_%s' % k, str(v)) for k, v in meta.items())
161
        url = join_urls(self.pithos_path, self.user)
162
        r = self.post(url, **kwargs)
163
        self.assertEqual(r.status_code, 202)
164
        account_meta = self.get_account_meta()
165
        (self.assertTrue('X-Account-Meta-%s' % k in account_meta) for
166
            k in meta.keys())
167
        (self.assertEqual(account_meta['X-Account-Meta-%s' % k], v) for
168
            k, v in meta.items())
169

    
170
    def delete_account_meta(self, meta):
171
        transform = lambda k: 'HTTP_%s' % k.replace('-', '_').upper()
172
        kwargs = dict((transform(k), '') for k, v in meta.items())
173
        url = join_urls(self.pithos_path, self.user)
174
        r = self.post('%s?update=' % url, **kwargs)
175
        self.assertEqual(r.status_code, 202)
176
        account_meta = self.get_account_meta()
177
        (self.assertTrue('X-Account-Meta-%s' % k not in account_meta) for
178
            k in meta.keys())
179
        return r
180

    
181
    def delete_account_groups(self, groups):
182
        url = join_urls(self.pithos_path, self.user)
183
        r = self.post('%s?update=' % url, **groups)
184
        self.assertEqual(r.status_code, 202)
185
        return r
186

    
187
    def get_account_info(self, until=None):
188
        url = join_urls(self.pithos_path, self.user)
189
        if until is not None:
190
            parts = list(urlsplit(url))
191
            parts[3] = urlencode({
192
                'until': until
193
            })
194
            url = urlunsplit(parts)
195
        r = self.head(url)
196
        self.assertEqual(r.status_code, 204)
197
        return r
198

    
199
    def get_account_meta(self, until=None):
200
        r = self.get_account_info(until=until)
201
        headers = dict(r._headers.values())
202
        map(headers.pop,
203
            [k for k in headers.keys()
204
                if not k.startswith('X-Account-Meta-')])
205
        return headers
206

    
207
    def get_account_groups(self, until=None):
208
        r = self.get_account_info(until=until)
209
        headers = dict(r._headers.values())
210
        map(headers.pop,
211
            [k for k in headers.keys()
212
                if not k.startswith('X-Account-Group-')])
213
        return headers
214

    
215
    def list_containers(self, format='json', headers={}, **params):
216
        _url = join_urls(self.pithos_path, self.user)
217
        parts = list(urlsplit(_url))
218
        params['format'] = format
219
        parts[3] = urlencode(params)
220
        url = urlunsplit(parts)
221
        _headers = dict(('HTTP_%s' % k.upper(), str(v))
222
                        for k, v in headers.items())
223
        r = self.get(url, **_headers)
224

    
225
        if format is None:
226
            containers = r.content.split('\n')
227
            if '' in containers:
228
                containers.remove('')
229
            return containers
230
        elif format == 'json':
231
            try:
232
                containers = json.loads(r.content)
233
            except:
234
                self.fail('json format expected')
235
            return containers
236
        elif format == 'xml':
237
            return minidom.parseString(r.content)
238

    
239
    def delete_container_content(self, cname):
240
        url = join_urls(self.pithos_path, self.user, cname)
241
        r = self.delete('%s?delimiter=/' % url)
242
        self.assertEqual(r.status_code, 204)
243
        return r
244

    
245
    def delete_container(self, cname):
246
        url = join_urls(self.pithos_path, self.user, cname)
247
        r = self.delete(url)
248
        self.assertEqual(r.status_code, 204)
249
        return r
250

    
251
    def create_container(self, cname):
252
        url = join_urls(self.pithos_path, self.user, cname)
253
        r = self.put(url, data='')
254
        self.assertTrue(r.status_code in (202, 201))
255
        return r
256

    
257
    def upload_object(self, cname, oname=None, **meta):
258
        oname = oname or get_random_word(8)
259
        data = get_random_word(length=random.randint(1, 1024))
260
        headers = dict(('HTTP_X_OBJECT_META_%s' % k.upper(), v)
261
                       for k, v in meta.iteritems())
262
        url = join_urls(self.pithos_path, self.user, cname, oname)
263
        r = self.put(url, data=data, **headers)
264
        self.assertEqual(r.status_code, 201)
265
        return oname, data, r
266

    
267
    def create_folder(self, cname, oname=get_random_word(8), **headers):
268
        url = join_urls(self.pithos_path, self.user, cname, oname)
269
        r = self.put(url, data='', content_type='application/directory',
270
                     **headers)
271
        self.assertEqual(r.status_code, 201)
272
        return oname, r
273

    
274
    def list_objects(self, cname):
275
        url = join_urls(self.pithos_path, self.user, cname)
276
        r = self.get('%s?format=json' % url)
277
        self.assertTrue(r.status_code in (200, 204))
278
        try:
279
            objects = json.loads(r.content)
280
        except:
281
            self.fail('json format expected')
282
        return objects
283

    
284
    def assert_status(self, status, codes):
285
        l = [elem for elem in return_codes]
286
        if isinstance(codes, list):
287
            l.extend(codes)
288
        else:
289
            l.append(codes)
290
        self.assertTrue(status in l)
291

    
292
    def assert_extended(self, data, format, type, size=10000):
293
        if format == 'xml':
294
            self._assert_xml(data, type, size)
295
        elif format == 'json':
296
            self._assert_json(data, type, size)
297

    
298
    def _assert_json(self, data, type, size):
299
        convert = lambda s: s.lower()
300
        info = [convert(elem) for elem in details[type]]
301
        self.assertTrue(len(data) <= size)
302
        for item in info:
303
            for i in data:
304
                if 'subdir' in i.keys():
305
                    continue
306
                self.assertTrue(item in i.keys())
307

    
308
    def _assert_xml(self, data, type, size):
309
        convert = lambda s: s.lower()
310
        info = [convert(elem) for elem in details[type]]
311
        try:
312
            info.remove('content_encoding')
313
        except ValueError:
314
            pass
315
        xml = data
316
        entities = xml.getElementsByTagName(type)
317
        self.assertTrue(len(entities) <= size)
318
        for e in entities:
319
            for item in info:
320
                self.assertTrue(e.getElementsByTagName(item))
321

    
322

    
323
class AssertMappingInvariant(object):
324
    def __init__(self, callable, *args, **kwargs):
325
        self.callable = callable
326
        self.args = args
327
        self.kwargs = kwargs
328

    
329
    def __enter__(self):
330
        self.map = self.callable(*self.args, **self.kwargs)
331
        return self.map
332

    
333
    def __exit__(self, type, value, tb):
334
        map = self.callable(*self.args, **self.kwargs)
335
        for k, v in self.map.items():
336
            if is_date(v):
337
                continue
338

    
339
            assert(k in map), '%s not in map' % k
340
            assert v == map[k]
341

    
342
django_sqlalchemy_engines = {
343
    'django.db.backends.postgresql_psycopg2': 'postgresql+psycopg2',
344
    'django.db.backends.postgresql': 'postgresql',
345
    'django.db.backends.mysql': '',
346
    'django.db.backends.sqlite3': 'mssql',
347
    'django.db.backends.oracle': 'oracle'}
348

    
349

    
350
def django_to_sqlalchemy():
351
    """Convert the django default database to sqlalchemy connection string"""
352
    # TODO support for more complex configuration
353
    db = settings.DATABASES['default']
354
    name = db.get('TEST_NAME', 'test_%s' % db['NAME'])
355
    if db['ENGINE'] == 'django.db.backends.sqlite3':
356
        db.get('TEST_NAME', db['NAME'])
357
        return 'sqlite:///%s' % name
358
    else:
359
        d = dict(scheme=django_sqlalchemy_engines.get(db['ENGINE']),
360
                 user=db['USER'],
361
                 pwd=db['PASSWORD'],
362
                 host=db['HOST'].lower(),
363
                 port=int(db['PORT']) if db['PORT'] != '' else '',
364
                 name=name)
365
        return '%(scheme)s://%(user)s:%(pwd)s@%(host)s:%(port)s/%(name)s' % d
366

    
367

    
368
def is_date(date):
369
    __D = r'(?P<day>\d{2})'
370
    __D2 = r'(?P<day>[ \d]\d)'
371
    __M = r'(?P<mon>\w{3})'
372
    __Y = r'(?P<year>\d{4})'
373
    __Y2 = r'(?P<year>\d{2})'
374
    __T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
375
    RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (
376
        __D, __M, __Y, __T))
377
    RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (
378
        __D, __M, __Y2, __T))
379
    ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (
380
        __M, __D2, __T, __Y))
381
    for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
382
        m = regex.match(date)
383
        if m is not None:
384
            return True
385
    return False
386

    
387

    
388
def strnextling(prefix):
389
    """Return the first unicode string
390
       greater than but not starting with given prefix.
391
       strnextling('hello') -> 'hellp'
392
    """
393
    if not prefix:
394
        ## all strings start with the null string,
395
        ## therefore we have to approximate strnextling('')
396
        ## with the last unicode character supported by python
397
        ## 0x10ffff for wide (32-bit unicode) python builds
398
        ## 0x00ffff for narrow (16-bit unicode) python builds
399
        ## We will not autodetect. 0xffff is safe enough.
400
        return unichr(0xffff)
401
    s = prefix[:-1]
402
    c = ord(prefix[-1])
403
    if c >= 0xffff:
404
        raise RuntimeError
405
    s += unichr(c + 1)
406
    return s
407

    
408

    
409
def test_concurrently(times=2):
410
    """
411
    Add this decorator to small pieces of code that you want to test
412
    concurrently to make sure they don't raise exceptions when run at the
413
    same time.  E.g., some Django views that do a SELECT and then a subsequent
414
    INSERT might fail when the INSERT assumes that the data has not changed
415
    since the SELECT.
416
    """
417
    def test_concurrently_decorator(test_func):
418
        def wrapper(*args, **kwargs):
419
            exceptions = []
420

    
421
            def call_test_func():
422
                try:
423
                    test_func(*args, **kwargs)
424
                except Exception, e:
425
                    exceptions.append(e)
426
                    raise
427

    
428
            threads = []
429
            for i in range(times):
430
                threads.append(threading.Thread())
431
            for t in threads:
432
                t.start()
433
            for t in threads:
434
                t.join()
435
            if exceptions:
436
                raise Exception(
437
                    ('test_concurrently intercepted %s',
438
                     'exceptions: %s') % (len(exceptions), exceptions))
439
        return wrapper
440
    return test_concurrently_decorator