summaryrefslogtreecommitdiff
path: root/tests/conftest.py
blob: 645836a17dd4cbd04b968c410d95878397e85870 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
import glob
import base64
import json
import os
import pytest
import re
import random
import requests
import signal
import socket
import subprocess
import sys
import time

from hashlib import sha512
from six.moves.urllib.parse import urljoin
from six.moves.urllib.parse import urlsplit
from uuid import uuid4

from leap.soledad.common.couch import CouchDatabase
from leap.soledad.client import Soledad


def _select_subdir(subdir, blacklist, items):

    # allow blacklisted subdir if explicited in command line
    if subdir and subdir in blacklist:
        blacklist.remove(subdir)

    # determine blacklisted subdirs
    dirname = os.path.dirname(__file__)
    blacklisted_subdirs = map(lambda s: os.path.join(dirname, s), blacklist)

    # determine base path for selected tests
    path = dirname
    if subdir:
        path = os.path.join(dirname, subdir)

    # remove tests from blacklisted subdirs
    selected = []
    deselected = []
    for item in items:
        filename = item.module.__file__
        blacklisted = any(
            map(lambda s: filename.startswith(s), blacklisted_subdirs))
        if blacklisted or not filename.startswith(path):
            deselected.append(item)
        else:
            selected.append(item)

    return selected, deselected


def pytest_collection_modifyitems(items, config):

    # mark tests that depend on couchdb
    marker = getattr(pytest.mark, 'needs_couch')
    for item in items:
        if 'soledad/testing/tests/couch/' in item.module.__file__:
            item.add_marker(marker)

    # select/deselect tests based on a blacklist and the subdir option given in
    # command line
    blacklist = ['benchmarks', 'responsiveness', 'e2e']
    subdir = config.getoption('subdir')
    selected, deselected = _select_subdir(subdir, blacklist, items)
    config.hook.pytest_deselected(items=deselected)
    items[:] = selected


#
# default options for all tests
#

DEFAULT_PASSPHRASE = '123'

DEFAULT_URL = 'http://127.0.0.1:2424'
DEFAULT_PRIVKEY = 'soledad_privkey.pem'
DEFAULT_CERTKEY = 'soledad_certkey.pem'
DEFAULT_TOKEN = 'an-auth-token'


def pytest_addoption(parser):
    parser.addoption(
        "--couch-url", type="string", default="http://127.0.0.1:5984",
        help="the url for the couch server to be used during tests")

    # the following options are only used in benchmarks, but has to be defined
    # here due to how pytest discovers plugins during startup.
    parser.addoption(
        "--watch-memory", default=False, action="store_true",
        help="whether to monitor memory percentages during test run. "
             "**Warning**: enabling this will impact the time taken and the "
             "CPU used by the benchmarked code, so use with caution!")

    parser.addoption(
        "--soledad-server-url", type="string", default=None,
        help="Soledad Server URL. A local server will be started if and only "
             "if  no URL is passed.")

    # the following option is only used in responsiveness tests, but has to be
    # defined here due to how pytest discovers plugins during startup.
    parser.addoption(
        "--elasticsearch-url", type="string", default=None,
        help="the url for posting responsiveness results to elasticsearch")

    parser.addoption(
        "--subdir", type="string", default=None,
        help="select only tests from a certain subdirectory of ./tests/")


def _request(method, url, data=None, do=True):
    if do:
        method = getattr(requests, method)
        method(url, data=data)
    else:
        cmd = 'curl --netrc -X %s %s' % (method.upper(), url)
        if data:
            cmd += ' -d "%s"' % json.dumps(data)
        print(cmd)


@pytest.fixture
def couch_url(request):
    url = request.config.option.couch_url
    request.cls.couch_url = url


@pytest.fixture
def method_tmpdir(request, tmpdir):
    request.instance.tempdir = tmpdir.strpath


#
# remote_db fixture: provides an empty database for a given user in a per
# function scope.
#

class UserDatabase(object):

    def __init__(self, url, uuid, create=True):
        self._remote_db_url = urljoin(url, 'user-%s' % uuid)
        self._create = create

    def setup(self):
        if self._create:
            return CouchDatabase.open_database(
                url=self._remote_db_url, create=True, replica_uid=None)
        else:
            _request('put', self._remote_db_url, do=False)

    def teardown(self):
        _request('delete', self._remote_db_url, do=self._create)


@pytest.fixture()
def remote_db(request):
    couch_url = request.config.option.couch_url

    def create(uuid, create=True):
        db = UserDatabase(couch_url, uuid, create=create)
        request.addfinalizer(db.teardown)
        return db.setup()
    return create


def get_pid(pidfile):
    if not os.path.isfile(pidfile):
        return 0
    try:
        with open(pidfile) as f:
            return int(f.read())
    except IOError:
        return 0


#
# soledad_server fixture: provides a running soledad server in a per module
# context (same soledad server for all tests in this module).
#

class SoledadServer(object):

    def __init__(self, tmpdir_factory, couch_url):
        tmpdir = tmpdir_factory.mktemp('soledad-server')
        self.tmpdir = tmpdir
        self._pidfile = os.path.join(tmpdir.strpath, 'soledad-server.pid')
        self._logfile = os.path.join(tmpdir.strpath, 'soledad-server.log')
        self._couch_url = couch_url

    def start(self):
        self._create_conf_file()
        # start the server
        executable = 'twistd'
        if 'VIRTUAL_ENV' not in os.environ:
            executable = os.path.join(
                os.path.dirname(os.environ['_']), 'twistd')
        subprocess.check_call([
            executable,
            '--logfile=%s' % self._logfile,
            '--pidfile=%s' % self._pidfile,
            'web',
            '--class=leap.soledad.server.entrypoints.SoledadEntrypoint',
            '--port=tcp:2424'
        ])

    def _create_conf_file(self):

        # come up with name of the configuration file
        fname = '/etc/soledad/soledad-server.conf'
        if not os.access('/etc', os.W_OK):
            fname = os.path.join(self.tmpdir.strpath, 'soledad-server.conf')

        # create the configuration file
        dirname = os.path.dirname(fname)
        if not os.path.isdir(dirname):
            os.mkdir(dirname)
        with open(fname, 'w') as f:
            blobs_path = os.path.join(str(self.tmpdir), 'blobs')
            content = '''[soledad-server]
couch_url = %s
blobs = true
blobs_path = %s''' % (self._couch_url, blobs_path)
            f.write(content)

        # update the environment to use that file
        os.environ.update({'SOLEDAD_SERVER_CONFIG_FILE': fname})

    def stop(self):
        pid = get_pid(self._pidfile)
        os.kill(pid, signal.SIGTERM)


@pytest.fixture(scope='module')
def soledad_server(tmpdir_factory, request):

    # avoid starting a server if the url is remote
    soledad_url = request.config.option.soledad_server_url
    if soledad_url is not None:
        return None

    # start a soledad server
    couch_url = request.config.option.couch_url
    server = SoledadServer(tmpdir_factory, couch_url)
    server.start()
    request.addfinalizer(server.stop)
    return server


#
# soledad_dbs fixture: provides all databases needed by soledad server in a per
# module scope (same databases for all tests in this module).
#

def _token_dbname():
    dbname = 'tokens_' + \
        str(int(time.time() / (30 * 24 * 3600)))
    return dbname


class SoledadDatabases(object):

    def __init__(self, url, create=True):
        self._token_db_url = urljoin(url, _token_dbname())
        self._shared_db_url = urljoin(url, 'shared')
        self._create = create

    def setup(self, uuid):
        self._create_dbs()
        self._add_token(uuid)

    def _create_dbs(self):
        _request('put', self._token_db_url, do=self._create)
        _request('put', self._shared_db_url, do=self._create)

    def _add_token(self, uuid):
        token = sha512(DEFAULT_TOKEN).hexdigest()
        content = {'type': 'Token', 'user_id': uuid}
        _request('put', self._token_db_url + '/' + token,
                 data=json.dumps(content), do=self._create)

    def teardown(self):
        _request('delete', self._token_db_url, do=self._create)
        _request('delete', self._shared_db_url, do=self._create)


@pytest.fixture()
def soledad_dbs(request):
    couch_url = request.config.option.couch_url

    def create(uuid, create=True):
        db = SoledadDatabases(couch_url, create=create)
        request.addfinalizer(db.teardown)
        return db.setup(uuid)
    return create


#
# soledad_client fixture: provides a clean soledad client for a test function.
#

def _get_certfile(url, tmpdir):

    # download the certificate
    parsed = urlsplit(url)
    netloc = re.sub('^[^\.]+\.', '', parsed.netloc)
    host, _ = netloc.split(':')
    response = requests.get('https://%s/ca.crt' % host, verify=False)

    # store it in a temporary file
    cert_file = os.path.join(tmpdir.strpath, 'cert.pem')
    with open(cert_file, 'w') as f:
        f.write(response.text)

    return cert_file


@pytest.fixture()
def soledad_client(tmpdir, soledad_server, remote_db, soledad_dbs, request):

    # default values for local server
    server_url = DEFAULT_URL
    default_uuid = uuid4().hex
    create = True
    cert_file = None

    # use values for remote server if server url is passed
    url_arg = request.config.option.soledad_server_url
    if url_arg:
        server_url = url_arg
        default_uuid = 'test-user'
        create = False
        cert_file = _get_certfile(server_url, tmpdir)

    remote_db(default_uuid, create=create)
    soledad_dbs(default_uuid, create=create)

    # get a soledad instance
    def create(force_fresh_db=False, uuid=default_uuid,
               passphrase=DEFAULT_PASSPHRASE, token=DEFAULT_TOKEN):

        secrets_file = '%s.secret' % uuid
        secrets_path = os.path.join(tmpdir.strpath, secrets_file)

        # in some tests we might want to use the same user and remote database
        # but with a clean/empty local database (i.e. download benchmarks), so
        # here we provide a way to do that.
        idx = 1
        if force_fresh_db:
            # find the next index for this user
            idx = len(glob.glob('%s/*-*.db' % tmpdir.strpath)) + 1
        db_file = '%s-%d.db' % (uuid, idx)
        local_db_path = os.path.join(tmpdir.strpath, db_file)

        soledad_client = Soledad(
            uuid,
            unicode(passphrase),
            secrets_path=secrets_path,
            local_db_path=local_db_path,
            server_url=server_url,
            cert_file=cert_file,
            auth_token=token,
            with_blobs=True)
        request.addfinalizer(soledad_client.close)
        return soledad_client
    return create


#
# pytest-benchmark customizations
#

# avoid hooking if this is not a benchmarking environment
if 'pytest_benchmark' in sys.modules:

    def pytest_benchmark_update_machine_info(config, machine_info):
        """
        Add the host's hostname information to machine_info.

        Get the value from the HOST_HOSTNAME environment variable if it is set,
        or from the actual system's hostname otherwise.
        """
        hostname = os.environ.get('HOST_HOSTNAME', socket.gethostname())
        machine_info['host'] = hostname


#
# benchmark/responsiveness fixtures
#

@pytest.fixture()
def payload():
    def generate(size):
        random.seed(1337)  # same seed to avoid different bench results
        payload_bytes = bytearray(random.getrandbits(8) for _ in xrange(size))
        # encode as base64 to avoid ascii encode/decode errors
        return base64.b64encode(payload_bytes)[:size]  # remove b64 overhead
    return generate