Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (15.4 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
        self.user = 'user'
96
        self.pithos_path = join_urls(get_service_path(
97
            pithos_settings.pithos_services, 'object-store'))
98

    
99
    def tearDown(self):
100
        #delete additionally created metadata
101
        meta = self.get_account_meta()
102
        self.delete_account_meta(meta)
103

    
104
        #delete additionally created groups
105
        groups = self.get_account_groups()
106
        self.delete_account_groups(groups)
107

    
108
        self._clean_account()
109

    
110
    def head(self, url, user='user', *args, **kwargs):
111
        with astakos_user(user):
112
            response = self.client.head(url, *args, **kwargs)
113
        return response
114

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

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

    
125
    def post(self, url, user='user', *args, **kwargs):
126
        with astakos_user(user):
127
            kwargs.setdefault('content_type', 'application/octet-stream')
128
            response = self.client.post(url, *args, **kwargs)
129
        return response
130

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

    
137
    def _clean_account(self):
138
        for c in self.list_containers():
139
            self.delete_container_content(c['name'])
140
            self.delete_container(c['name'])
141

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

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

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

    
177
    def delete_account_groups(self, groups):
178
        url = join_urls(self.pithos_path, self.user)
179
        r = self.post('%s?update=' % url, **groups)
180
        self.assertEqual(r.status_code, 202)
181
        return r
182

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

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

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

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

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

    
235
    def delete_container_content(self, cname):
236
        url = join_urls(self.pithos_path, self.user, cname)
237
        r = self.delete('%s?delimiter=/' % url)
238
        self.assertEqual(r.status_code, 204)
239
        return r
240

    
241
    def delete_container(self, cname):
242
        url = join_urls(self.pithos_path, self.user, cname)
243
        r = self.delete(url)
244
        self.assertEqual(r.status_code, 204)
245
        return r
246

    
247
    def create_container(self, cname):
248
        url = join_urls(self.pithos_path, self.user, cname)
249
        r = self.put(url, data='')
250
        self.assertTrue(r.status_code in (202, 201))
251
        return r
252

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

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

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

    
280
    def assert_status(self, status, codes):
281
        l = [elem for elem in return_codes]
282
        if isinstance(codes, list):
283
            l.extend(codes)
284
        else:
285
            l.append(codes)
286
        self.assertTrue(status in l)
287

    
288
    def assert_extended(self, data, format, type, size=10000):
289
        if format == 'xml':
290
            self._assert_xml(data, type, size)
291
        elif format == 'json':
292
            self._assert_json(data, type, size)
293

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

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

    
318

    
319
class AssertMappingInvariant(object):
320
    def __init__(self, callable, *args, **kwargs):
321
        self.callable = callable
322
        self.args = args
323
        self.kwargs = kwargs
324

    
325
    def __enter__(self):
326
        self.map = self.callable(*self.args, **self.kwargs)
327
        return self.map
328

    
329
    def __exit__(self, type, value, tb):
330
        map = self.callable(*self.args, **self.kwargs)
331
        for k, v in self.map.items():
332
            if is_date(v):
333
                continue
334

    
335
            assert(k in map), '%s not in map' % k
336
            assert v == map[k]
337

    
338
django_sqlalchemy_engines = {
339
    'django.db.backends.postgresql_psycopg2': 'postgresql+psycopg2',
340
    'django.db.backends.postgresql': 'postgresql',
341
    'django.db.backends.mysql': '',
342
    'django.db.backends.sqlite3': 'mssql',
343
    'django.db.backends.oracle': 'oracle'}
344

    
345

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

    
363

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

    
383

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

    
404

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

    
417
            def call_test_func():
418
                try:
419
                    test_func(*args, **kwargs)
420
                except Exception, e:
421
                    exceptions.append(e)
422
                    raise
423

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