diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2014-04-08 11:49:53 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2014-04-14 16:06:48 (GMT) |
commit | 71391e654f497234fac0a4602bba769820aa521c (patch) | |
tree | 2b5d6d66a4b23f28581adc4079a1aa28f3907407 | |
parent | 6ec16441c7c133c55385613f1e430c5ea37af632 (diff) |
More implementation polishing
* suppress passing guids while creating objects;
* access to request/response objects via "this";
* represent File objects as url strings;
* sepparate auth code;
* patch Logger.exception instead of using standalone function;
* move releases seqno to node.Volume.
49 files changed, 1758 insertions, 1427 deletions
@@ -4,12 +4,11 @@ - deliver spawn events only to local subscribers - test/run presolve - if node relocates api calls, do it only once in toolkit.http -- Remove temporal security hole with speciying guid in POST, - it was added as a fast hack to support offline creation (with later pushing to a node) - changed pulls should take into account accept_length - secure node-to-node sync - cache init sync pull - switch auth from WWW-AUTHENTICATE to mutual authentication over the HTTPS +- restrict ACL.LOCAL routes only to localhost clients v2.0 ==== diff --git a/sugar-network-client b/sugar-network-client index 5b8b350..386a3b8 100755 --- a/sugar-network-client +++ b/sugar-network-client @@ -30,7 +30,8 @@ import sugar_network_webui as webui from sugar_network import db, toolkit, client, node from sugar_network.client.routes import CachedClientRoutes from sugar_network.client.injector import Injector -from sugar_network.client.model import RESOURCES +from sugar_network.client.model import Volume +from sugar_network.client.auth import BasicCreds, SugarCreds from sugar_network.toolkit.router import Router, Request, Response from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import mountpoints, printf, application @@ -68,7 +69,7 @@ class Application(application.Daemon): printf.info('Index database in %r', client.local_root.value) - volume = db.Volume(client.path('db'), RESOURCES) + volume = Volume(client.path()) try: volume.populate() finally: @@ -104,8 +105,14 @@ class Application(application.Daemon): this.injector = Injector(client.path('cache'), client.cache_lifetime.value, client.cache_limit.value, client.cache_limit_percent.value) - volume = db.Volume(client.path('db'), RESOURCES) - routes = CachedClientRoutes(volume) + volume = Volume(client.path()) + if client.login.value and client.password.value: + creds = BasicCreds(client.login.value, client.password.value) + elif client.keyfile.value: + creds = SugarCreds(client.keyfile.value) + else: + raise RuntimeError('No credentials specified') + routes = CachedClientRoutes(volume, creds) router = Router(routes, allow_spawn=True) logging.info('Listening for IPC requests on %s port', diff --git a/sugar-network-node b/sugar-network-node index b0b4425..64249c7 100755 --- a/sugar-network-node +++ b/sugar-network-node @@ -25,6 +25,8 @@ from sugar_network.toolkit import coroutine coroutine.inject() from sugar_network import db, node, toolkit +from sugar_network.node.auth import SugarAuth +from sugar_network.node.model import Volume from sugar_network.node import obs, master, slave from sugar_network.toolkit.http import Connection from sugar_network.toolkit.router import Router @@ -55,21 +57,23 @@ class Application(application.Daemon): ssl_args['certfile'] = node.certfile.value if node.mode.value == 'master': - node_class = master.MasterRoutes + node_routes_class = master.MasterRoutes resources = master.RESOURCES logging.info('Start master node') else: - node_class = slave.SlaveRoutes + node_routes_class = slave.SlaveRoutes resources = slave.RESOURCES logging.info('Start slave node') - volume = db.Volume(node.data_root.value, resources) - cp = node_class(volume=volume, find_limit=node.find_limit.value) + volume = Volume(node.data_root.value, resources) + node_routes = node_routes_class(volume=volume, + auth=SugarAuth(node.data_root.value), + find_limit=node.find_limit.value) self.jobs.spawn(volume.populate) logging.info('Listening for requests on %s:%s', node.host.value, node.port.value) server = coroutine.WSGIServer((node.host.value, node.port.value), - Router(cp), **ssl_args) + Router(node_routes), **ssl_args) self.jobs.spawn(server.serve_forever) self.accept() diff --git a/sugar_network/client/auth.py b/sugar_network/client/auth.py new file mode 100644 index 0000000..db95aa5 --- /dev/null +++ b/sugar_network/client/auth.py @@ -0,0 +1,112 @@ +# Copyright (C) 2014 Aleksey Lim +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import hashlib +from base64 import b64encode +from urllib2 import parse_http_list, parse_keqv_list +from os.path import abspath, expanduser, dirname, exists + + +class BasicCreds(object): + + def __init__(self, login, password): + self._login = login + self._password = password + + @property + def login(self): + return self._login + + @property + def profile(self): + return None + + def logon(self, challenge): + creds = '%s:%s' % (self._login, self._password) + return {'authorization': 'Basic ' + b64encode(creds)} + + +class SugarCreds(object): + + def __init__(self, key_path): + self._key_path = abspath(expanduser(key_path)) + self._key = None + self._pubkey = None + self._login = None + + @property + def pubkey(self): + if self._pubkey is None: + self.ensure_key() + from M2Crypto.BIO import MemoryBuffer + buf = MemoryBuffer() + self._key.save_pub_key_bio(buf) + self._pubkey = buf.getvalue() + return self._pubkey + + @property + def login(self): + if self._login is None: + self._login = str(hashlib.sha1(self.pubkey).hexdigest()) + return self._login + + @property + def profile(self): + try: + import gconf + gconf_ = gconf.client_get_default() + name = gconf_.get_string('/desktop/sugar/user/nick') + except Exception: + name = self.login + return {'name': name, 'pubkey': self.pubkey} + + def logon(self, challenge): + self.ensure_key() + challenge = challenge.split(' ', 1)[-1] + nonce = parse_keqv_list(parse_http_list(challenge)).get('nonce') + data = hashlib.sha1('%s:%s' % (self.login, nonce)).digest() + signature = self._key.sign(data).encode('hex') + authorization = 'Sugar username="%s",nonce="%s",signature="%s"' % \ + (self.login, nonce, signature) + return {'authorization': authorization} + + def ensure_key(self): + from M2Crypto import RSA + + key_dir = dirname(self._key_path) + if exists(self._key_path): + if os.stat(key_dir).st_mode & 077: + os.chmod(key_dir, 0700) + self._key = RSA.load_key(self._key_path) + return + + if not exists(key_dir): + os.makedirs(key_dir) + os.chmod(key_dir, 0700) + + _logger.info('Generate RSA private key at %r', self._key_path) + self._key = RSA.gen_key(1024, 65537, lambda *args: None) + self._key.save_key(self._key_path, cipher=None) + os.chmod(self._key_path, 0600) + + pub_key_path = self._key_path + '.pub' + with file(pub_key_path, 'w') as f: + f.write('ssh-rsa %s %s@%s' % ( + b64encode('\x00\x00\x00\x07ssh-rsa%s%s' % self._key.pub()), + self.login, + os.uname()[1], + )) + _logger.info('Saved RSA public key at %r', pub_key_path) diff --git a/sugar_network/client/injector.py b/sugar_network/client/injector.py index 6d0c420..69dc06a 100644 --- a/sugar_network/client/injector.py +++ b/sugar_network/client/injector.py @@ -487,7 +487,7 @@ def _exec(context, release, path, args, environ): os.execvpe(args[0], args, env) except BaseException: - logging.exception('Failed to execute %r args=%r', release, args) + _logger.exception('Failed to execute %r args=%r', release, args) finally: os._exit(1) diff --git a/sugar_network/client/journal.py b/sugar_network/client/journal.py index 6a8f5ed..5a6f894 100644 --- a/sugar_network/client/journal.py +++ b/sugar_network/client/journal.py @@ -21,6 +21,7 @@ from tempfile import NamedTemporaryFile from sugar_network import client from sugar_network.toolkit.router import route, Request, File +from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import enforce @@ -63,11 +64,12 @@ class Routes(object): 'reply': ('uid', 'title', 'description', 'preview'), 'order_by': list, }) - def journal_find(self, request, response): + def journal_find(self): enforce(self._ds is not None, 'Journal is inaccessible') import dbus + request = this.request reply = request.pop('reply') if 'preview' in reply: reply.remove('preview') @@ -95,8 +97,8 @@ class Routes(object): return {'result': result, 'total': int(total)} @route('GET', ['journal', None], mime_type='application/json') - def journal_get(self, request, response): - guid = request.guid + def journal_get(self): + guid = this.request.guid return {'guid': guid, 'title': get(guid, 'title'), 'description': get(guid, 'description'), @@ -104,49 +106,49 @@ class Routes(object): } @route('GET', ['journal', None, 'preview']) - def journal_get_preview(self, request, response): - return File(_prop_path(request.guid, 'preview'), meta={ + def journal_get_preview(self): + return File(_prop_path(this.request.guid, 'preview'), meta={ 'content-type': 'image/png', }) @route('GET', ['journal', None, 'data']) - def journal_get_data(self, request, response): - return File(_ds_path(request.guid, 'data'), meta={ - 'content-type': get(request.guid, 'mime_type') or + def journal_get_data(self): + return File(_ds_path(this.request.guid, 'data'), meta={ + 'content-type': get(this.request.guid, 'mime_type') or 'application/octet', }) @route('GET', ['journal', None, None], mime_type='application/json') - def journal_get_prop(self, request, response): - return get(request.guid, request.prop) + def journal_get_prop(self): + return get(this.request.guid, this.request.prop) @route('PUT', ['journal', None], cmd='share') - def journal_share(self, request, response): + def journal_share(self): enforce(self._ds is not None, 'Journal is inaccessible') - guid = request.guid + guid = this.request.guid preview_path = _prop_path(guid, 'preview') enforce(os.access(preview_path, os.R_OK), 'No preview') data_path = _ds_path(guid, 'data') enforce(os.access(data_path, os.R_OK), 'No data') subrequest = Request(method='POST', document='artifact') - subrequest.content = request.content + subrequest.content = this.request.content subrequest.content_type = 'application/json' # pylint: disable-msg=E1101 - subguid = self.fallback(subrequest, response) + subguid = self.fallback(subrequest) subrequest = Request(method='PUT', document='artifact', guid=subguid, prop='preview') subrequest.content_type = 'image/png' with file(preview_path, 'rb') as subrequest.content_stream: - self.fallback(subrequest, response) + self.fallback(subrequest) subrequest = Request(method='PUT', document='artifact', guid=subguid, prop='data') subrequest.content_type = get(guid, 'mime_type') or 'application/octet' with file(data_path, 'rb') as subrequest.content_stream: - self.fallback(subrequest, response) + self.fallback(subrequest) def journal_update(self, guid, data=None, **kwargs): enforce(self._ds is not None, 'Journal is inaccessible') diff --git a/sugar_network/client/model.py b/sugar_network/client/model.py index 6207af2..70c8f46 100644 --- a/sugar_network/client/model.py +++ b/sugar_network/client/model.py @@ -19,18 +19,25 @@ from sugar_network import db from sugar_network.model.user import User from sugar_network.model.post import Post from sugar_network.model.report import Report -from sugar_network.model import context as base_context +from sugar_network.model.context import Context as _Context from sugar_network.toolkit.coroutine import this +from sugar_network.toolkit.router import ACL _logger = logging.getLogger('client.model') -class Context(base_context.Context): +class Context(_Context): - @db.indexed_property(db.List, prefix='RP', default=[]) + @db.indexed_property(db.List, prefix='RP', default=[], + acl=ACL.READ | ACL.LOCAL) def pins(self, value): return value + this.injector.pins(self.guid) -RESOURCES = (User, Context, Post, Report) +class Volume(db.Volume): + + def __init__(self, root): + db.Volume.__init__(self, root, [User, Context, Post, Report]) + for resource in ('user', 'context', 'post'): + self[resource].metadata['author'].acl |= ACL.LOCAL diff --git a/sugar_network/client/routes.py b/sugar_network/client/routes.py index c4b645d..f580789 100644 --- a/sugar_network/client/routes.py +++ b/sugar_network/client/routes.py @@ -15,17 +15,16 @@ import os import logging -from base64 import b64encode from httplib import IncompleteRead from os.path import join from sugar_network import db, client, node, toolkit, model from sugar_network.client import journal from sugar_network.toolkit.coroutine import this -from sugar_network.toolkit.router import ACL, Request, Response, Router +from sugar_network.toolkit.router import Request, Router, File from sugar_network.toolkit.router import route, fallbackroute from sugar_network.toolkit import netlink, zeroconf, coroutine, http, parcel -from sugar_network.toolkit import lsb_release, exception, enforce +from sugar_network.toolkit import ranges, lsb_release, enforce # Flag file to recognize a directory as a synchronization directory @@ -37,20 +36,24 @@ _logger = logging.getLogger('client.routes') class ClientRoutes(model.FrontRoutes, journal.Routes): - def __init__(self, home_volume, no_subscription=False): + def __init__(self, home_volume, creds, no_subscription=False): model.FrontRoutes.__init__(self) journal.Routes.__init__(self) this.localcast = this.broadcast self._local = _LocalRoutes(home_volume) + self._creds = creds self._inline = coroutine.Event() self._inline_job = coroutine.Pool() self._remote_urls = [] self._node = None self._connect_jobs = coroutine.Pool() self._no_subscription = no_subscription - self._auth = _Auth() + self._push_r = toolkit.Bin( + join(home_volume.root, 'var', 'push'), + [[1, None]]) + self._push_job = coroutine.Pool() def connect(self, api=None): if self._connect_jobs: @@ -68,36 +71,36 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): self._local.volume.close() @fallbackroute('GET', ['hub']) - def hub(self, request, response): + def hub(self): """Serve Hub via HTTP instead of file:// for IPC users. Since SSE doesn't support CORS for now. """ - if request.environ['PATH_INFO'] == '/hub': + if this.request.environ['PATH_INFO'] == '/hub': raise http.Redirect('/hub/') - path = request.path[1:] + path = this.request.path[1:] if not path: path = ['index.html'] path = join(client.hub_root.value, *path) mtime = int(os.stat(path).st_mtime) - if request.if_modified_since >= mtime: + if this.request.if_modified_since >= mtime: raise http.NotModified() if path.endswith('.js'): - response.content_type = 'text/javascript' + this.response.content_type = 'text/javascript' if path.endswith('.css'): - response.content_type = 'text/css' - response.last_modified = mtime + this.response.content_type = 'text/css' + this.response.last_modified = mtime return file(path, 'rb') @fallbackroute('GET', ['packages']) - def route_packages(self, request, response): + def route_packages(self): if self._inline.is_set(): - return self.fallback(request, response) + return self.fallback() else: # Let caller know that we are in offline and # no way to process specified request on the node @@ -109,30 +112,31 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): return self._inline.is_set() @route('GET', cmd='whoami', mime_type='application/json') - def whoami(self, request, response): + def whoami(self): if self._inline.is_set(): - result = self.fallback(request, response) + result = self.fallback() result['route'] = 'proxy' else: result = {'roles': [], 'route': 'offline'} - result['guid'] = self._auth.login + result['guid'] = self._creds.login return result @route('GET', [None], arguments={'offset': int, 'limit': int, 'reply': ('guid',)}, mime_type='application/json') - def find(self, request, response): + def find(self): + request = this.request if not self._inline.is_set() or 'pins' in request: - return self._local.call(request, response) + return self._local.call(request, this.response) reply = request.setdefault('reply', ['guid']) if 'pins' not in reply: - return self.fallback(request, response) + return self.fallback() if 'guid' not in reply: # Otherwise there is no way to mixin `pins` reply.append('guid') - result = self.fallback(request, response) + result = self.fallback() directory = self._local.volume[request.resource] for item in result['result']: @@ -143,18 +147,20 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): return result @route('GET', [None, None], mime_type='application/json') - def get(self, request, response): + def get(self): + request = this.request if self._local.volume[request.resource][request.guid].exists: - return self._local.call(request, response) + return self._local.call(request, this.response) else: - return self.fallback(request, response) + return self.fallback() @route('GET', [None, None, None], mime_type='application/json') - def get_prop(self, request, response): + def get_prop(self): + request = this.request if self._local.volume[request.resource][request.guid].exists: - return self._local.call(request, response) + return self._local.call(request, this.response) else: - return self.fallback(request, response) + return self.fallback() @route('POST', ['report'], cmd='submit', mime_type='text/event-stream') def submit_report(self): @@ -186,16 +192,16 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): yield event @route('DELETE', ['context', None], cmd='checkin') - def delete_checkin(self, request): + def delete_checkin(self): this.injector.checkout(this.request.guid) self._checkout_context() @route('PUT', ['context', None], cmd='favorite') - def put_favorite(self, request): + def put_favorite(self): self._checkin_context('favorite') @route('DELETE', ['context', None], cmd='favorite') - def delete_favorite(self, request): + def delete_favorite(self): self._checkout_context('favorite') @route('GET', cmd='recycle') @@ -205,14 +211,13 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): @fallbackroute() def fallback(self, request=None, response=None, **kwargs): if request is None: - request = Request(**kwargs) + request = Request(**kwargs) if kwargs else this.request if response is None: - response = Response() + response = this.response if not self._inline.is_set(): return self._local.call(request, response) - request.principal = self._auth.login try: reply = self._node.call(request, response) if hasattr(reply, 'read'): @@ -235,6 +240,7 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): self._local.volume.mute = True this.injector.api = url this.localcast({'event': 'inline', 'state': 'online'}) + self._push_job.spawn(self._push) def _got_offline(self): if self._node is not None: @@ -245,6 +251,7 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): self._local.volume.mute = False this.injector.api = None this.localcast({'event': 'inline', 'state': 'offline'}) + self._push_job.kill() def _restart_online(self): _logger.debug('Lost %r connection, try to reconnect in %s seconds', @@ -275,9 +282,8 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): def handshake(url): _logger.debug('Connecting to %r node', url) - self._node = client.Connection(url, auth=self._auth) + self._node = client.Connection(url, creds=self._creds) status = self._node.get(cmd='status') - self._auth.allow_basic_auth = (status.get('level') == 'master') seqno = status.get('seqno') if seqno and 'releases' in seqno: this.injector.seqno = seqno['releases'] @@ -302,7 +308,7 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): _logger.debug('Retry %r on gateway error', url) continue except Exception: - exception(_logger, 'Connection to %r failed', url) + _logger.exception('Connection to %r failed', url) break self._got_offline() if not timeout: @@ -323,7 +329,9 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): _logger.debug('Checkin %r context', context.guid) clone = self.fallback( method='GET', path=['context', context.guid], cmd='clone') - this.volume.patch(next(parcel.decode(clone))) + seqno, __ = this.volume.patch(next(parcel.decode(clone))) + if seqno: + ranges.exclude(self._push_r.value, seqno, seqno) pins = context['pins'] if pin and pin not in pins: this.volume['context'].update(context.guid, {'pins': pins + [pin]}) @@ -342,90 +350,52 @@ class ClientRoutes(model.FrontRoutes, journal.Routes): else: directory.delete(context.guid) + def _push(self): + return + resource = None + metadata = None + + for diff in self._local.volume.diff(self._push_r.value, blobs=False): + if 'resource' in diff: + resource = diff['resource'] + metadata = self._local.volume[resource] + elif 'commit' in diff: + ranges.exclude(self._push_r.value, diff['commit']) + self._push_r.commit() + # No reasons to keep failure reports after pushing + self._local.volume['report'].wipe() + else: + props = {} + blobs = [] + for prop, meta in diff['patch'].items(): + if isinstance(metadata[prop], db.Blob): + blobs.application -class CachedClientRoutes(ClientRoutes): - def __init__(self, home_volume, api_url=None, no_subscription=False): - self._push_seq = toolkit.PersistentSequence( - join(home_volume.root, 'push.sequence'), [1, None]) - self._push_job = coroutine.Pool() - ClientRoutes.__init__(self, home_volume, api_url, no_subscription) - def _got_online(self, url): - ClientRoutes._got_online(self, url) - self._push_job.spawn(self._push) + props[prop] = meta['value'] - def _got_offline(self): - self._push_job.kill() - ClientRoutes._got_offline(self) - def _push(self): - # TODO should work using regular diff - return + if isinstance(diff, File): + with file(diff.path, 'rb') as f: + self.fallback(method='POST') - pushed_seq = toolkit.Sequence() - skiped_seq = toolkit.Sequence() - volume = self._local.volume - def push(request, seq): - try: - self.fallback(request) - except Exception: - _logger.exception('Cannot push %r, will postpone', request) - skiped_seq.include(seq) - else: - pushed_seq.include(seq) - - for res in volume.resources: - if volume.mtime(res) <= self._push_seq.mtime: - continue - - _logger.debug('Check %r local cache to push', res) - - for guid, patch in volume[res].diff(self._push_seq): - diff = {} - diff_seq = toolkit.Sequence() - post_requests = [] - for prop, meta, seqno in patch: - value = meta['value'] - diff[prop] = value - diff_seq.include(seqno, seqno) - if not diff: - continue - if 'guid' in diff: - request = Request(method='POST', path=[res]) - access = ACL.CREATE | ACL.WRITE - else: - request = Request(method='PUT', path=[res, guid]) - access = ACL.WRITE - for name in diff.keys(): - if not (volume[res].metadata[name].acl & access): - del diff[name] - request.content_type = 'application/json' - request.content = diff - push(request, diff_seq) - for request, seqno in post_requests: - push(request, [[seqno, seqno]]) - - if not pushed_seq: - if not self._push_seq.mtime: - self._push_seq.commit() - return - _logger.info('Pushed %r local cache', pushed_seq) - self._push_seq.exclude(pushed_seq) - if not skiped_seq: - self._push_seq.stretch() - if 'report' in volume: - # No any decent reasons to keep fail reports after uploding. - # TODO The entire offlile synchronization should be improved, - # for now, it is possible to have a race here - volume['report'].wipe() - self._push_seq.commit() + pass + + + if 'guid' in props: + request = Request(method='POST', path=[resource]) + else: + request = Request(method='PUT', path=[resource, guid]) + request.content_type = 'application/json' + request.content = props + self.fallback(request) class _LocalRoutes(db.Routes, Router): @@ -453,28 +423,3 @@ class _ResponseStream(object): except (http.ConnectionError, IncompleteRead): self._on_fail_cb() raise - - -class _Auth(http.SugarAuth): - - def __init__(self): - http.SugarAuth.__init__(self, client.keyfile.value) - if client.login.value: - self._login = client.login.value - self.allow_basic_auth = False - - def profile(self): - if self.allow_basic_auth and \ - client.login.value and client.password.value: - return None - import gconf - conf = gconf.client_get_default() - self._profile['name'] = conf.get_string('/desktop/sugar/user/nick') - return http.SugarAuth.profile(self) - - def __call__(self, nonce): - if not self.allow_basic_auth or \ - not client.login.value or not client.password.value: - return http.SugarAuth.__call__(self, nonce) - auth = b64encode('%s:%s' % (client.login.value, client.password.value)) - return 'Basic %s' % auth diff --git a/sugar_network/db/__init__.py b/sugar_network/db/__init__.py index b2ceb67..d6b12c5 100644 --- a/sugar_network/db/__init__.py +++ b/sugar_network/db/__init__.py @@ -235,8 +235,8 @@ The example code uses all mentioned above features:: return self.volume[document].create(item.properties(['prop1', 'prop2'])) @db.property_command(method='PUT', cmd='mutate') - def mutate(self, document, guid, prop, request): - self.volume[document].update(guid, {prop: request.content}) + def mutate(self, document, guid, prop): + self.volume[document].update(guid, {prop: this.request.content}) volume = db.Volume('db', [MyDocyment]) cp = MyCommands(volume) diff --git a/sugar_network/db/blobs.py b/sugar_network/db/blobs.py index cfbe517..54fd78a 100644 --- a/sugar_network/db/blobs.py +++ b/sugar_network/db/blobs.py @@ -190,8 +190,8 @@ class Blobs(object): break def patch(self, patch, seqno): - if 'path' in patch: - path = self.path(patch.pop('path')) + if 'path' in patch.meta: + path = self.path(patch.meta.pop('path')) else: path = self._blob_path(patch.digest) if not patch.size: @@ -202,9 +202,9 @@ class Blobs(object): os.rename(patch.path, path) if exists(path + _META_SUFFIX): meta = _read_meta(path) - meta.update(patch) + meta.update(patch.meta) else: - meta = patch + meta = patch.meta meta['x-seqno'] = str(seqno) _write_meta(path, meta, seqno) os.utime(path, (seqno, seqno)) diff --git a/sugar_network/db/directory.py b/sugar_network/db/directory.py index 7fe127d..ecda920 100644 --- a/sugar_network/db/directory.py +++ b/sugar_network/db/directory.py @@ -20,7 +20,8 @@ from os.path import exists, join from sugar_network import toolkit from sugar_network.db.storage import Storage from sugar_network.db.metadata import Metadata, Guid -from sugar_network.toolkit import exception, enforce +from sugar_network.toolkit.router import ACL +from sugar_network.toolkit import enforce # To invalidate existed index on stcuture changes @@ -173,7 +174,7 @@ class Directory(object): self._index.store(guid, props) yield except Exception: - exception('Cannot populate %r in %r, invalidate it', + _logger.exception('Cannot populate %r in %r, invalidate it', guid, self.metadata.name) record.invalidate() @@ -227,8 +228,11 @@ class Directory(object): def _prestore(self, guid, changes, event): doc = self.resource(guid, self._storage.get(guid), posts=changes) - doc.post_seqno = self._seqno.next() + # It is important to iterate the `changes` by keys, + # values might be changed during iteration for prop in changes.keys(): + if not doc.post_seqno and not doc.metadata[prop].acl & ACL.LOCAL: + doc.post_seqno = self._seqno.next() doc.post(prop, changes[prop]) for prop in self.metadata.keys(): enforce(doc[prop] is not None, 'Empty %r property', prop) diff --git a/sugar_network/db/index.py b/sugar_network/db/index.py index eb8f0cb..89ea6e8 100644 --- a/sugar_network/db/index.py +++ b/sugar_network/db/index.py @@ -23,7 +23,7 @@ from os.path import exists, join import xapian from sugar_network.db.metadata import GUID_PREFIX -from sugar_network.toolkit import Option, coroutine, exception, enforce +from sugar_network.toolkit import Option, coroutine, enforce index_flush_timeout = Option( @@ -398,7 +398,7 @@ class IndexWriter(IndexReader): self._db = xapian.WritableDatabase(self._path, xapian.DB_CREATE_OR_OPEN) except xapian.DatabaseError: - exception('Cannot open Xapian index in %r, will rebuild it', + _logger.exception('Cannot open Xapian %r index, will rebuild', self.metadata.name) shutil.rmtree(self._path, ignore_errors=True) self._db = xapian.WritableDatabase(self._path, diff --git a/sugar_network/db/metadata.py b/sugar_network/db/metadata.py index 67a6d13..e820fc9 100644 --- a/sugar_network/db/metadata.py +++ b/sugar_network/db/metadata.py @@ -381,7 +381,10 @@ class Aggregated(Composite): self._subtype.teardown(value) def typecast(self, value): - return dict(value) + raise RuntimeError('Aggregated properties cannot be set directly') + + def reprcast(self, value): + return [(i, self.subreprcast(j['value'])) for i, j in value.items()] def encode(self, items): for agg in items.values(): diff --git a/sugar_network/db/resource.py b/sugar_network/db/resource.py index 38c1ce4..9af5086 100644 --- a/sugar_network/db/resource.py +++ b/sugar_network/db/resource.py @@ -85,7 +85,7 @@ class Resource(object): def status(self, value): return value - @indexed_property(List, prefix='RP', default=[]) + @indexed_property(List, prefix='RP', default=[], acl=ACL.READ) def pins(self, value): return value @@ -163,7 +163,7 @@ class Resource(object): def diff(self, r): patch = {} for name, prop in self.metadata.items(): - if name == 'seqno' or prop.acl & ACL.CALC: + if name == 'seqno' or prop.acl & (ACL.CALC | ACL.LOCAL): continue meta = self.meta(name) if meta is None: @@ -203,15 +203,18 @@ class Resource(object): prop = self.metadata[prop] if prop.on_set is not None: value = prop.on_set(self, value) - if isinstance(prop, Aggregated): + seqno = None + if not prop.acl & ACL.LOCAL: + seqno = meta['seqno'] = self.post_seqno + if seqno and isinstance(prop, Aggregated): for agg in value.values(): - agg['seqno'] = self.post_seqno + agg['seqno'] = seqno if isinstance(prop, Composite): orig_value = self.orig(prop.name) if orig_value: orig_value.update(value) value = orig_value - self.record.set(prop.name, value=value, seqno=self.post_seqno, **meta) + self.record.set(prop.name, value=value, **meta) self.posts[prop.name] = value def __contains__(self, prop): diff --git a/sugar_network/db/routes.py b/sugar_network/db/routes.py index f319658..c74a93e 100644 --- a/sugar_network/db/routes.py +++ b/sugar_network/db/routes.py @@ -19,7 +19,7 @@ from contextlib import contextmanager from sugar_network import toolkit from sugar_network.db.metadata import Aggregated -from sugar_network.toolkit.router import ACL, File, route, fallbackroute +from sugar_network.toolkit.router import ACL, route, fallbackroute from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http, parcel, enforce @@ -32,64 +32,62 @@ _logger = logging.getLogger('db.routes') class Routes(object): def __init__(self, volume, find_limit=None): - self.volume = volume + this.volume = self.volume = volume self._find_limit = find_limit - this.volume = self.volume @route('POST', [None], acl=ACL.AUTH, mime_type='application/json') - def create(self, request): - with self._post(request, ACL.CREATE) as doc: + def create(self): + with self._post(ACL.CREATE) as doc: doc.created() - if request.principal: + if this.principal: authors = doc.posts['author'] = {} - self._useradd(authors, request.principal, ACL.ORIGINAL) - self.volume[request.resource].create(doc.posts) + self._useradd(authors, this.principal, ACL.ORIGINAL) + self.volume[this.request.resource].create(doc.posts) return doc['guid'] @route('GET', [None], arguments={'offset': int, 'limit': int, 'reply': ('guid',)}, mime_type='application/json') - def find(self, request, reply, limit): - self._preget(request) - if self._find_limit: - if limit <= 0: - request['limit'] = self._find_limit - elif limit > self._find_limit: - _logger.warning('The find limit is restricted to %s', - self._find_limit) - request['limit'] = self._find_limit + def find(self, reply, limit): + self._preget() + request = this.request + if self._find_limit and limit > self._find_limit: + _logger.warning('The find limit is restricted to %s', + self._find_limit) + request['limit'] = self._find_limit documents, total = self.volume[request.resource].find( not_state='deleted', **request) - result = [self._postget(request, i, reply) for i in documents] + result = [self._postget(i, reply) for i in documents] return {'total': total, 'result': result} @route('GET', [None, None], cmd='exists', mime_type='application/json') - def exists(self, request): - return self.volume[request.resource][request.guid].exists + def exists(self): + return self.volume[this.request.resource][this.request.guid].exists @route('PUT', [None, None], acl=ACL.AUTH | ACL.AUTHOR) - def update(self, request): - with self._post(request, ACL.WRITE) as doc: + def update(self): + with self._post(ACL.WRITE) as doc: if not doc.posts: return doc.updated() - self.volume[request.resource].update(doc.guid, doc.posts) + self.volume[this.request.resource].update(doc.guid, doc.posts) @route('PUT', [None, None, None], acl=ACL.AUTH | ACL.AUTHOR) - def update_prop(self, request): + def update_prop(self): + request = this.request if request.content is None: value = request.content_stream else: value = request.content request.content = {request.prop: value} - self.update(request) + self.update() @route('DELETE', [None, None], acl=ACL.AUTH | ACL.AUTHOR) - def delete(self, request): + def delete(self): # Node data should not be deleted immediately # to make master-slave synchronization possible - directory = self.volume[request.resource] - doc = directory[request.guid] + directory = self.volume[this.request.resource] + doc = directory[this.request.guid] enforce(doc.exists, http.NotFound, 'Resource not found') doc.posts['state'] = 'deleted' doc.updated() @@ -97,45 +95,43 @@ class Routes(object): @route('GET', [None, None], arguments={'reply': list}, mime_type='application/json') - def get(self, request, reply): + def get(self, reply): if not reply: reply = [] - for prop in self.volume[request.resource].metadata.values(): - if prop.acl & ACL.READ and not (prop.acl & ACL.LOCAL) and \ - not isinstance(prop, Aggregated): + for prop in self.volume[this.request.resource].metadata.values(): + if prop.acl & ACL.READ and not isinstance(prop, Aggregated): reply.append(prop.name) - self._preget(request) - doc = self.volume[request.resource].get(request.guid) + self._preget() + doc = self.volume[this.request.resource].get(this.request.guid) enforce(doc.exists and doc['state'] != 'deleted', http.NotFound, 'Resource not found') - return self._postget(request, doc, reply) + return self._postget(doc, reply) @route('GET', [None, None, None], mime_type='application/json') - def get_prop(self, request, response): + def get_prop(self): + request = this.request directory = self.volume[request.resource] directory.metadata[request.prop].assert_access(ACL.READ) - value = directory[request.guid].repr(request.prop) - enforce(value is not File.AWAY, http.NotFound, 'No blob') - return value + return directory[request.guid].repr(request.prop) @route('HEAD', [None, None, None]) - def get_prop_meta(self, request, response): - return self.get_prop(request, response) + def get_prop_meta(self): + return self.get_prop() @route('POST', [None, None, None], acl=ACL.AUTH, mime_type='application/json') - def insert_to_aggprop(self, request): - return self._aggpost(request, ACL.INSERT) + def insert_to_aggprop(self): + return self._aggpost(ACL.INSERT) @route('PUT', [None, None, None, None], acl=ACL.AUTH, mime_type='application/json') - def update_aggprop(self, request): - self._aggpost(request, ACL.REPLACE, request.key) + def update_aggprop(self): + self._aggpost(ACL.REPLACE) @route('DELETE', [None, None, None, None], acl=ACL.AUTH, mime_type='application/json') - def remove_from_aggprop(self, request): - self._aggpost(request, ACL.REMOVE, request.key) + def remove_from_aggprop(self): + self._aggpost(ACL.REMOVE) @route('GET', [None, None, None, None], mime_type='application/json') def get_aggprop(self): @@ -147,13 +143,12 @@ class Routes(object): agg_value = doc[prop.name].get(this.request.key) enforce(agg_value is not None, http.NotFound, 'Aggregated item not found') - value = prop.subreprcast(agg_value['value']) - enforce(value is not File.AWAY, http.NotFound, 'No blob') - return value + return prop.subreprcast(agg_value['value']) @route('PUT', [None, None], cmd='useradd', arguments={'role': 0}, acl=ACL.AUTH | ACL.AUTHOR) - def useradd(self, request, user, role): + def useradd(self, user, role): + request = this.request enforce(user, "Argument 'user' is not specified") directory = self.volume[request.resource] authors = directory.get(request.guid)['author'] @@ -161,9 +156,10 @@ class Routes(object): directory.update(request.guid, {'author': authors}) @route('PUT', [None, None], cmd='userdel', acl=ACL.AUTH | ACL.AUTHOR) - def userdel(self, request, user): + def userdel(self, user): + request = this.request enforce(user, "Argument 'user' is not specified") - enforce(user != request.principal, 'Cannot remove yourself') + enforce(user != this.principal, 'Cannot remove yourself') directory = self.volume[request.resource] authors = directory.get(request.guid)['author'] enforce(user in authors, 'No such user') @@ -171,38 +167,36 @@ class Routes(object): directory.update(request.guid, {'author': authors}) @route('GET', [None, None], cmd='clone') - def clone(self, request): - clone = self.volume.clone(request.resource, request.guid) + def clone(self): + clone = self.volume.clone(this.request.resource, this.request.guid) return parcel.encode([('push', None, clone)]) @fallbackroute('GET', ['blobs']) def blobs(self): - return this.volume.blobs.get(this.request.guid) - - def on_aggprop_update(self, request, prop, value): - pass + return self.volume.blobs.get(this.request.guid) @contextmanager - def _post(self, request, access): - content = request.content + def _post(self, access): + content = this.request.content enforce(isinstance(content, dict), http.BadRequest, 'Invalid value') if access == ACL.CREATE: - if 'guid' in content: - # TODO Temporal security hole, see TODO - guid = content['guid'] + guid = content.get('guid') + if guid: + enforce(this.principal and this.principal.admin, + http.BadRequest, 'GUID should not be specified') enforce(_GUID_RE.match(guid) is not None, - http.BadRequest, 'Malformed %s GUID', guid) + http.BadRequest, 'Malformed GUID') else: guid = toolkit.uuid() - doc = self.volume[request.resource][guid] + doc = self.volume[this.request.resource][guid] enforce(not doc.exists, 'Resource already exists') doc.posts['guid'] = guid for name, prop in doc.metadata.items(): if name not in content and prop.default is not None: doc.posts[name] = prop.default else: - doc = self.volume[request.resource][request.guid] + doc = self.volume[this.request.resource][this.request.guid] enforce(doc.exists, 'Resource not found') this.resource = doc @@ -223,7 +217,7 @@ class Routes(object): except Exception, error: error = 'Value %r for %r property is invalid: %s' % \ (value, prop.name, error) - toolkit.exception(error) + _logger.exception(error) raise http.BadRequest(error) yield doc except Exception: @@ -232,22 +226,19 @@ class Routes(object): else: teardown(doc.origs, doc.posts) - def _preget(self, request): - reply = request.get('reply') + def _preget(self): + reply = this.request.get('reply') if not reply: - request['reply'] = ('guid',) + this.request['reply'] = ('guid',) else: - directory = self.volume[request.resource] + directory = self.volume[this.request.resource] for prop in reply: directory.metadata[prop].assert_access(ACL.READ) - def _postget(self, request, doc, props): + def _postget(self, doc, props): result = {} for name in props: - value = doc.repr(name) - if isinstance(value, File): - value = value.url - result[name] = value + result[name] = doc.repr(name) return result def _useradd(self, authors, user, role): @@ -270,20 +261,29 @@ class Routes(object): props['order'] = 0 authors[user] = props - def _aggpost(self, request, acl, aggid=None): + def _aggpost(self, acl): + request = this.request doc = this.resource = self.volume[request.resource][request.guid] prop = doc.metadata[request.prop] enforce(isinstance(prop, Aggregated), http.BadRequest, 'Property is not aggregated') prop.assert_access(acl) + def enforce_authority(author): + if prop.acl & ACL.AUTHOR: + author = doc['author'] + enforce(not author or this.principal in author or + this.principal and this.principal.admin, + http.Forbidden, 'Authors only') + + aggid = request.key if aggid and aggid in doc[request.prop]: aggvalue = doc[request.prop][aggid] - self.on_aggprop_update(request, prop, aggvalue) + enforce_authority(aggvalue.get('author')) prop.subteardown(aggvalue['value']) else: enforce(acl != ACL.REMOVE, http.NotFound, 'No aggregated item') - self.on_aggprop_update(request, prop, None) + enforce_authority(None) aggvalue = {} if acl != ACL.REMOVE: @@ -299,10 +299,10 @@ class Routes(object): aggid = toolkit.uuid() aggvalue['value'] = value - if request.principal: + if this.principal: authors = aggvalue['author'] = {} - role = ACL.ORIGINAL if request.principal in doc['author'] else 0 - self._useradd(authors, request.principal, role) + role = ACL.ORIGINAL if this.principal in doc['author'] else 0 + self._useradd(authors, this.principal, role) doc.posts[request.prop] = {aggid: aggvalue} doc.updated() self.volume[request.resource].update(request.guid, doc.posts) diff --git a/sugar_network/db/volume.py b/sugar_network/db/volume.py index 7bf738c..295fc02 100644 --- a/sugar_network/db/volume.py +++ b/sugar_network/db/volume.py @@ -23,6 +23,7 @@ from sugar_network.db.metadata import Blob from sugar_network.db.directory import Directory from sugar_network.db.index import IndexWriter from sugar_network.db.blobs import Blobs +from sugar_network.toolkit.router import File from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http, coroutine, ranges, enforce @@ -34,7 +35,7 @@ class Volume(dict): _flush_pool = [] - def __init__(self, root, documents, index_class=None): + def __init__(self, root, resources, index_class=None): Volume._flush_pool.append(self) self.resources = {} self.mute = False @@ -49,12 +50,10 @@ class Volume(dict): if not exists(root): os.makedirs(root) self._index_class = index_class - self.seqno = toolkit.Seqno(join(self._root, 'var', 'db.seqno')) - self.releases_seqno = toolkit.Seqno( - join(self._root, 'var', 'releases.seqno')) + self.seqno = toolkit.Seqno(join(self._root, 'var', 'seqno')) self.blobs = Blobs(root, self.seqno) - for document in documents: + for document in resources: if isinstance(document, basestring): name = document.split('.')[-1] else: @@ -72,14 +71,13 @@ class Volume(dict): while self: __, cls = self.popitem() cls.close() - self.releases_seqno.commit() def populate(self): for cls in self.values(): for __ in cls.populate(): coroutine.dispatch() - def diff(self, r, exclude=None, files=None, one_way=False): + def diff(self, r, exclude=None, files=None, blobs=True, one_way=False): if exclude: include = deepcopy(r) ranges.exclude(include, exclude) @@ -105,14 +103,15 @@ class Volume(dict): yield {'guid': doc.guid, 'patch': patch} found = True last_seqno = max(last_seqno, doc['seqno']) - for blob in self.blobs.diff(include): - seqno = int(blob.pop('x-seqno')) - yield blob - found = True - last_seqno = max(last_seqno, seqno) + if blobs: + for blob in self.blobs.diff(include): + seqno = int(blob.meta.pop('x-seqno')) + yield blob + found = True + last_seqno = max(last_seqno, seqno) for dirpath in files or []: for blob in self.blobs.diff(include, dirpath): - seqno = int(blob.pop('x-seqno')) + seqno = int(blob.meta.pop('x-seqno')) yield blob found = True last_seqno = max(last_seqno, seqno) @@ -142,25 +141,24 @@ class Volume(dict): seqno = None for record in records: - resource_ = record.get('resource') - if resource_: - directory = self[resource_] - continue - - if 'guid' in record: - seqno = directory.patch(record['guid'], record['patch'], seqno) - continue - - if 'content-length' in record: + if isinstance(record, File): if seqno is None: seqno = self.seqno.next() self.blobs.patch(record, seqno) continue - + resource = record.get('resource') + if resource: + directory = self[resource] + continue + guid = record.get('guid') + if guid is not None: + seqno = directory.patch(guid, record['patch'], seqno) + continue commit = record.get('commit') if commit is not None: ranges.include(committed, commit) continue + raise http.BadRequest('Malformed patch') return seqno, committed diff --git a/sugar_network/model/__init__.py b/sugar_network/model/__init__.py index 4ff89ff..c6b3321 100644 --- a/sugar_network/model/__init__.py +++ b/sugar_network/model/__init__.py @@ -28,7 +28,7 @@ from sugar_network.toolkit.spec import EMPTY_LICENSE from sugar_network.toolkit.coroutine import this from sugar_network.toolkit.bundle import Bundle from sugar_network.toolkit.router import ACL -from sugar_network.toolkit import i18n, http, svg_to_png, exception, enforce +from sugar_network.toolkit import i18n, http, svg_to_png, enforce CONTEXT_TYPES = [ @@ -87,6 +87,9 @@ class Release(object): context=this.request.guid) return release['bundles']['*-*']['blob'], release + def reprcast(self, release): + return this.volume.blobs.get(release['bundles']['*-*']['blob']) + def teardown(self, release): if this.resource.exists and \ 'activity' not in this.resource['type'] and \ @@ -180,19 +183,21 @@ def load_bundle(blob, context=None, initial=False, extra_deps=None): 'unpack_size': unpack_size, }, } - blob['content-type'] = 'application/vnd.olpc-sugar' + blob.meta['content-type'] = 'application/vnd.olpc-sugar' enforce(context, http.BadRequest, 'Context is not specified') enforce(version, http.BadRequest, 'Version is not specified') release['version'] = parse_version(version) doc = this.volume['context'][context] - if initial: - if not doc.exists: - enforce(context_meta, http.BadRequest, 'No way to initate context') - context_meta['guid'] = context - context_meta['type'] = [context_type] - this.call(method='POST', path=['context'], content=context_meta) + if initial and not doc.exists: + enforce(context_meta, http.BadRequest, 'No way to initate context') + context_meta['guid'] = context + context_meta['type'] = [context_type] + with this.principal as principal: + principal.admin = True + this.call(method='POST', path=['context'], content=context_meta, + principal=principal) else: enforce(doc.exists, http.NotFound, 'No context') enforce(context_type in doc['type'], @@ -207,10 +212,11 @@ def load_bundle(blob, context=None, initial=False, extra_deps=None): _logger.debug('Load %r release: %r', context, release) - if this.request.principal in doc['author']: + if this.principal in doc['author']: patch = doc.format_patch(context_meta) if patch: - this.call(method='PUT', path=['context', context], content=patch) + this.call(method='PUT', path=['context', context], content=patch, + principal=this.principal) doc.posts.update(patch) # TRANS: Release notes title title = i18n._('%(name)s %(version)s release') @@ -227,13 +233,13 @@ def load_bundle(blob, context=None, initial=False, extra_deps=None): ), 'message': release_notes or '', }, - content_type='application/json') + content_type='application/json', principal=this.principal) - blob['content-disposition'] = 'attachment; filename="%s-%s%s"' % ( - ''.join(i18n.decode(doc['title']).split()), - version, mimetypes.guess_extension(blob.get('content-type')) or '', + blob.meta['content-disposition'] = 'attachment; filename="%s-%s%s"' % ( + ''.join(i18n.decode(doc['title']).split()), version, + mimetypes.guess_extension(blob.meta.get('content-type')) or '', ) - this.volume.blobs.update(blob.digest, blob) + this.volume.blobs.update(blob.digest, blob.meta) return context, release @@ -261,7 +267,7 @@ def _load_context_metadata(bundle, spec): icon_file.close() except Exception: - exception(_logger, 'Failed to load icon') + _logger.exception('Failed to load icon') msgids = {} for prop, confname in [ @@ -289,6 +295,6 @@ def _load_context_metadata(bundle, spec): if lang == 'en' or msgstr != value: result[prop][lang] = msgstr except Exception: - exception(_logger, 'Gettext failed to read %r', mo_path[-1]) + _logger.exception('Gettext failed to read %r', mo_path[-1]) return result diff --git a/sugar_network/model/context.py b/sugar_network/model/context.py index 5e12360..78df790 100644 --- a/sugar_network/model/context.py +++ b/sugar_network/model/context.py @@ -113,7 +113,7 @@ class Context(db.Resource): def rating(self, value): return value - @db.stored_property(default='', acl=ACL.PUBLIC | ACL.LOCAL) + @db.stored_property(default='', acl=ACL.PUBLIC) def dependencies(self, value): """Software dependencies. @@ -122,20 +122,3 @@ class Context(db.Resource): """ return value - - def created(self): - db.Resource.created(self) - self._invalidate_solutions() - - def updated(self): - db.Resource.updated(self) - self._invalidate_solutions() - - def _invalidate_solutions(self): - if self['releases'] and \ - [i for i in ('state', 'releases', 'dependencies') - if i in self.posts and self.posts[i] != self.orig(i)]: - this.broadcast({ - 'event': 'release', - 'seqno': this.volume.releases_seqno.next(), - }) diff --git a/sugar_network/model/routes.py b/sugar_network/model/routes.py index fb409d4..eda26dc 100644 --- a/sugar_network/model/routes.py +++ b/sugar_network/model/routes.py @@ -35,50 +35,32 @@ class FrontRoutes(object): return _HELLO_HTML @route('OPTIONS') - def options(self, request, response): - if request.environ['HTTP_ORIGIN']: + def options(self): + response = this.response + environ = this.request.environ + if environ['HTTP_ORIGIN']: response['Access-Control-Allow-Methods'] = \ - request.environ['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] + environ['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] response['Access-Control-Allow-Headers'] = \ - request.environ['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] + environ['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'] else: response['Allow'] = 'GET, HEAD, POST, PUT, DELETE' response.content_length = 0 @route('GET', cmd='subscribe', mime_type='text/event-stream') - def subscribe(self, request=None, response=None, **condition): + def subscribe(self, **condition): """Subscribe to Server-Sent Events.""" - if request is not None and not condition: - condition = request - if response is not None: - response.content_type = 'text/event-stream' - response['Cache-Control'] = 'no-cache' - return self._pull_events(request, condition) + this.response['Cache-Control'] = 'no-cache' - @route('GET', ['robots.txt'], mime_type='text/plain') - def robots(self, request, response): - return 'User-agent: *\nDisallow: /\n' - - @route('GET', ['favicon.ico']) - def favicon(self): - return this.volume.blobs.get('favicon.ico') - - def _broadcast(self, event): - _logger.debug('Broadcast event: %r', event) - self._spooler.notify_all(event) - - def _pull_events(self, request, condition): _logger.debug('Start %s-nth subscription', self._spooler.waiters + 1) # Unblock `GET /?cmd=subscribe` call to let non-greenlet application # initiate a subscription and do not stuck in waiting for the 1st event yield {'event': 'pong'} - subscription = None - if request is not None: - subscription = request.content_stream - if subscription is not None: - coroutine.spawn(self._wait_for_closing, subscription) + subscription = this.request.content_stream + if subscription is not None: + coroutine.spawn(self._wait_for_closing, subscription) while True: event = self._spooler.wait() @@ -98,6 +80,18 @@ class FrontRoutes(object): _logger.debug('Stop %s-nth subscription', self._spooler.waiters) + @route('GET', ['robots.txt'], mime_type='text/plain') + def robots(self): + return 'User-agent: *\nDisallow: /\n' + + @route('GET', ['favicon.ico']) + def favicon(self): + return this.volume.blobs.get('favicon.ico') + + def _broadcast(self, event): + _logger.debug('Broadcast event: %r', event) + self._spooler.notify_all(event) + def _wait_for_closing(self, rfile): try: coroutine.select([rfile.fileno()], [], []) diff --git a/sugar_network/model/user.py b/sugar_network/model/user.py index b44093e..41f48a0 100644 --- a/sugar_network/model/user.py +++ b/sugar_network/model/user.py @@ -31,6 +31,6 @@ class User(db.Resource): def birthday(self, value): return value - @db.stored_property(db.Blob, acl=ACL.CREATE, mime_type='text/plain') + @db.stored_property(acl=ACL.READ | ACL.CREATE) def pubkey(self, value): return value diff --git a/sugar_network/node/auth.py b/sugar_network/node/auth.py new file mode 100644 index 0000000..27b334c --- /dev/null +++ b/sugar_network/node/auth.py @@ -0,0 +1,118 @@ +# Copyright (C) 2014 Aleksey Lim +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import time +import hashlib +import logging +from ConfigParser import ConfigParser +from os.path import join, dirname, exists, expanduser, abspath + +from sugar_network.toolkit.coroutine import this +from sugar_network.toolkit import pylru, http, enforce + + +_SIGNATURE_LIFETIME = 600 +_AUTH_POOL_SIZE = 1024 + +_logger = logging.getLogger('node.auth') + + +class Unauthorized(http.Unauthorized): + + def __init__(self, message, nonce=None): + http.Unauthorized.__init__(self, message) + if not nonce: + nonce = int(time.time()) + _SIGNATURE_LIFETIME + self.headers = {'www-authenticate': 'Sugar nonce="%s"' % nonce} + + +class Principal(str): + + admin = False + editor = False + translator = False + + _backup = None + + def __enter__(self): + self._backup = (self.admin, self.editor, self.translator) + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.admin, self.editor, self.translator = self._backup + self._backup = None + + +class SugarAuth(object): + + def __init__(self, root): + self._config_path = join(root, 'etc', 'authorization.conf') + self._pool = pylru.lrucache(_AUTH_POOL_SIZE) + self._config = None + + def reload(self): + self._config = ConfigParser() + if exists(self._config_path): + self._config.read(self._config_path) + self._pool.clear() + + def logon(self, request): + auth = request.environ.get('HTTP_AUTHORIZATION') + enforce(auth, Unauthorized, 'No credentials') + + if self._config is None: + self.reload() + + from M2Crypto import RSA, BIO + from urllib2 import parse_http_list, parse_keqv_list + + if auth in self._pool: + login, nonce = self._pool[auth] + else: + scheme, creds = auth.strip().split(' ', 1) + enforce(scheme.lower() == 'sugar', http.BadRequest, + 'Unsupported authentication scheme') + creds = parse_keqv_list(parse_http_list(creds)) + login = creds['username'] + signature = creds['signature'] + nonce = int(creds['nonce']) + user = this.volume['user'][login] + enforce(user.exists, Unauthorized, 'Principal does not exist') + key = RSA.load_pub_key_bio(BIO.MemoryBuffer(str(user['pubkey']))) + data = hashlib.sha1('%s:%s' % (login, nonce)).digest() + enforce(key.verify(data, signature.decode('hex')), + http.Forbidden, 'Bad credentials') + self._pool[auth] = (login, nonce) + + enforce(abs(time.time() - nonce) <= _SIGNATURE_LIFETIME, + Unauthorized, 'Credentials expired') + principal = Principal(login) + + user = principal + if not self._config.has_option('permissions', user): + user = 'default' + if not self._config.has_option('permissions', user): + user = None + if user: + for role in self._config.get('permissions', user).split(): + role = role.lower() + if role == 'admin': + principal.admin = True + elif role == 'editor': + principal.editor = True + elif role == 'translator': + principal.translator = True + + return principal diff --git a/sugar_network/node/master.py b/sugar_network/node/master.py index b93dcbc..c5b15e6 100644 --- a/sugar_network/node/master.py +++ b/sugar_network/node/master.py @@ -58,8 +58,8 @@ class MasterRoutes(NodeRoutes): @route('PUT', ['context', None], cmd='presolve', acl=ACL.AUTH, mime_type='application/json') - def presolve(self, request): - aliases = this.volume['context'].get(request.guid)['aliases'] + def presolve(self): + aliases = this.volume['context'].get(this.request.guid)['aliases'] enforce(aliases, http.BadRequest, 'Nothing to presolve') return obs.presolve(None, aliases, this.volume.blobs.path('packages')) diff --git a/sugar_network/node/model.py b/sugar_network/node/model.py index 8f9819b..b1cb401 100644 --- a/sugar_network/node/model.py +++ b/sugar_network/node/model.py @@ -16,10 +16,10 @@ import bisect import hashlib import logging +from os.path import join -from sugar_network import db +from sugar_network import db, toolkit from sugar_network.model import Release, context as _context, user as _user - from sugar_network.node import obs from sugar_network.toolkit.router import ACL from sugar_network.toolkit.coroutine import this @@ -33,8 +33,7 @@ _presolve_queue = None class User(_user.User): def created(self): - with file(this.volume.blobs.get(self['pubkey']).path) as f: - self.posts['guid'] = str(hashlib.sha1(f.read()).hexdigest()) + self.posts['guid'] = str(hashlib.sha1(self['pubkey']).hexdigest()) class _Release(Release): @@ -107,6 +106,34 @@ class Context(_context.Context): def releases(self, value): return value + def created(self): + _context.Context.created(self) + self._invalidate_solutions() + + def updated(self): + _context.Context.updated(self) + self._invalidate_solutions() + + def _invalidate_solutions(self): + if self['releases'] and \ + [i for i in ('state', 'releases', 'dependencies') + if i in self.posts and self.posts[i] != self.orig(i)]: + this.broadcast({ + 'event': 'release', + 'seqno': this.volume.release_seqno.next(), + }) + + +class Volume(db.Volume): + + def __init__(self, root, resources, **kwargs): + db.Volume.__init__(self, root, resources, **kwargs) + self.release_seqno = toolkit.Seqno(join(root, 'var', 'seqno-release')) + + def close(self): + db.Volume.close(self) + self.release_seqno.commit() + def solve(volume, top_context, command=None, lsb_id=None, lsb_release=None, stability=None, requires=None): @@ -199,7 +226,7 @@ def solve(volume, top_context, command=None, lsb_id=None, lsb_release=None, blob = volume.blobs.get(digest) if blob is not None: release_info['size'] = blob.size - release_info['content-type'] = blob['content-type'] + release_info['content-type'] = blob.meta['content-type'] unpack_size = release['bundles']['*-*'].get('unpack_size') if unpack_size is not None: release_info['unpack_size'] = unpack_size diff --git a/sugar_network/node/obs.py b/sugar_network/node/obs.py index 796ea7c..0c68a6e 100644 --- a/sugar_network/node/obs.py +++ b/sugar_network/node/obs.py @@ -84,7 +84,7 @@ def presolve(repo_name, packages, dst_path): to_download.append((url, path)) files.setdefault(arch, []).append(binary) except Exception: - toolkit.exception(_logger, 'Failed to presolve %r on %s', + _logger.exception('Failed to presolve %r on %s', packages, repo['name']) continue diff --git a/sugar_network/node/routes.py b/sugar_network/node/routes.py index ea23297..4457b2f 100644 --- a/sugar_network/node/routes.py +++ b/sugar_network/node/routes.py @@ -13,51 +13,70 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os -import time import logging -import hashlib -from ConfigParser import ConfigParser -from os.path import join, exists +from os.path import join -from sugar_network import db, node +from sugar_network import db from sugar_network.model import FrontRoutes, load_bundle from sugar_network.node import model # pylint: disable-msg=W0611 -from sugar_network.toolkit.router import route, preroute, postroute, ACL, File -from sugar_network.toolkit.router import Unauthorized, Request, fallbackroute +from sugar_network.toolkit.router import route, postroute, ACL, File +from sugar_network.toolkit.router import Request, fallbackroute, preroute from sugar_network.toolkit.spec import parse_requires, parse_version from sugar_network.toolkit.bundle import Bundle from sugar_network.toolkit.coroutine import this -from sugar_network.toolkit import pylru, http, coroutine, exception, enforce +from sugar_network.toolkit import http, coroutine, enforce -_MAX_STAT_RECORDS = 100 -_AUTH_POOL_SIZE = 1024 - _logger = logging.getLogger('node.routes') class NodeRoutes(db.Routes, FrontRoutes): - def __init__(self, guid, **kwargs): + def __init__(self, guid, auth=None, **kwargs): db.Routes.__init__(self, **kwargs) FrontRoutes.__init__(self) self._guid = guid - self._auth_pool = pylru.lrucache(_AUTH_POOL_SIZE) - self._auth_config = None - self._auth_config_mtime = 0 + self._auth = auth @property def guid(self): return self._guid + @preroute + def preroute(self, op): + request = this.request + if request.principal: + this.principal = request.principal + elif op.acl & ACL.AUTH: + this.principal = self._auth.logon(request) + else: + this.principal = None + if op.acl & ACL.AUTHOR and request.guid: + if not this.principal: + this.principal = self._auth.logon(request) + allowed = this.principal.admin + if not allowed: + if request.resource == 'user': + allowed = (this.principal == request.guid) + else: + doc = self.volume[request.resource].get(request.guid) + allowed = this.principal in doc['author'] + enforce(allowed, http.Forbidden, 'Authors only') + if op.acl & ACL.SUPERUSER: + if not this.principal: + this.principal = self._auth.logon(request) + enforce(this.principal.admin, http.Forbidden, 'Superusers only') + @route('GET', cmd='whoami', mime_type='application/json') - def whoami(self, request, response): + def whoami(self): roles = [] - if self.authorize(request.principal, 'root'): + if this.principal and this.principal.admin: roles.append('root') - return {'roles': roles, 'guid': request.principal, 'route': 'direct'} + return {'roles': roles, + 'guid': this.principal, + 'route': 'direct', + } @route('GET', cmd='status', mime_type='application/json') def status(self): @@ -69,45 +88,45 @@ class NodeRoutes(db.Routes, FrontRoutes): } @route('POST', ['user'], mime_type='application/json') - def register(self, request): + def register(self): # To avoid authentication while registering new user - self.create(request) + self.create() @fallbackroute('GET', ['packages']) - def route_packages(self, request, response): + def route_packages(self): path = this.request.path if path and path[-1] == 'updates': result = [] last_modified = 0 - for blob in this.volume.blobs.diff( + for blob in self.volume.blobs.diff( [[this.request.if_modified_since + 1, None]], join(*path[:-1]), recursive=False): if '.' in blob.name: continue result.append(blob.name) last_modified = max(last_modified, blob.mtime) - response.content_type = 'application/json' + this.response.content_type = 'application/json' if last_modified: - response.last_modified = last_modified + this.response.last_modified = last_modified return result - blob = this.volume.blobs.get(join(*path)) + blob = self.volume.blobs.get(join(*path)) if isinstance(blob, File): return blob else: - response.content_type = 'application/json' + this.response.content_type = 'application/json' return [i.name for i in blob if '.' not in i.name] @route('POST', ['context'], cmd='submit', arguments={'initial': False}, mime_type='application/json', acl=ACL.AUTH) def submit_release(self, initial): - blob = this.volume.blobs.post( + blob = self.volume.blobs.post( this.request.content_stream, this.request.content_type) try: context, release = load_bundle(blob, initial=initial) except Exception: - this.volume.blobs.delete(blob.digest) + self.volume.blobs.delete(blob.digest) raise this.call(method='POST', path=['context', context, 'releases'], content_type='application/json', content=release) @@ -116,88 +135,16 @@ class NodeRoutes(db.Routes, FrontRoutes): @route('GET', ['context', None], cmd='solve', arguments={'requires': list, 'stability': list}, mime_type='application/json') - def solve(self, request): - solution = model.solve(self.volume, request.guid, **request) + def solve(self): + solution = model.solve(self.volume, this.request.guid, **this.request) enforce(solution is not None, 'Failed to solve') return solution @route('GET', ['context', None], cmd='resolve', arguments={'requires': list, 'stability': list}) - def resolve(self, request): - solution = self.solve(request) - return this.volume.blobs.get(solution[request.guid]['blob']) - - @preroute - def preroute(self, op, request, response): - if op.acl & ACL.AUTH and request.principal is None: - if not request.authorization: - enforce(self.authorize(None, 'user'), - Unauthorized, 'No credentials') - else: - if request.authorization not in self._auth_pool: - self.authenticate(request.authorization) - self._auth_pool[request.authorization] = True - enforce(not request.authorization.nonce or - request.authorization.nonce >= time.time(), - Unauthorized, 'Credentials expired') - request.principal = request.authorization.login + def resolve(self): + solution = self.solve() + return self.volume.blobs.get(solution[this.request.guid]['blob']) - if op.acl & ACL.AUTHOR and request.guid: - self._enforce_authority(request) - if op.acl & ACL.SUPERUSER: - enforce(self.authorize(request.principal, 'root'), http.Forbidden, - 'Operation is permitted only for superusers') - - def on_aggprop_update(self, request, prop, value): - if prop.acl & ACL.AUTHOR: - self._enforce_authority(request) - elif value is not None: - self._enforce_authority(request, value.get('author')) - - def authenticate(self, auth): - enforce(auth.scheme == 'sugar', http.BadRequest, - 'Unknown authentication scheme') - enforce(self.volume['user'][auth.login].exists, Unauthorized, - 'Principal does not exist') - - from M2Crypto import RSA - - pubkey = self.volume['user'][auth.login]['pubkey'] - key = RSA.load_pub_key(this.volume.blobs.get(pubkey).path) - data = hashlib.sha1('%s:%s' % (auth.login, auth.nonce)).digest() - enforce(key.verify(data, auth.signature.decode('hex')), - http.Forbidden, 'Bad credentials') - - def authorize(self, user, role): - if role == 'user' and user: - return True - - config_path = join(node.data_root.value, 'authorization.conf') - if exists(config_path): - mtime = os.stat(config_path).st_mtime - if mtime > self._auth_config_mtime: - self._auth_config_mtime = mtime - self._auth_config = ConfigParser() - self._auth_config.read(config_path) - if self._auth_config is None: - return False - - if not user: - user = 'anonymous' - if not self._auth_config.has_section(user): - user = 'DEFAULT' - if self._auth_config.has_option(user, role): - return self._auth_config.get(user, role).strip().lower() in \ - ('true', 'on', '1', 'allow') - - def _enforce_authority(self, request, author=None): - if request.resource == 'user': - allowed = (request.principal == request.guid) - else: - if author is None: - doc = self.volume[request.resource].get(request.guid) - author = doc['author'] - allowed = request.principal in author - enforce(allowed or self.authorize(request.principal, 'root'), - http.Forbidden, 'Operation is permitted only for authors') +this.principal = None diff --git a/sugar_network/node/slave.py b/sugar_network/node/slave.py index 76593e9..074ae79 100644 --- a/sugar_network/node/slave.py +++ b/sugar_network/node/slave.py @@ -41,9 +41,17 @@ _logger = logging.getLogger('node.slave') class SlaveRoutes(NodeRoutes): def __init__(self, volume, **kwargs): - self._creds = http.SugarAuth( - join(volume.root, 'etc', 'private', 'node')) - NodeRoutes.__init__(self, self._creds.login, volume=volume, **kwargs) + guid_path = join(volume.root, 'etc', 'node') + if exists(guid_path): + with file(guid_path) as f: + guid = f.read().strip() + else: + guid = toolkit.uuid() + if not exists(dirname(guid_path)): + os.makedirs(dirname(guid_path)) + with file(guid_path, 'w') as f: + f.write(guid) + NodeRoutes.__init__(self, guid, volume=volume, **kwargs) vardir = join(volume.root, 'var') self._push_r = toolkit.Bin(join(vardir, 'push.ranges'), [[1, None]]) self._pull_r = toolkit.Bin(join(vardir, 'pull.ranges'), [[1, None]]) diff --git a/sugar_network/toolkit/__init__.py b/sugar_network/toolkit/__init__.py index 70868c0..675c25f 100644 --- a/sugar_network/toolkit/__init__.py +++ b/sugar_network/toolkit/__init__.py @@ -79,41 +79,6 @@ def enforce(condition, error=None, *args): raise exception_class(error) -def exception(*args): - """Log about exception on low log level. - - That might be useful for non-critial exception. Input arguments are the - same as for `logging.exception` function. - - :param args: - optional arguments to pass to logging function; - the first argument might be a `logging.Logger` to use instead of - using direct `logging` calls - - """ - if args and isinstance(args[0], logging.Logger): - logger = args[0] - args = args[1:] - else: - logger = logging - - klass, error, tb = sys.exc_info() - - import traceback - tb = [i.rstrip() for i in traceback.format_exception(klass, error, tb)] - - error_message = str(error) or '%s exception' % type(error).__name__ - if args: - if len(args) == 1: - message = args[0] - else: - message = args[0] % args[1:] - error_message = '%s: %s' % (message, error_message) - - logger.error(error_message) - logger.debug('\n'.join(tb)) - - def ascii(value): if not isinstance(value, basestring): return str(value) @@ -159,15 +124,6 @@ def init_logging(debug_level=None, **kwargs): else: logging_level = 8 - def disable_logger(loggers): - for log_name in loggers: - logger = logging.getLogger(log_name) - logger.propagate = False - logger.addHandler(_NullHandler()) - - logging.Logger.trace = lambda self, message, *args, **kwargs: None - logging.Logger.heartbeat = lambda self, message, *args, **kwargs: None - if logging_level <= 8: logging.Logger.trace = lambda self, message, *args, **kwargs: \ self._log(9, message, args, **kwargs) @@ -176,18 +132,18 @@ def init_logging(debug_level=None, **kwargs): elif logging_level == 9: logging.Logger.trace = lambda self, message, *args, **kwargs: \ self._log(9, message, args, **kwargs) - disable_logger(['sugar_stats']) else: - disable_logger([ - 'requests.packages.urllib3.connectionpool', - 'requests.packages.urllib3.poolmanager', - 'requests.packages.urllib3.response', - 'requests.packages.urllib3', - 'inotify', - 'netlink', - 'sugar_stats', - '0install', - ]) + for log_name in ( + 'requests.packages.urllib3.connectionpool', + 'requests.packages.urllib3.poolmanager', + 'requests.packages.urllib3.response', + 'requests.packages.urllib3', + 'inotify', + 'netlink', + ): + logger = logging.getLogger(log_name) + logger.propagate = False + logger.addHandler(_NullHandler()) root_logger = logging.getLogger('') for i in root_logger.handlers: @@ -196,6 +152,24 @@ def init_logging(debug_level=None, **kwargs): format='%(asctime)s %(levelname)s %(name)s: %(message)s', **kwargs) + def exception(self, *args): + from traceback import format_exception + + klass, error, tb = sys.exc_info() + tb = [i.rstrip() for i in format_exception(klass, error, tb)] + error_message = str(error) or '%s exception' % type(error).__name__ + if args: + if len(args) == 1: + message = args[0] + else: + message = args[0] % args[1:] + error_message = '%s: %s' % (message, error_message) + + self.error(error_message) + self.debug('\n'.join(tb)) + + logging.Logger.exception = exception + def iter_file(*path): with file(join(*path), 'rb') as f: @@ -661,3 +635,7 @@ def _nb_read(stream): return '' finally: fcntl.fcntl(fd, fcntl.F_SETFL, orig_flags) + + +logging.Logger.trace = lambda self, message, *args, **kwargs: None +logging.Logger.heartbeat = lambda self, message, *args, **kwargs: None diff --git a/sugar_network/toolkit/gbus.py b/sugar_network/toolkit/gbus.py index e1b24eb..8b64bf5 100644 --- a/sugar_network/toolkit/gbus.py +++ b/sugar_network/toolkit/gbus.py @@ -19,7 +19,7 @@ import json import struct import logging -from sugar_network.toolkit import coroutine, exception +from sugar_network.toolkit import coroutine _logger = logging.getLogger('gbus') @@ -65,7 +65,7 @@ def pipe(op, *args, **kwargs): try: op(feedback, *args, **kwargs) except Exception: - exception('Failed to call %r(%r, %r)', op, args, kwargs) + _logger.exception('Failed to call %r(%r, %r)', op, args, kwargs) os.close(fd_w) _logger.trace('Pipe %s(%r, %r)', op, args, kwargs) diff --git a/sugar_network/toolkit/http.py b/sugar_network/toolkit/http.py index 9dd437e..0ebee86 100644 --- a/sugar_network/toolkit/http.py +++ b/sugar_network/toolkit/http.py @@ -13,13 +13,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os import sys import json import types -import hashlib import logging -from os.path import join, dirname, exists, expanduser, abspath +from os.path import join, dirname from sugar_network import toolkit from sugar_network.toolkit import i18n, enforce @@ -112,13 +110,12 @@ class Connection(object): _Session = None - def __init__(self, url='', auth=None, max_retries=0, **session_args): + def __init__(self, url='', creds=None, max_retries=0, **session_args): self.url = url - self.auth = auth + self.creds = creds self._max_retries = max_retries self._session_args = session_args self._session = None - self._nonce = None def __repr__(self): return '<Connection url=%s>' % self.url @@ -185,8 +182,8 @@ class Connection(object): f.close() return reply - def upload(self, path, data, **kwargs): - reply = self.request('POST', path, data, params=kwargs) + def upload(self, path_=None, data_=None, **kwargs): + reply = self.request('POST', path_, data_, params=kwargs) if reply.headers.get('Content-Type') == 'application/json': return json.loads(reply.content) else: @@ -206,13 +203,21 @@ class Connection(object): self._session.cookies.clear() try_ = 0 + challenge = None while True: try_ += 1 reply = self._session.request(method, path, data=data, headers=headers, params=params, **kwargs) if reply.status_code == Unauthorized.status_code: - enforce(self.auth is not None, Unauthorized, 'No credentials') - self._authenticate(reply.headers.get('www-authenticate')) + enforce(self.creds is not None, Unauthorized, 'No credentials') + challenge_ = reply.headers.get('www-authenticate') + if challenge and challenge == challenge_: + profile = self.creds.profile + enforce(profile, Unauthorized, 'No way to self-register') + _logger.info('Register on the server') + self.post(['user'], profile) + challenge = challenge_ + self._session.headers.update(self.creds.logon(challenge)) try_ = 0 elif reply.status_code == 200 or \ allowed and reply.status_code in allowed: @@ -319,90 +324,6 @@ class Connection(object): setattr(self._session, arg, value) self._session.stream = True - def _authenticate(self, challenge): - from urllib2 import parse_http_list, parse_keqv_list - - nonce = None - if challenge: - challenge = challenge.split(' ', 1)[-1] - nonce = parse_keqv_list(parse_http_list(challenge)).get('nonce') - - if self._nonce and nonce == self._nonce: - enforce(self.auth.profile(), Unauthorized, 'Bad credentials') - _logger.info('Register on the server') - self.post(['user'], self.auth.profile()) - - self._session.headers['authorization'] = self.auth(nonce) - self._nonce = nonce - - -class SugarAuth(object): - - def __init__(self, key_path, profile=None): - self._key_path = abspath(expanduser(key_path)) - self._profile = profile or {} - self._key = None - self._pubkey = None - self._login = None - - @property - def pubkey(self): - if self._pubkey is None: - self.ensure_key() - from M2Crypto.BIO import MemoryBuffer - buf = MemoryBuffer() - self._key.save_pub_key_bio(buf) - self._pubkey = buf.getvalue() - return self._pubkey - - @property - def login(self): - if self._login is None: - self._login = str(hashlib.sha1(self.pubkey).hexdigest()) - return self._login - - def profile(self): - if 'name' not in self._profile: - self._profile['name'] = self.login - self._profile['pubkey'] = self.pubkey - return self._profile - - def __call__(self, nonce): - self.ensure_key() - data = hashlib.sha1('%s:%s' % (self.login, nonce)).digest() - signature = self._key.sign(data).encode('hex') - return 'Sugar username="%s",nonce="%s",signature="%s"' % \ - (self.login, nonce, signature) - - def ensure_key(self): - from M2Crypto import RSA - from base64 import b64encode - - key_dir = dirname(self._key_path) - if exists(self._key_path): - if os.stat(key_dir).st_mode & 077: - os.chmod(key_dir, 0700) - self._key = RSA.load_key(self._key_path) - return - - if not exists(key_dir): - os.makedirs(key_dir) - os.chmod(key_dir, 0700) - - _logger.info('Generate RSA private key at %r', self._key_path) - self._key = RSA.gen_key(1024, 65537, lambda *args: None) - self._key.save_key(self._key_path, cipher=None) - os.chmod(self._key_path, 0600) - - pub_key_path = self._key_path + '.pub' - with file(pub_key_path, 'w') as f: - f.write('ssh-rsa %s %s@%s' % ( - b64encode('\x00\x00\x00\x07ssh-rsa%s%s' % self._key.pub()), - self.login, - os.uname()[1], - )) - _logger.info('Saved RSA public key at %r', pub_key_path) - class _Subscription(object): @@ -431,8 +352,9 @@ class _Subscription(object): except Exception: if try_ == 0: raise - toolkit.exception('Failed to read from %r subscription, ' - 'will resubscribe', self._client.url) + _logger.exception( + 'Failed to read from %r subscription, resubscribe', + self._client.url) self._content = None return _parse_event(line) diff --git a/sugar_network/toolkit/mountpoints.py b/sugar_network/toolkit/mountpoints.py index 28076d7..f8324fa 100644 --- a/sugar_network/toolkit/mountpoints.py +++ b/sugar_network/toolkit/mountpoints.py @@ -19,7 +19,7 @@ from os.path import join, exists from sugar_network.toolkit.inotify import Inotify, \ IN_DELETE_SELF, IN_CREATE, IN_DELETE, IN_MOVED_TO, IN_MOVED_FROM -from sugar_network.toolkit import coroutine, exception +from sugar_network.toolkit import coroutine _COMPLETE_MOUNT_TIMEOUT = 3 @@ -96,4 +96,4 @@ def _call(path, filename, cb): try: cb(path) except Exception: - exception(_logger, 'Cannot call %r for %r mount', cb, path) + _logger.exception('Cannot call %r for %r mount', cb, path) diff --git a/sugar_network/toolkit/parcel.py b/sugar_network/toolkit/parcel.py index f09bdb5..9d583cd 100644 --- a/sugar_network/toolkit/parcel.py +++ b/sugar_network/toolkit/parcel.py @@ -99,7 +99,10 @@ def encode(packets, limit=None, header=None, compresslevel=None, blob_len = 0 if isinstance(record, File): blob_len = record.size - chunk = ostream.write_record(record, + chunk = record.meta + else: + chunk = record + chunk = ostream.write_record(chunk, None if finalizing else limit - blob_len) if chunk is None: _logger.debug('Reach the encoding limit') diff --git a/sugar_network/toolkit/router.py b/sugar_network/toolkit/router.py index 8eb84da..e9e91fd 100644 --- a/sugar_network/toolkit/router.py +++ b/sugar_network/toolkit/router.py @@ -16,7 +16,6 @@ import os import cgi import json -import time import types import logging import calendar @@ -33,7 +32,6 @@ from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import i18n, http, coroutine, enforce -_SIGNATURE_LIFETIME = 600 _NOT_SET = object() _logger = logging.getLogger('router') @@ -106,15 +104,6 @@ class ACL(object): } -class Unauthorized(http.Unauthorized): - - def __init__(self, message, nonce=None): - http.Unauthorized.__init__(self, message) - if not nonce: - nonce = int(time.time()) + _SIGNATURE_LIFETIME - self.headers = {'www-authenticate': 'Sugar nonce="%s"' % nonce} - - class Request(dict): def __init__(self, environ=None, method=None, path=None, cmd=None, @@ -133,7 +122,6 @@ class Request(dict): self._accept_language = _NOT_SET self._content_stream = content_stream or _NOT_SET self._content_type = content_type or _NOT_SET - self._authorization = _NOT_SET if environ: url = environ.get('PATH_INFO', '').strip('/') @@ -299,28 +287,6 @@ class Request(dict): self._dirty_query = False return self.environ.get('QUERY_STRING') - @property - def authorization(self): - if self._authorization is _NOT_SET: - auth = self.environ.get('HTTP_AUTHORIZATION') - if not auth: - self._authorization = None - else: - auth = self._authorization = _Authorization(auth) - auth.scheme, creds = auth.strip().split(' ', 1) - auth.scheme = auth.scheme.lower() - if auth.scheme == 'basic': - auth.login, auth.password = b64decode(creds).split(':') - elif auth.scheme == 'sugar': - from urllib2 import parse_http_list, parse_keqv_list - creds = parse_keqv_list(parse_http_list(creds)) - auth.login = creds['username'] - auth.signature = creds['signature'] - auth.nonce = int(creds['nonce']) - else: - raise http.BadRequest('Unsupported authentication scheme') - return self._authorization - def add(self, key, *values): existing_value = self.get(key) for value in values: @@ -418,18 +384,29 @@ class Response(CaseInsensitiveDict): return '<Response %r>' % items -class File(CaseInsensitiveDict): +class File(str): AWAY = None class Digest(str): pass - def __init__(self, path, digest=None, meta=None): - CaseInsensitiveDict.__init__(self, meta or []) + def __new__(cls, path=None, digest=None, meta=None): + meta = CaseInsensitiveDict(meta or []) + + url = '' + if meta: + url = meta.get('location') + if not url and digest: + url = '%s/blobs/%s' % (this.request.static_prefix, digest) + self = str.__new__(cls, url) + + self.meta = meta self.path = path self.digest = File.Digest(digest) if digest else None - self._stat = None + self.stat = None + + return self @property def exists(self): @@ -437,47 +414,37 @@ class File(CaseInsensitiveDict): @property def size(self): - if self._stat is None: + if self.stat is None: if not self.exists: - size = self.get('content-length', 0) + size = self.meta.get('content-length', 0) return int(size) if size else 0 - self._stat = os.stat(self.path) - return self._stat.st_size + self.stat = os.stat(self.path) + return self.stat.st_size @property def mtime(self): - if self._stat is None: - self._stat = os.stat(self.path) - return int(self._stat.st_mtime) - - @property - def url(self): - if self is File.AWAY: - return '' - return self.get('location') or \ - '%s/blobs/%s' % (this.request.static_prefix, self.digest) + if self.stat is None: + self.stat = os.stat(self.path) + return int(self.stat.st_mtime) @property def name(self): if self.path: return basename(self.path) - def __repr__(self): - return '<File %r>' % self.url - def iter_content(self): if self.path: return self._iter_content() - url = self.get('location') + url = self.meta.get('location') enforce(url, http.NotFound, 'No location') blob = this.http.request('GET', url, allow_redirects=True, # Request for uncompressed data headers={'accept-encoding': ''}) - self.clear() + self.meta.clear() for tag in ('content-length', 'content-type', 'content-disposition'): value = blob.headers.get(tag) if value: - self[tag] = value + self.meta[tag] = value return blob.iter_content(toolkit.BUFFER_SIZE) def _iter_content(self): @@ -544,8 +511,7 @@ class Router(object): this.call = self.call - def call(self, request=None, response=None, environ=None, principal=None, - **kwargs): + def call(self, request=None, response=None, environ=None, **kwargs): if request is None: if this.request is not None: if not environ: @@ -558,9 +524,7 @@ class Router(object): ): if key in this.request.environ: environ[key] = this.request.environ[key] - if not principal: - principal = this.request.principal - request = Request(environ=environ, principal=principal, **kwargs) + request = Request(environ=environ, **kwargs) if response is None: response = Response() @@ -583,15 +547,10 @@ class Router(object): 'Cannot typecast %r argument: %s' % (arg, error)) kwargs = {} for arg in route_.kwarg_names: - if arg == 'request': - kwargs[arg] = request - elif arg == 'response': - kwargs[arg] = response - elif arg not in kwargs: - kwargs[arg] = request.get(arg) + kwargs[arg] = request.get(arg) for i in self._preroutes: - i(route_, request, response) + i(route_) result = None exception = None try: @@ -609,7 +568,7 @@ class Router(object): raise finally: for i in self._postroutes: - i(request, response, result, exception) + i(result, exception) return result @@ -638,9 +597,10 @@ class Router(object): result = self.call(request, response) if isinstance(result, File): - response.update(result) - if 'location' in result: - raise http.Redirect(result['location']) + enforce(result is not File.AWAY, http.NotFound, 'No such file') + response.update(result.meta) + if 'location' in result.meta: + raise http.Redirect(result.meta['location']) enforce(isfile(result.path), 'No such file') if request.if_modified_since and \ result.mtime <= request.if_modified_since: @@ -663,7 +623,7 @@ class Router(object): if error.headers: response.update(error.headers) except Exception, error: - toolkit.exception('Error while processing %r request', request.url) + _logger.exception('Error while processing %r request', request.url) if isinstance(error, http.Status): response.status = error.status response.update(error.headers or {}) @@ -946,7 +906,7 @@ class _Route(object): if hasattr(callback, 'func_code'): code = callback.func_code # `1:` is for skipping the first, `self` or `cls`, argument - self.kwarg_names = code.co_varnames[1:code.co_argcount] + self.kwarg_names = set(code.co_varnames[1:code.co_argcount]) def __repr__(self): path = '/'.join(['*' if i is None else i for i in self.path]) @@ -955,12 +915,4 @@ class _Route(object): return '%s /%s (%s)' % (self.method, path, self.callback.__name__) -class _Authorization(str): - scheme = None - login = None - password = None - signature = None - nonce = None - - File.AWAY = File(None) diff --git a/sugar_network/toolkit/spec.py b/sugar_network/toolkit/spec.py index bd852d4..b3f83e9 100644 --- a/sugar_network/toolkit/spec.py +++ b/sugar_network/toolkit/spec.py @@ -20,7 +20,7 @@ from os.path import join, exists, dirname from ConfigParser import ConfigParser from sugar_network.toolkit.licenses import GOOD_LICENSES -from sugar_network.toolkit import exception, enforce +from sugar_network.toolkit import enforce EMPTY_LICENSE = 'License is not specified' @@ -104,7 +104,6 @@ def parse_version(version_string, ignore_errors=False): else: parts[x] = [] # (because ''.split('.') == [''], not []) except ValueError as error: - exception() raise ValueError('Invalid version format in "%s": %s' % (version_string, error)) except KeyError as error: diff --git a/tests/__init__.py b/tests/__init__.py index 386616a..e1c3222 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -3,6 +3,7 @@ import os import sys import json +import gconf import signal import shutil import hashlib @@ -20,16 +21,19 @@ from sugar_network.toolkit import coroutine coroutine.inject() from sugar_network.toolkit import http, mountpoints, Option, gbus, i18n, languages, parcel -from sugar_network.toolkit.router import Router, Request +from sugar_network.toolkit.router import Router, Request, Response from sugar_network.toolkit.coroutine import this from sugar_network.client import IPCConnection, journal, routes as client_routes, model as client_model from sugar_network.client.injector import Injector -from sugar_network.client.routes import ClientRoutes, _Auth +from sugar_network.client.routes import ClientRoutes +from sugar_network.client.auth import SugarCreds from sugar_network import db, client, node, toolkit, model from sugar_network.model.user import User from sugar_network.model.context import Context from sugar_network.node.model import Context as MasterContext from sugar_network.node.model import User as MasterUser +from sugar_network.node.model import Volume as NodeVolume +from sugar_network.node.auth import SugarAuth from sugar_network.model.post import Post from sugar_network.node.master import MasterRoutes from sugar_network.node import obs, slave, master @@ -116,11 +120,15 @@ class Test(unittest.TestCase): 'sugar_network.model.report', ] - if tmp_root is None: - self.override(_Auth, 'profile', lambda self: { - 'name': 'test', - 'pubkey': PUBKEY, - }) + class GConf(object): + + def get_string(self, key): + if key == '/desktop/sugar/user/nick': + return 'test' + else: + return key + + self.override(gconf, 'client_get_default', lambda: GConf()) os.makedirs('tmp') @@ -134,6 +142,7 @@ class Test(unittest.TestCase): this.call = None this.broadcast = lambda x: x this.injector = None + this.principal = None def tearDown(self): self.stop_nodes() @@ -268,8 +277,8 @@ class Test(unittest.TestCase): if classes is None: classes = master.RESOURCES #self.touch(('master/etc/private/node', file(join(root, 'data', NODE_UID)).read())) - self.node_volume = db.Volume('master', classes) - self.node_routes = routes(volume=self.node_volume) + self.node_volume = NodeVolume('master', classes) + self.node_routes = routes(volume=self.node_volume, auth=SugarAuth('master')) self.node_router = Router(self.node_routes) self.node = coroutine.WSGIServer(('127.0.0.1', 7777), self.node_router) coroutine.spawn(self.node.serve_forever) @@ -283,19 +292,17 @@ class Test(unittest.TestCase): classes = master.RESOURCES def node(): - volume = db.Volume('master', classes) - node = coroutine.WSGIServer(('127.0.0.1', 7777), Router(routes(volume=volume))) + volume = NodeVolume('master', classes) + node = coroutine.WSGIServer(('127.0.0.1', 7777), Router(routes(volume=volume, auth=SugarAuth('master')))) node.serve_forever() pid = self.fork(node) coroutine.sleep(.1) return pid - def start_client(self, classes=None, routes=None): - if routes is None: - routes = ClientRoutes - volume = db.Volume('client', classes or client_model.RESOURCES) - self.client_routes = routes(volume) + def start_client(self): + volume = client_model.Volume('client') + self.client_routes = ClientRoutes(volume, SugarCreds(client.keyfile.value)) self.client_routes.connect(client.api.value) self.client = coroutine.WSGIServer( ('127.0.0.1', client.ipc_port.value), Router(self.client_routes)) @@ -307,8 +314,11 @@ class Test(unittest.TestCase): def start_online_client(self, classes=None): self.fork_master(classes) this.injector = Injector('client/cache') - home_volume = db.Volume('client', classes or client_model.RESOURCES) - self.client_routes = ClientRoutes(home_volume) + if classes: + home_volume = db.Volume('client', classes) + else: + home_volume = client_model.Volume('client') + self.client_routes = ClientRoutes(home_volume, SugarCreds(client.keyfile.value)) self.client_routes.connect(client.api.value) self.wait_for_events(self.client_routes, event='inline', state='online').wait() self.client = coroutine.WSGIServer( @@ -318,10 +328,10 @@ class Test(unittest.TestCase): this.volume = home_volume return home_volume - def start_offline_client(self, resources=None): + def start_offline_client(self): this.injector = Injector('client/cache') - home_volume = db.Volume('client', resources or client_model.RESOURCES) - self.client_routes = ClientRoutes(home_volume) + home_volume = client_model.Volume('client') + self.client_routes = ClientRoutes(home_volume, SugarCreds(client.keyfile.value)) server = coroutine.WSGIServer(('127.0.0.1', client.ipc_port.value), Router(self.client_routes)) coroutine.spawn(server.serve_forever) coroutine.dispatch() @@ -336,6 +346,7 @@ class Test(unittest.TestCase): trigger = coroutine.AsyncResult() def waiter(trigger): + this.response = Response() for event in cp.subscribe(): if isinstance(event, basestring) and event.startswith('data: '): event = json.loads(event[6:]) diff --git a/tests/units/client/injector.py b/tests/units/client/injector.py index ec88975..7170758 100755 --- a/tests/units/client/injector.py +++ b/tests/units/client/injector.py @@ -14,6 +14,7 @@ from __init__ import tests from sugar_network import db, client from sugar_network.client import Connection, keyfile, api, packagekit, injector as injector_, model from sugar_network.client.injector import _PreemptivePool, Injector +from sugar_network.client.auth import SugarCreds from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http, lsb_release @@ -349,7 +350,7 @@ class InjectorTest(tests.Test): def test_solve(self): self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -382,7 +383,7 @@ class InjectorTest(tests.Test): def test_solve_FailInOffline(self): self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = None injector.seqno = 0 @@ -403,7 +404,7 @@ class InjectorTest(tests.Test): def test_solve_ReuseCachedSolution(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -426,7 +427,7 @@ class InjectorTest(tests.Test): def test_solve_InvalidateCachedSolution(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = 'http://127.0.0.1:7777' injector.seqno = 1 @@ -492,7 +493,7 @@ class InjectorTest(tests.Test): def test_solve_ForceUsingStaleCachedSolutionInOffline(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -519,7 +520,7 @@ class InjectorTest(tests.Test): def test_download_SetExecPermissions(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -552,7 +553,7 @@ class InjectorTest(tests.Test): def test_checkin(self): self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -603,7 +604,7 @@ class InjectorTest(tests.Test): def test_checkin_PreemptivePool(self): self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -660,7 +661,7 @@ class InjectorTest(tests.Test): def test_checkin_Refresh(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -694,7 +695,7 @@ class InjectorTest(tests.Test): def test_launch(self): self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -742,7 +743,7 @@ class InjectorTest(tests.Test): def test_launch_PreemptivePool(self): self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -783,7 +784,7 @@ class InjectorTest(tests.Test): def test_launch_DonntAcquireCheckins(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector('client') injector.api = client.api.value injector.seqno = 0 @@ -810,7 +811,7 @@ class InjectorTest(tests.Test): def test_launch_RefreshCheckins(self): self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector(tests.tmpdir + '/client') injector.api = client.api.value injector.seqno = 1 @@ -862,7 +863,7 @@ class InjectorTest(tests.Test): def test_launch_InstallDeps(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector(tests.tmpdir + '/client') injector.api = client.api.value injector.seqno = 1 @@ -902,7 +903,7 @@ class InjectorTest(tests.Test): def test_launch_Document(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector(tests.tmpdir + '/client') injector.api = client.api.value injector.seqno = 1 @@ -936,7 +937,7 @@ class InjectorTest(tests.Test): def test_launch_DocumentWithDetectingAppByMIMEType(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) injector = Injector(tests.tmpdir + '/client') injector.api = client.api.value injector.seqno = 1 diff --git a/tests/units/client/journal.py b/tests/units/client/journal.py index bae636b..f421237 100755 --- a/tests/units/client/journal.py +++ b/tests/units/client/journal.py @@ -12,6 +12,7 @@ from __init__ import tests from sugar_network import db from sugar_network.client import journal, ipc_port +from sugar_network.toolkit.coroutine import this from sugar_network.toolkit.router import Request, Response @@ -103,38 +104,38 @@ class JournalTest(tests.Test): ds.journal_update('guid2', StringIO('data2'), title='title2', description='description2', preview=StringIO('preview2')) ds.journal_update('guid3', StringIO('data3'), title='title3', description='description3', preview=StringIO('preview3')) - request = Request(reply=['uid', 'title', 'description', 'preview']) - request.path = ['journal'] - response = Response() + this.request = Request(reply=['uid', 'title', 'description', 'preview']) + this.request.path = ['journal'] + this.response = Response() self.assertEqual([ {'guid': 'guid1', 'title': 'title1', 'description': 'description1', 'preview': {'url': url + 'guid1/preview'}}, {'guid': 'guid2', 'title': 'title2', 'description': 'description2', 'preview': {'url': url + 'guid2/preview'}}, {'guid': 'guid3', 'title': 'title3', 'description': 'description3', 'preview': {'url': url + 'guid3/preview'}}, ], - ds.journal_find(request, response)['result']) + ds.journal_find()['result']) - request = Request(offset=1, limit=1, reply=['uid', 'title', 'description', 'preview']) - request.path = ['journal'] + this.request = Request(offset=1, limit=1, reply=['uid', 'title', 'description', 'preview']) + this.request.path = ['journal'] self.assertEqual([ {'guid': 'guid2', 'title': 'title2', 'description': 'description2', 'preview': {'url': url + 'guid2/preview'}}, ], - ds.journal_find(request, response)['result']) + ds.journal_find()['result']) - request = Request(query='title3', reply=['uid', 'title', 'description', 'preview']) - request.path = ['journal'] + this.request = Request(query='title3', reply=['uid', 'title', 'description', 'preview']) + this.request.path = ['journal'] self.assertEqual([ {'guid': 'guid3', 'title': 'title3', 'description': 'description3', 'preview': {'url': url + 'guid3/preview'}}, ], - ds.journal_find(request, response)['result']) + ds.journal_find()['result']) - request = Request(order_by=['+title'], reply=['uid', 'title', 'description', 'preview']) - request.path = ['journal'] + this.request = Request(order_by=['+title'], reply=['uid', 'title', 'description', 'preview']) + this.request.path = ['journal'] self.assertEqual([ {'guid': 'guid3', 'title': 'title3', 'description': 'description3', 'preview': {'url': url + 'guid3/preview'}}, {'guid': 'guid2', 'title': 'title2', 'description': 'description2', 'preview': {'url': url + 'guid2/preview'}}, {'guid': 'guid1', 'title': 'title1', 'description': 'description1', 'preview': {'url': url + 'guid1/preview'}}, ], - ds.journal_find(request, response)['result']) + ds.journal_find()['result']) def test_GetRequest(self): url = 'http://127.0.0.1:%s/journal/' % ipc_port.value @@ -142,34 +143,34 @@ class JournalTest(tests.Test): ds = journal.Routes() ds.journal_update('guid1', StringIO('data1'), title='title1', description='description1', preview=StringIO('preview1')) - request = Request() - request.path = ['journal', 'guid1'] - response = Response() + this.request = Request() + this.request.path = ['journal', 'guid1'] + this.response = Response() self.assertEqual( {'guid': 'guid1', 'title': 'title1', 'description': 'description1', 'preview': {'url': url + 'guid1/preview'}}, - ds.journal_get(request, response)) + ds.journal_get()) def test_GetPropRequest(self): ds = journal.Routes() ds.journal_update('guid1', StringIO('data1'), title='title1', description='description1', preview=StringIO('preview1')) - request = Request() - request.path = ['journal', 'guid1', 'title'] - response = Response() - self.assertEqual('title1', ds.journal_get_prop(request, response)) + this.request = Request() + this.request.path = ['journal', 'guid1', 'title'] + this.response = Response() + self.assertEqual('title1', ds.journal_get_prop()) - request = Request() - request.path = ['journal', 'guid1', 'preview'] - response = Response() - blob = ds.journal_get_preview(request, response) + this.request = Request() + this.request.path = ['journal', 'guid1', 'preview'] + this.response = Response() + blob = ds.journal_get_preview() self.assertEqual({ 'content-type': 'image/png', }, - dict(blob)) + blob.meta) self.assertEqual( '.sugar/default/datastore/gu/guid1/metadata/preview', blob.path) - self.assertEqual(None, response.content_type) + self.assertEqual(None, this.response.content_type) if __name__ == '__main__': diff --git a/tests/units/client/routes.py b/tests/units/client/routes.py index 325ac99..6072571 100755 --- a/tests/units/client/routes.py +++ b/tests/units/client/routes.py @@ -13,9 +13,10 @@ from __init__ import tests from sugar_network import db, client, toolkit from sugar_network.client import journal, IPCConnection, cache_limit, cache_lifetime, api, injector, routes -from sugar_network.client.model import RESOURCES +from sugar_network.client.model import Volume from sugar_network.client.injector import Injector -from sugar_network.client.routes import ClientRoutes, CachedClientRoutes +from sugar_network.client.routes import ClientRoutes +from sugar_network.client.auth import SugarCreds from sugar_network.node.model import User from sugar_network.node.master import MasterRoutes from sugar_network.toolkit.router import Router, Request, Response, route @@ -28,8 +29,8 @@ import requests class RoutesTest(tests.Test): def test_Hub(self): - volume = db.Volume('db', RESOURCES) - cp = ClientRoutes(volume) + volume = Volume('db') + cp = ClientRoutes(volume, SugarCreds(client.keyfile.value)) server = coroutine.WSGIServer( ('127.0.0.1', client.ipc_port.value), Router(cp)) coroutine.spawn(server.serve_forever) @@ -353,7 +354,6 @@ class RoutesTest(tests.Test): ipc = IPCConnection() guid1 = ipc.post(['context'], { - 'guid': 'context1', 'type': 'activity', 'title': '1', 'summary': 'summary', @@ -382,20 +382,19 @@ class RoutesTest(tests.Test): ]]), cmd='submit', initial=True) guid3 = 'context3' guid4 = ipc.post(['context'], { - 'guid': 'context4', 'type': 'activity', 'title': '4', 'summary': 'summary', 'description': 'description', }) - self.assertEqual([ + self.assertEqual(sorted([ {'guid': guid1, 'title': '1', 'pins': []}, {'guid': guid2, 'title': '2', 'pins': []}, {'guid': guid3, 'title': '3', 'pins': []}, {'guid': guid4, 'title': '4', 'pins': []}, - ], - ipc.get(['context'], reply=['guid', 'title', 'pins'])['result']) + ]), + sorted(ipc.get(['context'], reply=['guid', 'title', 'pins'])['result'])) self.assertEqual([ ], ipc.get(['context'], reply=['guid', 'title'], pins='favorite')['result']) @@ -421,13 +420,13 @@ class RoutesTest(tests.Test): home_volume['context'].update(guid2, {'title': {i18n.default_lang(): '2_'}}) home_volume['context'].update(guid3, {'title': {i18n.default_lang(): '3_'}}) - self.assertEqual([ + self.assertEqual(sorted([ {'guid': guid1, 'title': '1', 'pins': ['favorite']}, {'guid': guid2, 'title': '2', 'pins': ['checkin', 'favorite']}, {'guid': guid3, 'title': '3', 'pins': ['checkin']}, {'guid': guid4, 'title': '4', 'pins': []}, - ], - ipc.get(['context'], reply=['guid', 'title', 'pins'])['result']) + ]), + sorted(ipc.get(['context'], reply=['guid', 'title', 'pins'])['result'])) self.assertEqual([ {'guid': guid1, 'title': '1_'}, {'guid': guid2, 'title': '2_'}, @@ -442,13 +441,13 @@ class RoutesTest(tests.Test): ipc.delete(['context', guid1], cmd='favorite') ipc.delete(['context', guid2], cmd='checkin') - self.assertEqual([ + self.assertEqual(sorted([ {'guid': guid1, 'pins': []}, {'guid': guid2, 'pins': ['favorite']}, {'guid': guid3, 'pins': ['checkin']}, {'guid': guid4, 'pins': []}, - ], - ipc.get(['context'], reply=['guid', 'pins'])['result']) + ]), + sorted(ipc.get(['context'], reply=['guid', 'pins'])['result'])) self.assertEqual([ {'guid': guid2, 'pins': ['favorite']}, ], @@ -873,7 +872,7 @@ class RoutesTest(tests.Test): 'content2', 'content3', ]), - sorted([ipc.get(['report', guid, 'logs', i]) for i in ipc.get(['report', guid, 'logs']).keys()])) + sorted([''.join(ipc.download(i[1])) for i in ipc.get(['report', guid, 'logs'])])) assert not home_volume['report'][guid].exists self.stop_master() @@ -897,14 +896,14 @@ class RoutesTest(tests.Test): 'content2', 'content3', ]), - sorted([ipc.get(['report', guid, 'logs', i]) for i in ipc.get(['report', guid, 'logs']).keys()])) + sorted([''.join(ipc.download(i[1])) for i in ipc.get(['report', guid, 'logs'])])) assert home_volume['report'][guid].exists def test_inline(self): routes._RECONNECT_TIMEOUT = 2 this.injector = Injector('client') - cp = ClientRoutes(db.Volume('client', RESOURCES)) + cp = ClientRoutes(Volume('client'), SugarCreds(client.keyfile.value)) cp.connect(client.api.value) assert not cp.inline() @@ -1064,7 +1063,7 @@ class RoutesTest(tests.Test): subscribe_tries = 0 - def __init__(self, volume, *args): + def __init__(self, volume, auth, *args): pass @route('GET', cmd='status', mime_type='application/json') diff --git a/tests/units/db/blobs.py b/tests/units/db/blobs.py index cee8667..9672b39 100755 --- a/tests/units/db/blobs.py +++ b/tests/units/db/blobs.py @@ -35,7 +35,7 @@ class BlobsTest(tests.Test): 'content-length': str(len(content)), 'x-seqno': '1', }, - blob) + blob.meta) self.assertEqual( content, @@ -70,7 +70,7 @@ class BlobsTest(tests.Test): 'content-length': str(len(content)), 'x-seqno': '1', }, - blob) + blob.meta) self.assertEqual( content, @@ -108,7 +108,7 @@ class BlobsTest(tests.Test): 'content-length': '0', 'x-seqno': '1', }, - blob) + blob.meta) self.assertEqual( '', @@ -138,7 +138,7 @@ class BlobsTest(tests.Test): 'content-length': str(len('probe')), 'x-seqno': '1', }, - blob) + blob.meta) blobs.update(blob.digest, {'foo': 'bar'}) self.assertEqual({ @@ -147,7 +147,7 @@ class BlobsTest(tests.Test): 'x-seqno': '1', 'foo': 'bar', }, - blobs.get(blob.digest)) + blobs.get(blob.digest).meta) def test_delete(self): blobs = Blobs('.', Seqno()) @@ -160,7 +160,7 @@ class BlobsTest(tests.Test): 'content-type': 'application/octet-stream', 'x-seqno': '1', }, - dict(blobs.get(blob.digest))) + blobs.get(blob.digest).meta) blobs.delete(blob.digest) assert not exists(blob.path) @@ -171,7 +171,7 @@ class BlobsTest(tests.Test): 'status': '410 Gone', 'x-seqno': '2', }, - dict(blobs.get(blob.digest))) + blobs.get(blob.digest).meta) def test_diff_Blobs(self): blobs = Blobs('.', Seqno()) @@ -195,10 +195,10 @@ class BlobsTest(tests.Test): ('1000000000000000000000000000000000000002', {'n': '2', 'x-seqno': '2'}), ('1000000000000000000000000000000000000001', {'n': '1', 'x-seqno': '1'}), ], - [(i.digest, dict(i)) for i in blobs.diff([[1, None]])]) + [(i.digest, i.meta) for i in blobs.diff([[1, None]])]) self.assertEqual([ ], - [(i.digest, dict(i)) for i in blobs.diff([[4, None]])]) + [(i.digest, i.meta) for i in blobs.diff([[4, None]])]) self.touch('blobs/200/2000000000000000000000000000000000000004', ('blobs/200/2000000000000000000000000000000000000004.meta', 'n: 4\nx-seqno: 4')) @@ -213,7 +213,7 @@ class BlobsTest(tests.Test): ('3000000000000000000000000000000000000005', {'n': '5', 'x-seqno': '5'}), ('2000000000000000000000000000000000000004', {'n': '4', 'x-seqno': '4'}), ], - [(i.digest, dict(i)) for i in blobs.diff([[4, None]])]) + [(i.digest, i.meta) for i in blobs.diff([[4, None]])]) self.assertEqual([ ], [i for i in blobs.diff([[6, None]])]) @@ -225,7 +225,7 @@ class BlobsTest(tests.Test): ('1000000000000000000000000000000000000002', {'n': '2', 'x-seqno': '2'}), ('1000000000000000000000000000000000000001', {'n': '1', 'x-seqno': '1'}), ], - [(i.digest, dict(i)) for i in blobs.diff([[1, None]])]) + [(i.digest, i.meta) for i in blobs.diff([[1, None]])]) def test_diff_Files(self): blobs = Blobs('.', Seqno()) @@ -254,31 +254,31 @@ class BlobsTest(tests.Test): self.assertEqual(sorted([ ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]])])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]])])) self.assertEqual(sorted([ ('1', {'n': '1', 'path': '1', 'x-seqno': '1'}), ('2/3', {'n': '2', 'path': '2/3', 'x-seqno': '2'}), ('2/4/5', {'n': '3', 'path': '2/4/5', 'x-seqno': '3'}), ('6', {'n': '4', 'path': '6', 'x-seqno': '4'}), ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], '')])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], '')])) self.assertEqual(sorted([ ('2/3', {'n': '2', 'path': '2/3', 'x-seqno': '2'}), ('2/4/5', {'n': '3', 'path': '2/4/5', 'x-seqno': '3'}), ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], '2')])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], '2')])) self.assertEqual(sorted([ ('2/4/5', {'n': '3', 'path': '2/4/5', 'x-seqno': '3'}), ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], '2/4')])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], '2/4')])) self.assertEqual(sorted([ ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], 'foo')])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], 'foo')])) self.assertEqual(sorted([ ('1', {'n': '1', 'path': '1', 'x-seqno': '1'}), ('6', {'n': '4', 'path': '6', 'x-seqno': '4'}), ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], '', False)])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], '', False)])) def test_diff_FailOnRelativePaths(self): blobs = Blobs('.', Seqno()) @@ -306,7 +306,7 @@ class BlobsTest(tests.Test): ('2/4/5.svg', {'content-type': 'image/svg+xml', 'content-length': '3', 'x-seqno': '1', 'path': '2/4/5.svg'}), ('6.png', {'content-type': 'image/png', 'content-length': '4', 'x-seqno': '1', 'path': '6.png'}), ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], '')])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], '')])) self.assertEqual(1, blobs._seqno.value) self.assertEqual(sorted([ @@ -315,7 +315,7 @@ class BlobsTest(tests.Test): ('2/4/5.svg', {'content-type': 'image/svg+xml', 'content-length': '3', 'x-seqno': '1', 'path': '2/4/5.svg'}), ('6.png', {'content-type': 'image/png', 'content-length': '4', 'x-seqno': '1', 'path': '6.png'}), ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], '')])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], '')])) self.assertEqual(1, blobs._seqno.value) def test_diff_HandleUpdates(self): @@ -335,23 +335,23 @@ class BlobsTest(tests.Test): self.assertEqual([ ('0000000000000000000000000000000000000001', {'n': '1', 'content-length': '50', 'x-seqno': '11'}), ], - [(i.digest, dict(i)) for i in blobs.diff([[1, None]])]) + [(i.digest, i.meta) for i in blobs.diff([[1, None]])]) self.assertEqual(11, blobs._seqno.value) self.assertEqual([ ('0000000000000000000000000000000000000001', {'n': '1', 'content-length': '50', 'x-seqno': '11'}), ], - [(i.digest, dict(i)) for i in blobs.diff([[1, None]])]) + [(i.digest, i.meta) for i in blobs.diff([[1, None]])]) self.assertEqual(11, blobs._seqno.value) self.assertEqual(sorted([ ('2', {'n': '2', 'path': '2', 'content-length': '7', 'x-seqno': '12'}), ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], '')])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], '')])) self.assertEqual(12, blobs._seqno.value) self.assertEqual(sorted([ ('2', {'n': '2', 'path': '2', 'content-length': '7', 'x-seqno': '12'}), ]), - sorted([(i.digest, dict(i)) for i in blobs.diff([[1, None]], '')])) + sorted([(i.digest, i.meta) for i in blobs.diff([[1, None]], '')])) self.assertEqual(12, blobs._seqno.value) def test_patch_Blob(self): @@ -363,7 +363,7 @@ class BlobsTest(tests.Test): self.assertEqual(tests.tmpdir + '/blobs/000/0000000000000000000000000000000000000001', blob.path) self.assertEqual('0000000000000000000000000000000000000001', blob.digest) self.assertEqual('1', file(blob.path).read()) - self.assertEqual({'x-seqno': '-1', 'n': '1'}, blob) + self.assertEqual({'x-seqno': '-1', 'n': '1'}, blob.meta) assert not exists('blob') blobs.patch(File('./fake', '0000000000000000000000000000000000000002', {'n': 2, 'content-length': '0'}), -2) @@ -372,7 +372,7 @@ class BlobsTest(tests.Test): blobs.patch(File('./fake', '0000000000000000000000000000000000000001', {'n': 3, 'content-length': '0'}), -3) blob = blobs.get('0000000000000000000000000000000000000001') assert not exists(blob.path) - self.assertEqual({'x-seqno': '-3', 'n': '1', 'status': '410 Gone'}, dict(blob)) + self.assertEqual({'x-seqno': '-3', 'n': '1', 'status': '410 Gone'}, blob.meta) def test_patch_File(self): blobs = Blobs('.', Seqno()) @@ -381,7 +381,7 @@ class BlobsTest(tests.Test): blobs.patch(File('./file', '1', {'n': 1, 'path': 'foo/bar'}), -1) blob = blobs.get('foo/bar') self.assertEqual('1', file(blob.path).read()) - self.assertEqual({'x-seqno': '-1', 'n': '1'}, blob) + self.assertEqual({'x-seqno': '-1', 'n': '1'}, blob.meta) assert not exists('file') blobs.patch(File('./fake', 'bar/foo', {'n': 2, 'content-length': '0'}), -2) @@ -390,7 +390,7 @@ class BlobsTest(tests.Test): blobs.patch(File('./fake', 'foo/bar', {'n': 3, 'content-length': '0', 'path': 'foo/bar'}), -3) blob = blobs.get('foo/bar') assert not exists(blob.path) - self.assertEqual({'x-seqno': '-3', 'n': '1', 'status': '410 Gone'}, dict(blob)) + self.assertEqual({'x-seqno': '-3', 'n': '1', 'status': '410 Gone'}, blob.meta) class Seqno(object): diff --git a/tests/units/db/routes.py b/tests/units/db/routes.py index 4fa4cef..4189502 100755 --- a/tests/units/db/routes.py +++ b/tests/units/db/routes.py @@ -170,7 +170,7 @@ class RoutesTest(tests.Test): router = Router(db.Routes(volume)) guid = this.call(method='POST', path=['testdocument'], content={}) - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY this.call(method='PUT', path=['testdocument', guid, 'blob'], content='blob1') self.assertEqual('blob1', file(this.call(method='GET', path=['testdocument', guid, 'blob']).path).read()) @@ -179,7 +179,7 @@ class RoutesTest(tests.Test): self.assertEqual('blob2', file(this.call(method='GET', path=['testdocument', guid, 'blob']).path).read()) this.call(method='PUT', path=['testdocument', guid, 'blob'], content=None) - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY def test_CreateBLOBsWithMeta(self): @@ -195,16 +195,16 @@ class RoutesTest(tests.Test): self.assertRaises(http.BadRequest, this.call, method='PUT', path=['testdocument', guid, 'blob'], content={}, content_type='application/json') - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY self.assertRaises(http.BadRequest, this.call, method='PUT', path=['testdocument', guid, 'blob'], content={'location': 'foo'}, content_type='application/json') - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY self.assertRaises(http.BadRequest, this.call, method='PUT', path=['testdocument', guid, 'blob'], content={'location': 'url', 'digest': 'digest', 'foo': 'bar', 'content-type': 'foo/bar'}, content_type='application/json') - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY def test_UpdateBLOBsWithMeta(self): @@ -224,7 +224,7 @@ class RoutesTest(tests.Test): 'content-length': '4', 'x-seqno': '1', }, - dict(blob)) + blob.meta) self.assertEqual('blob', file(blob.path).read()) self.assertRaises(http.BadRequest, this.call, method='PUT', path=['testdocument', guid, 'blob'], @@ -235,7 +235,7 @@ class RoutesTest(tests.Test): 'content-length': '4', 'x-seqno': '1', }, - dict(blob)) + blob.meta) self.assertEqual('blob', file(blob.path).read()) def test_RemoveBLOBs(self): @@ -253,7 +253,7 @@ class RoutesTest(tests.Test): self.assertEqual('blob', file(this.call(method='GET', path=['testdocument', guid, 'blob']).path).read()) this.call(method='PUT', path=['testdocument', guid, 'blob']) - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY def test_ReuploadBLOBs(self): @@ -294,10 +294,10 @@ class RoutesTest(tests.Test): router = Router(db.Routes(volume)) guid = this.call(method='POST', path=['testdocument'], content={}) - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY self.assertRaises(RuntimeError, this.call, method='PUT', path=['testdocument', guid, 'blob'], content='probe') - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY assert not exists('blobs/%s' % hashlib.sha1('probe').hexdigest()) def test_SetBLOBsWithMimeType(self): @@ -380,7 +380,7 @@ class RoutesTest(tests.Test): router = Router(db.Routes(volume)) guid1 = this.call(method='POST', path=['testdocument'], content={}) - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid1, 'blob']) + assert this.call(method='GET', path=['testdocument', guid1, 'blob']) is File.AWAY self.assertEqual( {'blob': ''}, this.call(method='GET', path=['testdocument', guid1], reply=['blob'], environ={'HTTP_HOST': '127.0.0.1'})) @@ -431,7 +431,7 @@ class RoutesTest(tests.Test): router = Router(db.Routes(volume)) guid = this.call(method='POST', path=['testdocument'], content={}) - self.assertRaises(http.NotFound, this.call, method='GET', path=['testdocument', guid, 'blob']) + assert this.call(method='GET', path=['testdocument', guid, 'blob']) is File.AWAY self.assertEqual( {'blob': ''}, this.call(method='GET', path=['testdocument', guid], reply=['blob'], environ={'HTTP_HOST': 'localhost'})) @@ -719,22 +719,15 @@ class RoutesTest(tests.Test): this.call(method='PUT', path=['testdocument', guid], content={'prop': 'bar'}) self.assertEqual('overriden', volume['testdocument'].get(guid)['prop']) - def __test_DoNotPassGuidsForCreate(self): + def test_DoNotPassGuidsForCreate(self): class TestDocument(db.Resource): - - @db.indexed_property(slot=1, default='') - def prop(self, value): - return value - - @db.indexed_property(db.Localized, prefix='L', default={}) - def localized_prop(self, value): - return value + pass volume = db.Volume(tests.tmpdir, [TestDocument]) router = Router(db.Routes(volume)) - self.assertRaises(http.Forbidden, this.call, method='POST', path=['testdocument'], content={'guid': 'foo'}) + self.assertRaises(http.BadRequest, this.call, method='POST', path=['testdocument'], content={'guid': 'foo'}) guid = this.call(method='POST', path=['testdocument'], content={}) assert guid @@ -905,20 +898,19 @@ class RoutesTest(tests.Test): return -1 @db.stored_property(db.Blob) - def blob(self, meta): - meta['blob'] = 'new-blob' - return meta + def blob(self, blob): + blob.meta['foo'] = 'bar' + return blob volume = db.Volume(tests.tmpdir, [TestDocument]) router = Router(db.Routes(volume)) guid = this.call(method='POST', path=['testdocument'], content={}) - self.touch(('new-blob', 'new-blob')) this.call(method='PUT', path=['testdocument', guid, 'blob'], content='old-blob') self.assertEqual( - 'new-blob', - this.call(method='GET', path=['testdocument', guid, 'blob'])['blob']) + 'bar', + this.call(method='GET', path=['testdocument', guid, 'blob']).meta['foo']) self.assertEqual( '1', this.call(method='GET', path=['testdocument', guid, 'prop1'])) @@ -1122,8 +1114,9 @@ class RoutesTest(tests.Test): volume = db.Volume(tests.tmpdir, [User, Document]) router = Router(db.Routes(volume)) + this.principal = 'user' - guid = this.call(method='POST', path=['document'], content={}, principal='user') + guid = this.call(method='POST', path=['document'], content={}) self.assertEqual( [{'name': 'user', 'role': 2}], this.call(method='GET', path=['document', guid, 'author'])) @@ -1133,7 +1126,7 @@ class RoutesTest(tests.Test): volume['user'].create({'guid': 'user', 'pubkey': '', 'name': 'User'}) - guid = this.call(method='POST', path=['document'], content={}, principal='user') + guid = this.call(method='POST', path=['document'], content={}) self.assertEqual( [{'guid': 'user', 'name': 'User', 'role': 3}], this.call(method='GET', path=['document', guid, 'author'])) @@ -1163,9 +1156,12 @@ class RoutesTest(tests.Test): volume['user'].create({'guid': 'user2', 'pubkey': '', 'name': 'User Name2'}) volume['user'].create({'guid': 'user3', 'pubkey': '', 'name': 'User Name 3'}) - guid1 = this.call(method='POST', path=['document'], content={}, principal='user1') - guid2 = this.call(method='POST', path=['document'], content={}, principal='user2') - guid3 = this.call(method='POST', path=['document'], content={}, principal='user3') + this.principal = 'user1' + guid1 = this.call(method='POST', path=['document'], content={}) + this.principal = 'user2' + guid2 = this.call(method='POST', path=['document'], content={}) + this.principal = 'user2' + guid3 = this.call(method='POST', path=['document'], content={}) self.assertEqual(sorted([ {'guid': guid1}, @@ -1210,7 +1206,8 @@ class RoutesTest(tests.Test): volume['user'].create({'guid': 'user2', 'pubkey': '', 'name': 'User2'}) volume['user'].create({'guid': 'user3', 'pubkey': '', 'name': 'User3'}) - guid = this.call(method='POST', path=['document'], content={}, principal='user1') + this.principal = 'user1' + guid = this.call(method='POST', path=['document'], content={}) this.call(method='PUT', path=['document', guid], cmd='useradd', user='user2', role=0) this.call(method='PUT', path=['document', guid], cmd='useradd', user='user3', role=0) @@ -1227,7 +1224,8 @@ class RoutesTest(tests.Test): }, volume['document'].get(guid)['author']) - this.call(method='PUT', path=['document', guid], cmd='userdel', user='user2', principal='user1') + this.principal = 'user1' + this.call(method='PUT', path=['document', guid], cmd='userdel', user='user2') this.call(method='PUT', path=['document', guid], cmd='useradd', user='user2', role=0) self.assertEqual([ @@ -1243,7 +1241,8 @@ class RoutesTest(tests.Test): }, volume['document'].get(guid)['author']) - this.call(method='PUT', path=['document', guid], cmd='userdel', user='user2', principal='user1') + this.principal = 'user1' + this.call(method='PUT', path=['document', guid], cmd='userdel', user='user2') this.call(method='PUT', path=['document', guid], cmd='useradd', user='user2', role=0) self.assertEqual([ @@ -1259,7 +1258,8 @@ class RoutesTest(tests.Test): }, volume['document'].get(guid)['author']) - this.call(method='PUT', path=['document', guid], cmd='userdel', user='user3', principal='user1') + this.principal = 'user1' + this.call(method='PUT', path=['document', guid], cmd='userdel', user='user3') this.call(method='PUT', path=['document', guid], cmd='useradd', user='user3', role=0) self.assertEqual([ @@ -1296,7 +1296,8 @@ class RoutesTest(tests.Test): volume['user'].create({'guid': 'user1', 'pubkey': '', 'name': 'User1'}) volume['user'].create({'guid': 'user2', 'pubkey': '', 'name': 'User2'}) - guid = this.call(method='POST', path=['document'], content={}, principal='user1') + this.principal = 'user1' + guid = this.call(method='POST', path=['document'], content={}) self.assertEqual([ {'guid': 'user1', 'name': 'User1', 'role': 3}, ], @@ -1367,7 +1368,8 @@ class RoutesTest(tests.Test): router = Router(db.Routes(volume)) volume['user'].create({'guid': 'user1', 'pubkey': '', 'name': 'User1'}) - guid = this.call(method='POST', path=['document'], content={}, principal='user1') + this.principal = 'user1' + guid = this.call(method='POST', path=['document'], content={}) this.call(method='PUT', path=['document', guid], cmd='useradd', user='User2', role=0) self.assertEqual([ @@ -1425,7 +1427,8 @@ class RoutesTest(tests.Test): volume['user'].create({'guid': 'user1', 'pubkey': '', 'name': 'User1'}) volume['user'].create({'guid': 'user2', 'pubkey': '', 'name': 'User2'}) - guid = this.call(method='POST', path=['document'], content={}, principal='user1') + this.principal = 'user1' + guid = this.call(method='POST', path=['document'], content={}) this.call(method='PUT', path=['document', guid], cmd='useradd', user='user2') this.call(method='PUT', path=['document', guid], cmd='useradd', user='User3') self.assertEqual([ @@ -1442,10 +1445,13 @@ class RoutesTest(tests.Test): volume['document'].get(guid)['author']) # Do not remove yourself - self.assertRaises(RuntimeError, this.call, method='PUT', path=['document', guid], cmd='userdel', user='user1', principal='user1') - self.assertRaises(RuntimeError, this.call, method='PUT', path=['document', guid], cmd='userdel', user='user2', principal='user2') + this.principal = 'user1' + self.assertRaises(RuntimeError, this.call, method='PUT', path=['document', guid], cmd='userdel', user='user1') + this.principal = 'user2' + self.assertRaises(RuntimeError, this.call, method='PUT', path=['document', guid], cmd='userdel', user='user2') - this.call(method='PUT', path=['document', guid], cmd='userdel', user='user1', principal='user2') + this.principal = 'user2' + this.call(method='PUT', path=['document', guid], cmd='userdel', user='user1') self.assertEqual([ {'guid': 'user2', 'name': 'User2', 'role': 1}, {'name': 'User3', 'role': 0}, @@ -1457,7 +1463,8 @@ class RoutesTest(tests.Test): }, volume['document'].get(guid)['author']) - this.call(method='PUT', path=['document', guid], cmd='userdel', user='User3', principal='user2') + this.principal = 'user2' + this.call(method='PUT', path=['document', guid], cmd='userdel', user='User3') self.assertEqual([ {'guid': 'user2', 'name': 'User2', 'role': 1}, ], @@ -1508,7 +1515,7 @@ class RoutesTest(tests.Test): def prop1(self, value): return value - @db.stored_property(db.Aggregated, acl=ACL.INSERT) + @db.stored_property(db.Aggregated, db.Property(), acl=ACL.INSERT) def prop3(self, value): return value @@ -1562,11 +1569,11 @@ class RoutesTest(tests.Test): class Document(db.Resource): - @db.stored_property(db.Aggregated, acl=ACL.INSERT) + @db.stored_property(db.Aggregated, db.Property(), acl=ACL.INSERT) def prop1(self, value): return value - @db.stored_property(db.Aggregated, acl=ACL.INSERT | ACL.REMOVE) + @db.stored_property(db.Aggregated, db.Property(), acl=ACL.INSERT | ACL.REMOVE) def prop2(self, value): return value @@ -1604,11 +1611,82 @@ class RoutesTest(tests.Test): ], events) + def test_NoDirectWriteToAggprops(self): + + class Document(db.Resource): + + @db.stored_property(db.Aggregated) + def prop(self, value): + return value + + volume = db.Volume(tests.tmpdir, [Document]) + router = Router(db.Routes(volume)) + guid = this.call(method='POST', path=['document'], content={}) + + self.assertRaises(http.Forbidden, this.call, method='POST', path=['document'], content={'prop': {}}) + self.assertRaises(http.Forbidden, this.call, method='PUT', path=['document', guid], content={'prop': {}}) + + def test_AggpropSubtypeCasts(self): + + class Document(db.Resource): + + @db.stored_property(db.Aggregated) + def props(self, value): + return value + + @db.stored_property(db.Aggregated, db.Blob()) + def blobs(self, value): + return value + + volume = db.Volume(tests.tmpdir, [Document]) + router = Router(db.Routes(volume)) + guid = this.call(method='POST', path=['document'], content={}) + + agg1 = this.call(method='POST', path=['document', guid, 'props'], content=-1) + agg2 = this.call(method='POST', path=['document', guid, 'props'], content=None) + agg3 = this.call(method='POST', path=['document', guid, 'props'], content={'foo': 'bar'}) + + self.assertEqual({ + agg1: {'seqno': 2, 'value': -1}, + agg2: {'seqno': 3, 'value': None}, + agg3: {'seqno': 4, 'value': {'foo': 'bar'}}, + }, + volume['document'][guid]['props']) + self.assertEqual({ + agg1: -1, + agg2: None, + agg3: {'foo': 'bar'}, + }, + dict(this.call(method='GET', path=['document', guid, 'props']))) + self.assertEqual({ + agg1: -1, + agg2: None, + agg3: {'foo': 'bar'}, + }, + dict(this.call(method='GET', path=['document', guid], reply=['props'])['props'])) + self.assertEqual({ + agg1: -1, + agg2: None, + agg3: {'foo': 'bar'}, + }, + dict(this.call(method='GET', path=['document'], reply=['props'])['result'][0]['props'])) + + agg1 = this.call(method='POST', path=['document', guid, 'blobs'], content='1') + agg2 = this.call(method='POST', path=['document', guid, 'blobs'], content='2') + agg3 = this.call(method='POST', path=['document', guid, 'blobs'], content='3') + + self.assertEqual({ + agg1: 'http://localhost/blobs/' + hashlib.sha1('1').hexdigest(), + agg2: 'http://localhost/blobs/' + hashlib.sha1('2').hexdigest(), + agg3: 'http://localhost/blobs/' + hashlib.sha1('3').hexdigest(), + }, + dict(this.call(method='GET', path=['document', guid, 'blobs'], environ={'HTTP_HOST': 'localhost'}))) + def test_FailOnAbsentAggprops(self): class Document(db.Resource): - @db.stored_property(db.Aggregated, acl=ACL.INSERT | ACL.REMOVE | ACL.REPLACE) + @db.stored_property(db.Aggregated, db.Property(), acl=ACL.INSERT | ACL.REMOVE | ACL.REPLACE) def prop(self, value): return value @@ -1627,11 +1705,11 @@ class RoutesTest(tests.Test): class Document(db.Resource): - @db.stored_property(db.Aggregated) + @db.stored_property(db.Aggregated, db.Property()) def prop1(self, value): return value - @db.stored_property(db.Aggregated, acl=ACL.INSERT | ACL.REMOVE | ACL.REPLACE) + @db.stored_property(db.Aggregated, db.Property(), acl=ACL.INSERT | ACL.REMOVE | ACL.REPLACE) def prop2(self, value): return value @@ -1674,7 +1752,7 @@ class RoutesTest(tests.Test): class Document(db.Resource): - @db.stored_property(db.Aggregated, acl=ACL.INSERT | ACL.REMOVE | ACL.REPLACE) + @db.stored_property(db.Aggregated, db.Property(), acl=ACL.INSERT | ACL.REMOVE | ACL.REPLACE) def prop(self, value): return value @@ -1701,7 +1779,7 @@ class RoutesTest(tests.Test): class Document(db.Resource): - @db.stored_property(db.Aggregated, acl=ACL.INSERT | ACL.REMOVE) + @db.stored_property(db.Aggregated, db.Property(), acl=ACL.INSERT | ACL.REMOVE) def prop(self, value): return value @@ -1710,18 +1788,22 @@ class RoutesTest(tests.Test): volume['user'].create({'guid': 'user1', 'pubkey': '', 'name': 'User1'}) volume['user'].create({'guid': 'user2', 'pubkey': '', 'name': 'User2'}) - guid = this.call(method='POST', path=['document'], content={}, principal=tests.UID) + this.principal = tests.UID + guid = this.call(method='POST', path=['document'], content={}) assert ACL.ORIGINAL & volume['document'][guid]['author'][tests.UID]['role'] - agg_guid1 = this.call(method='POST', path=['document', guid, 'prop'], content=1, principal=tests.UID) + this.principal = tests.UID + agg_guid1 = this.call(method='POST', path=['document', guid, 'prop'], content=1) assert tests.UID2 not in volume['document'][guid]['prop'][agg_guid1]['author'] assert ACL.ORIGINAL & volume['document'][guid]['prop'][agg_guid1]['author'][tests.UID]['role'] - agg_guid2 = this.call(method='POST', path=['document', guid, 'prop'], content=1, principal=tests.UID2) + this.principal = tests.UID2 + agg_guid2 = this.call(method='POST', path=['document', guid, 'prop'], content=1) assert tests.UID not in volume['document'][guid]['prop'][agg_guid2]['author'] assert not (ACL.ORIGINAL & volume['document'][guid]['prop'][agg_guid2]['author'][tests.UID2]['role']) - this.call(method='DELETE', path=['document', guid, 'prop', agg_guid2], principal=tests.UID2) + this.principal = tests.UID2 + this.call(method='DELETE', path=['document', guid, 'prop', agg_guid2]) assert tests.UID not in volume['document'][guid]['prop'][agg_guid2]['author'] assert not (ACL.ORIGINAL & volume['document'][guid]['prop'][agg_guid2]['author'][tests.UID2]['role']) @@ -1729,7 +1811,7 @@ class RoutesTest(tests.Test): class Document(db.Resource): - @db.stored_property(db.Aggregated, subtype=db.Blob()) + @db.stored_property(db.Aggregated, db.Blob()) def blobs(self, value): return value @@ -1789,7 +1871,7 @@ class RoutesTest(tests.Test): class Document(db.Resource): - @db.indexed_property(db.Aggregated, prefix='A', full_text=True) + @db.indexed_property(db.Aggregated, db.Property(), prefix='A', full_text=True) def comments(self, value): return value @@ -1851,7 +1933,8 @@ class RoutesTest(tests.Test): events = [] this.localcast = lambda x: events.append(x) - this.call(method='DELETE', path=['document', guid], principal=tests.UID) + this.principal = tests.UID + this.call(method='DELETE', path=['document', guid]) self.assertRaises(http.NotFound, this.call, method='GET', path=['document', guid]) self.assertEqual('deleted', volume['document'][guid]['state']) diff --git a/tests/units/db/volume.py b/tests/units/db/volume.py index b5f01a7..22d4782 100755 --- a/tests/units/db/volume.py +++ b/tests/units/db/volume.py @@ -22,7 +22,7 @@ from sugar_network.db import storage, index from sugar_network.db import directory as directory_ from sugar_network.db.directory import Directory from sugar_network.db.index import IndexWriter -from sugar_network.toolkit.router import ACL +from sugar_network.toolkit.router import ACL, File from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http, ranges @@ -78,7 +78,7 @@ class VolumeTest(tests.Test): {'content-type': 'application/octet-stream', 'content-length': '2', 'path': 'foo/2'}, {'commit': [[1, 5]]}, ], - [dict(i) for i in volume.diff(r, files=['foo'])]) + [i.meta if isinstance(i, File) else i for i in volume.diff(r, files=['foo'])]) self.assertEqual([[6, None]], r) r = [[2, 2]] @@ -126,7 +126,7 @@ class VolumeTest(tests.Test): {'content-type': 'application/octet-stream', 'content-length': '3', 'path': 'bar/3'}, {'commit': [[7, 9]]}, ], - [dict(i) for i in volume.diff(r, files=['foo', 'bar'])]) + [i.meta if isinstance(i, File) else i for i in volume.diff(r, files=['foo', 'bar'])]) self.assertEqual([[10, None]], r) def test_diff_SyncUsecase(self): @@ -366,7 +366,7 @@ class VolumeTest(tests.Test): 'prop4': {'value': hashlib.sha1('4444').hexdigest(), 'mtime': 1}, }}, ], - [dict(i) for i in volume.clone('document', 'guid')]) + [i.meta if isinstance(i, File) else i for i in volume.clone('document', 'guid')]) def test_patch_New(self): @@ -425,7 +425,7 @@ class VolumeTest(tests.Test): 'content-length': '1', 'content-type': 'application/octet-stream', }, - blob) + blob.meta) self.assertEqual('1', file(blob.path).read()) blob = volume2.blobs.get('foo/2') @@ -434,7 +434,7 @@ class VolumeTest(tests.Test): 'content-length': '2', 'content-type': 'application/octet-stream', }, - blob) + blob.meta) self.assertEqual('22', file(blob.path).read()) assert volume2.blobs.get('bar/3') is None @@ -513,7 +513,7 @@ class VolumeTest(tests.Test): class Document(db.Resource): - @db.stored_property(db.Aggregated) + @db.stored_property(db.Aggregated, db.Property()) def prop(self, value): return value @@ -662,7 +662,7 @@ class VolumeTest(tests.Test): class Document(db.Resource): - @db.stored_property(db.Aggregated) + @db.stored_property(db.Aggregated, db.Property()) def prop(self, value): return value @@ -798,7 +798,7 @@ class VolumeTest(tests.Test): def prop(self, value): return value - self.touch(('var/db.seqno', '100')) + self.touch(('var/seqno', '100')) volume = db.Volume('.', [Document]) def generator(): @@ -819,6 +819,160 @@ class VolumeTest(tests.Test): self.assertEqual((101, [[1, 3]]), volume.patch(patch)) assert volume['document']['1'].exists + def test_EditLocalProps(self): + + class Document(db.Resource): + + @db.stored_property() + def prop1(self, value): + return value + + @db.stored_property(acl=ACL.PUBLIC | ACL.LOCAL) + def prop2(self, value): + return value + + @db.stored_property() + def prop3(self, value): + return value + + directory = db.Volume('.', [Document])['document'] + + directory.create({'guid': '1', 'prop1': '1', 'prop2': '1', 'prop3': '1', 'ctime': 1, 'mtime': 1}) + self.utime('db/document', 0) + + self.assertEqual( + {'seqno': 1, 'value': 1, 'mtime': 0}, + directory['1'].meta('seqno')) + self.assertEqual( + {'seqno': 1, 'value': '1', 'mtime': 0}, + directory['1'].meta('prop1')) + self.assertEqual( + {'value': '1', 'mtime': 0}, + directory['1'].meta('prop2')) + self.assertEqual( + {'seqno': 1, 'value': '1', 'mtime': 0}, + directory['1'].meta('prop3')) + + directory.update('1', {'prop1': '2'}) + self.utime('db/document', 0) + + self.assertEqual( + {'seqno': 2, 'value': 2, 'mtime': 0}, + directory['1'].meta('seqno')) + self.assertEqual( + {'seqno': 2, 'value': '2', 'mtime': 0}, + directory['1'].meta('prop1')) + self.assertEqual( + {'value': '1', 'mtime': 0}, + directory['1'].meta('prop2')) + self.assertEqual( + {'seqno': 1, 'value': '1', 'mtime': 0}, + directory['1'].meta('prop3')) + + directory.update('1', {'prop2': '3'}) + self.utime('db/document', 0) + + self.assertEqual( + {'seqno': 2, 'value': 2, 'mtime': 0}, + directory['1'].meta('seqno')) + self.assertEqual( + {'seqno': 2, 'value': '2', 'mtime': 0}, + directory['1'].meta('prop1')) + self.assertEqual( + {'value': '3', 'mtime': 0}, + directory['1'].meta('prop2')) + self.assertEqual( + {'seqno': 1, 'value': '1', 'mtime': 0}, + directory['1'].meta('prop3')) + + directory.update('1', {'prop1': '4', 'prop2': '4', 'prop3': '4'}) + self.utime('db/document', 0) + + self.assertEqual( + {'seqno': 3, 'value': 3, 'mtime': 0}, + directory['1'].meta('seqno')) + self.assertEqual( + {'seqno': 3, 'value': '4', 'mtime': 0}, + directory['1'].meta('prop1')) + self.assertEqual( + {'value': '4', 'mtime': 0}, + directory['1'].meta('prop2')) + self.assertEqual( + {'seqno': 3, 'value': '4', 'mtime': 0}, + directory['1'].meta('prop3')) + + def test_DiffLocalProps(self): + + class Document(db.Resource): + + @db.stored_property() + def prop1(self, value): + return value + + @db.stored_property(acl=ACL.PUBLIC | ACL.LOCAL) + def prop2(self, value): + return value + + @db.stored_property() + def prop3(self, value): + return value + + volume = db.Volume('.', [Document]) + + volume['document'].create({'guid': '1', 'prop1': '1', 'prop2': '1', 'prop3': '1', 'ctime': 1, 'mtime': 1}) + self.utime('db/document/1/1', 0) + + r = [[1, None]] + self.assertEqual([ + {'resource': 'document'}, + {'guid': '1', 'patch': { + 'guid': {'value': '1', 'mtime': 0}, + 'ctime': {'value': 1, 'mtime': 0}, + 'prop1': {'value': '1', 'mtime': 0}, + 'prop3': {'value': '1', 'mtime': 0}, + 'mtime': {'value': 1, 'mtime': 0}, + }}, + {'commit': [[1, 1]]}, + ], + [dict(i) for i in volume.diff(r, files=['foo'])]) + self.assertEqual([[2, None]], r) + + volume['document'].update('1', {'prop1': '2'}) + self.utime('db/document', 0) + + self.assertEqual([ + {'resource': 'document'}, + {'guid': '1', 'patch': { + 'prop1': {'value': '2', 'mtime': 0}, + }}, + {'commit': [[2, 2]]}, + ], + [dict(i) for i in volume.diff(r, files=['foo'])]) + self.assertEqual([[3, None]], r) + + volume['document'].update('1', {'prop2': '3'}) + self.utime('db/document', 0) + + self.assertEqual([ + {'resource': 'document'}, + ], + [dict(i) for i in volume.diff(r, files=['foo'])]) + self.assertEqual([[3, None]], r) + + volume['document'].update('1', {'prop1': '4', 'prop2': '4', 'prop3': '4'}) + self.utime('db/document', 0) + + self.assertEqual([ + {'resource': 'document'}, + {'guid': '1', 'patch': { + 'prop1': {'value': '4', 'mtime': 0}, + 'prop3': {'value': '4', 'mtime': 0}, + }}, + {'commit': [[3, 3]]}, + ], + [dict(i) for i in volume.diff(r, files=['foo'])]) + self.assertEqual([[4, None]], r) + class _SessionSeqno(object): diff --git a/tests/units/model/context.py b/tests/units/model/context.py index 45a1ce8..d2ba27e 100755 --- a/tests/units/model/context.py +++ b/tests/units/model/context.py @@ -10,6 +10,7 @@ from __init__ import tests from sugar_network import db from sugar_network.db import blobs from sugar_network.client import IPCConnection, Connection, keyfile +from sugar_network.client.auth import SugarCreds from sugar_network.model.context import Context from sugar_network.toolkit.coroutine import this from sugar_network.toolkit.router import Request @@ -21,7 +22,7 @@ class ContextTest(tests.Test): def test_PackageImages(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) guid = conn.post(['context'], { 'type': 'package', @@ -36,7 +37,7 @@ class ContextTest(tests.Test): def test_ContextImages(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) guid = conn.post(['context'], { 'type': 'activity', @@ -73,7 +74,7 @@ class ContextTest(tests.Test): def test_Releases(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) context = conn.post(['context'], { 'type': 'activity', @@ -96,8 +97,8 @@ class ContextTest(tests.Test): assert release1 == str(hashlib.sha1(bundle1).hexdigest()) self.assertEqual({ release1: { - 'seqno': 10, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 9, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': next(volume['post'].find(query='title:1')[0]).guid, @@ -108,7 +109,7 @@ class ContextTest(tests.Test): 'stability': 'stable', }, }, - }, conn.get(['context', context, 'releases'])) + }, volume['context'][context]['releases']) assert volume.blobs.get(str(hashlib.sha1(bundle1).hexdigest())).exists activity_info2 = '\n'.join([ @@ -125,8 +126,8 @@ class ContextTest(tests.Test): assert release2 == str(hashlib.sha1(bundle2).hexdigest()) self.assertEqual({ release1: { - 'seqno': 10, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 9, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': next(volume['post'].find(query='title:1')[0]).guid, @@ -138,8 +139,8 @@ class ContextTest(tests.Test): }, }, release2: { - 'seqno': 13, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 12, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': next(volume['post'].find(query='title:2')[0]).guid, @@ -150,19 +151,19 @@ class ContextTest(tests.Test): 'stability': 'stable', }, }, - }, conn.get(['context', context, 'releases'])) + }, volume['context'][context]['releases']) assert volume.blobs.get(str(hashlib.sha1(bundle1).hexdigest())).exists assert volume.blobs.get(str(hashlib.sha1(bundle2).hexdigest())).exists conn.delete(['context', context, 'releases', release1]) self.assertEqual({ release1: { - 'seqno': 15, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 14, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, }, release2: { - 'seqno': 13, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 12, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': next(volume['post'].find(query='title:2')[0]).guid, @@ -173,212 +174,24 @@ class ContextTest(tests.Test): 'stability': 'stable', }, }, - }, conn.get(['context', context, 'releases'])) + }, volume['context'][context]['releases']) assert not volume.blobs.get(str(hashlib.sha1(bundle1).hexdigest())).exists assert volume.blobs.get(str(hashlib.sha1(bundle2).hexdigest())).exists conn.delete(['context', context, 'releases', release2]) self.assertEqual({ release1: { - 'seqno': 15, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 14, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, }, release2: { - 'seqno': 17, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 16, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, }, - }, conn.get(['context', context, 'releases'])) + }, volume['context'][context]['releases']) assert not volume.blobs.get(str(hashlib.sha1(bundle1).hexdigest())).exists assert not volume.blobs.get(str(hashlib.sha1(bundle2).hexdigest())).exists - def test_IncrementReleasesSeqnoOnNewReleases(self): - events = [] - volume = self.start_master() - this.broadcast = lambda x: events.append(x) - conn = Connection(auth=http.SugarAuth(keyfile.value)) - - context = conn.post(['context'], { - 'type': 'activity', - 'title': 'Activity', - 'summary': 'summary', - 'description': 'description', - }) - self.assertEqual([ - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(0, volume.releases_seqno.value) - - conn.put(['context', context], { - 'summary': 'summary2', - }) - self.assertEqual([ - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(0, volume.releases_seqno.value) - - bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ - '[Activity]', - 'name = Activity', - 'bundle_id = %s' % context, - 'exec = true', - 'icon = icon', - 'activity_version = 1', - 'license = Public Domain', - ]))) - release = conn.upload(['context', context, 'releases'], StringIO(bundle)) - self.assertEqual([ - {'event': 'release', 'seqno': 1}, - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(1, volume.releases_seqno.value) - - bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ - '[Activity]', - 'name = Activity', - 'bundle_id = %s' % context, - 'exec = true', - 'icon = icon', - 'activity_version = 2', - 'license = Public Domain', - ]))) - release = conn.upload(['context', context, 'releases'], StringIO(bundle)) - self.assertEqual([ - {'event': 'release', 'seqno': 1}, - {'event': 'release', 'seqno': 2}, - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(2, volume.releases_seqno.value) - - bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ - '[Activity]', - 'name = Activity', - 'bundle_id = %s' % context, - 'exec = true', - 'icon = icon', - 'activity_version = 2', - 'license = Public Domain', - ]))) - release = conn.upload(['context', context, 'releases'], StringIO(bundle)) - self.assertEqual([ - {'event': 'release', 'seqno': 1}, - {'event': 'release', 'seqno': 2}, - {'event': 'release', 'seqno': 3}, - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(3, volume.releases_seqno.value) - - conn.delete(['context', context, 'releases', release]) - self.assertEqual([ - {'event': 'release', 'seqno': 1}, - {'event': 'release', 'seqno': 2}, - {'event': 'release', 'seqno': 3}, - {'event': 'release', 'seqno': 4}, - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(4, volume.releases_seqno.value) - - def test_IncrementReleasesSeqnoOnDependenciesChange(self): - events = [] - volume = self.start_master() - this.broadcast = lambda x: events.append(x) - conn = Connection(auth=http.SugarAuth(keyfile.value)) - - context = conn.post(['context'], { - 'type': 'activity', - 'title': 'Activity', - 'summary': 'summary', - 'description': 'description', - }) - self.assertEqual([ - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(0, volume.releases_seqno.value) - - bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ - '[Activity]', - 'name = Activity', - 'bundle_id = %s' % context, - 'exec = true', - 'icon = icon', - 'activity_version = 2', - 'license = Public Domain', - ]))) - release = conn.upload(['context', context, 'releases'], StringIO(bundle)) - self.assertEqual([ - {'seqno': 1, 'event': 'release'} - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(1, volume.releases_seqno.value) - del events[:] - - conn.put(['context', context], { - 'dependencies': 'dep', - }) - self.assertEqual([ - {'event': 'release', 'seqno': 2}, - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(2, volume.releases_seqno.value) - - def test_IncrementReleasesSeqnoOnDeletes(self): - events = [] - volume = self.start_master() - this.broadcast = lambda x: events.append(x) - conn = Connection(auth=http.SugarAuth(keyfile.value)) - - context = conn.post(['context'], { - 'type': 'activity', - 'title': 'Activity', - 'summary': 'summary', - 'description': 'description', - }) - self.assertEqual([ - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(0, volume.releases_seqno.value) - - bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ - '[Activity]', - 'name = Activity', - 'bundle_id = %s' % context, - 'exec = true', - 'icon = icon', - 'activity_version = 2', - 'license = Public Domain', - ]))) - release = conn.upload(['context', context, 'releases'], StringIO(bundle)) - self.assertEqual([ - {'seqno': 1, 'event': 'release'} - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(1, volume.releases_seqno.value) - del events[:] - - conn.delete(['context', context]) - self.assertEqual([ - {'event': 'release', 'seqno': 2}, - ], [i for i in events if i['event'] == 'release']) - self.assertEqual(2, volume.releases_seqno.value) - del events[:] - - def test_RestoreReleasesSeqno(self): - events = [] - volume = self.start_master() - this.broadcast = lambda x: events.append(x) - conn = Connection(auth=http.SugarAuth(keyfile.value)) - - context = conn.post(['context'], { - 'type': 'activity', - 'title': 'Activity', - 'summary': 'summary', - 'description': 'description', - 'dependencies': 'dep', - }) - bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ - '[Activity]', - 'name = Activity', - 'bundle_id = %s' % context, - 'exec = true', - 'icon = icon', - 'activity_version = 2', - 'license = Public Domain', - ]))) - release = conn.upload(['context', context, 'releases'], StringIO(bundle)) - self.assertEqual(1, volume.releases_seqno.value) - - volume.close() - volume = db.Volume('master', []) - self.assertEqual(1, volume.releases_seqno.value) - if __name__ == '__main__': tests.main() diff --git a/tests/units/model/model.py b/tests/units/model/model.py index 857a54b..e28dd51 100755 --- a/tests/units/model/model.py +++ b/tests/units/model/model.py @@ -12,7 +12,9 @@ from sugar_network.model import load_bundle from sugar_network.model.post import Post from sugar_network.model.context import Context from sugar_network.node.model import User +from sugar_network.node.auth import Principal as _Principal from sugar_network.client import IPCConnection, Connection, keyfile +from sugar_network.client.auth import SugarCreds from sugar_network.toolkit.router import Request from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import i18n, http, coroutine, enforce @@ -43,10 +45,9 @@ class ModelTest(tests.Test): def test_load_bundle_Activity(self): volume = self.start_master() blobs = volume.blobs - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - conn.post(['context'], { - 'guid': 'bundle_id', + bundle_id = conn.post(['context'], { 'type': 'activity', 'title': 'Activity', 'summary': 'summary', @@ -55,7 +56,7 @@ class ModelTest(tests.Test): activity_info = '\n'.join([ '[Activity]', 'name = Activity', - 'bundle_id = bundle_id', + 'bundle_id = %s' % bundle_id, 'exec = true', 'icon = icon', 'activity_version = 1', @@ -70,16 +71,17 @@ class ModelTest(tests.Test): ) blob = blobs.post(bundle) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID) - context, release = load_bundle(blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id]) + context, release = load_bundle(blob, bundle_id) self.assertEqual({ 'content-type': 'application/vnd.olpc-sugar', 'content-disposition': 'attachment; filename="Activity-1%s"' % (mimetypes.guess_extension('application/vnd.olpc-sugar') or ''), 'content-length': str(len(bundle)), - 'x-seqno': '7', - }, dict(blobs.get(blob.digest))) - self.assertEqual('bundle_id', context) + 'x-seqno': '6', + }, blobs.get(blob.digest).meta) + self.assertEqual(bundle_id, context) self.assertEqual([[1], 0], release['version']) self.assertEqual('developer', release['stability']) self.assertEqual(['Public Domain'], release['license']) @@ -112,10 +114,9 @@ class ModelTest(tests.Test): def test_load_bundle_NonActivity(self): volume = self.start_master() blobs = volume.blobs - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - conn.post(['context'], { - 'guid': 'bundle_id', + bundle_id = conn.post(['context'], { 'type': 'book', 'title': 'NonActivity', 'summary': 'summary', @@ -123,18 +124,19 @@ class ModelTest(tests.Test): }) bundle = 'non-activity' blob = blobs.post(bundle) - blob['content-type'] = 'application/pdf' + blob.meta['content-type'] = 'application/pdf' - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID, version='2', license='GPL') - context, release = load_bundle(blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id], version='2', license='GPL') + context, release = load_bundle(blob, bundle_id) self.assertEqual({ 'content-type': 'application/pdf', 'content-disposition': 'attachment; filename="NonActivity-2.pdf"', 'content-length': str(len(bundle)), - 'x-seqno': '7', - }, dict(blobs.get(blob.digest))) - self.assertEqual('bundle_id', context) + 'x-seqno': '6', + }, blobs.get(blob.digest).meta) + self.assertEqual(bundle_id, context) self.assertEqual([[2], 0], release['version']) self.assertEqual(['GPL'], release['license']) @@ -153,10 +155,9 @@ class ModelTest(tests.Test): def test_load_bundle_ReuseActivityLicense(self): volume = self.start_master() blobs = volume.blobs - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - conn.post(['context'], { - 'guid': 'bundle_id', + bundle_id = conn.post(['context'], { 'type': 'activity', 'title': 'Activity', 'summary': 'summary', @@ -166,46 +167,48 @@ class ModelTest(tests.Test): activity_info_wo_license = '\n'.join([ '[Activity]', 'name = Activity', - 'bundle_id = bundle_id', + 'bundle_id = %s' % bundle_id, 'exec = true', 'icon = icon', 'activity_version = 1', ]) bundle = self.zips(('topdir/activity/activity.info', activity_info_wo_license)) blob_wo_license = blobs.post(bundle) - self.assertRaises(http.BadRequest, load_bundle, blob_wo_license, 'bundle_id') + self.assertRaises(http.BadRequest, load_bundle, blob_wo_license, bundle_id) - volume['context'].update('bundle_id', {'releases': { + volume['context'].update(bundle_id, {'releases': { 'new': {'value': {'release': 2, 'license': ['New']}}, }}) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID) - context, release = load_bundle(blob_wo_license, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id]) + context, release = load_bundle(blob_wo_license, bundle_id) self.assertEqual(['New'], release['license']) - volume['context'].update('bundle_id', {'releases': { + volume['context'].update(bundle_id, {'releases': { 'new': {'value': {'release': 2, 'license': ['New']}}, 'old': {'value': {'release': 1, 'license': ['Old']}}, }}) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID) - context, release = load_bundle(blob_wo_license, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id]) + context, release = load_bundle(blob_wo_license, bundle_id) self.assertEqual(['New'], release['license']) - volume['context'].update('bundle_id', {'releases': { + volume['context'].update(bundle_id, {'releases': { 'new': {'value': {'release': 2, 'license': ['New']}}, 'old': {'value': {'release': 1, 'license': ['Old']}}, 'newest': {'value': {'release': 3, 'license': ['Newest']}}, }}) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID) - context, release = load_bundle(blob_wo_license, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id]) + context, release = load_bundle(blob_wo_license, bundle_id) self.assertEqual(['Newest'], release['license']) def test_load_bundle_ReuseNonActivityLicense(self): volume = self.start_master() blobs = volume.blobs - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - conn.post(['context'], { - 'guid': 'bundle_id', + bundle_id = conn.post(['context'], { 'type': 'book', 'title': 'Activity', 'summary': 'summary', @@ -213,40 +216,43 @@ class ModelTest(tests.Test): }) blob = blobs.post('non-activity') - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID, version='1') - self.assertRaises(http.BadRequest, load_bundle, blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id], version='1') + self.assertRaises(http.BadRequest, load_bundle, blob, bundle_id) - volume['context'].update('bundle_id', {'releases': { + volume['context'].update(bundle_id, {'releases': { 'new': {'value': {'release': 2, 'license': ['New']}}, }}) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID, version='1') - context, release = load_bundle(blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id], version='1') + context, release = load_bundle(blob, bundle_id) self.assertEqual(['New'], release['license']) - volume['context'].update('bundle_id', {'releases': { + volume['context'].update(bundle_id, {'releases': { 'new': {'value': {'release': 2, 'license': ['New']}}, 'old': {'value': {'release': 1, 'license': ['Old']}}, }}) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID, version='1') - context, release = load_bundle(blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id], version='1') + context, release = load_bundle(blob, bundle_id) self.assertEqual(['New'], release['license']) - volume['context'].update('bundle_id', {'releases': { + volume['context'].update(bundle_id, {'releases': { 'new': {'value': {'release': 2, 'license': ['New']}}, 'old': {'value': {'release': 1, 'license': ['Old']}}, 'newest': {'value': {'release': 3, 'license': ['Newest']}}, }}) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID, version='1') - context, release = load_bundle(blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id], version='1') + context, release = load_bundle(blob, bundle_id) self.assertEqual(['Newest'], release['license']) def test_load_bundle_WrontContextType(self): volume = self.start_master() blobs = volume.blobs - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - conn.post(['context'], { - 'guid': 'bundle_id', + bundle_id = conn.post(['context'], { 'type': 'group', 'title': 'NonActivity', 'summary': 'summary', @@ -254,13 +260,14 @@ class ModelTest(tests.Test): }) blob = blobs.post('non-activity') - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID, version='2', license='GPL') - self.assertRaises(http.BadRequest, load_bundle, blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id], version='2', license='GPL') + self.assertRaises(http.BadRequest, load_bundle, blob, bundle_id) activity_info = '\n'.join([ '[Activity]', 'name = Activity', - 'bundle_id = bundle_id', + 'bundle_id = %s' % bundle_id, 'exec = true', 'icon = icon', 'activity_version = 1', @@ -274,13 +281,13 @@ class ModelTest(tests.Test): ('topdir/CHANGELOG', changelog), ) blob = blobs.post(bundle) - self.assertRaises(http.BadRequest, load_bundle, blob, 'bundle_id') + self.assertRaises(http.BadRequest, load_bundle, blob, bundle_id) def test_load_bundle_MissedContext(self): volume = self.start_master() blobs = volume.blobs volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ '[Activity]', @@ -295,14 +302,15 @@ class ModelTest(tests.Test): ]))) blob = blobs.post(bundle) - this.request = Request(principal=tests.UID) + this.principal = Principal(tests.UID) + this.request = Request() self.assertRaises(http.NotFound, load_bundle, blob, initial=False) def test_load_bundle_CreateContext(self): volume = self.start_master() blobs = volume.blobs volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) bundle = self.zips( ('ImageViewer.activity/activity/activity.info', '\n'.join([ @@ -322,7 +330,8 @@ class ModelTest(tests.Test): ) blob = blobs.post(bundle) - this.request = Request(principal=tests.UID) + this.principal = Principal(tests.UID) + this.request = Request() context, release = load_bundle(blob, initial=True) self.assertEqual('org.laptop.ImageViewerActivity', context) @@ -348,7 +357,11 @@ class ModelTest(tests.Test): def test_load_bundle_UpdateContext(self): volume = self.start_master() blobs = volume.blobs - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) + self.touch(('master/etc/authorization.conf', [ + '[permissions]', + '%s = admin' % tests.UID, + ])) conn.post(['context'], { 'guid': 'org.laptop.ImageViewerActivity', @@ -395,7 +408,8 @@ class ModelTest(tests.Test): ) blob = blobs.post(bundle) - this.request = Request(method='POST', path=['context', 'org.laptop.ImageViewerActivity'], principal=tests.UID) + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', 'org.laptop.ImageViewerActivity']) context, release = load_bundle(blob, initial=True) context = volume['context'].get('org.laptop.ImageViewerActivity') @@ -423,10 +437,9 @@ class ModelTest(tests.Test): volume = self.start_master() blobs = volume.blobs volume['user'].create({'guid': tests.UID2, 'name': 'user2', 'pubkey': tests.PUBKEY2}) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - conn.post(['context'], { - 'guid': 'bundle_id', + bundle_id = conn.post(['context'], { 'type': 'activity', 'title': 'Activity', 'summary': 'summary', @@ -436,7 +449,7 @@ class ModelTest(tests.Test): bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ '[Activity]', 'name = Activity2', - 'bundle_id = bundle_id', + 'bundle_id = %s' % bundle_id, 'exec = true', 'icon = icon', 'activity_version = 1', @@ -444,12 +457,13 @@ class ModelTest(tests.Test): 'stability = developer', ]))) blob = blobs.post(bundle) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID2) - context, release = load_bundle(blob, 'bundle_id') + this.principal = Principal(tests.UID2) + this.request = Request(method='POST', path=['context', bundle_id]) + context, release = load_bundle(blob, bundle_id) - assert tests.UID in volume['context']['bundle_id']['author'] - assert tests.UID2 not in volume['context']['bundle_id']['author'] - self.assertEqual({'en': 'Activity'}, volume['context']['bundle_id']['title']) + assert tests.UID in volume['context'][bundle_id]['author'] + assert tests.UID2 not in volume['context'][bundle_id]['author'] + self.assertEqual({'en': 'Activity'}, volume['context'][bundle_id]['title']) post = volume['post'][release['announce']] assert tests.UID not in post['author'] @@ -463,12 +477,13 @@ class ModelTest(tests.Test): blobs.delete(blob.digest) blob = blobs.post(bundle) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID) - context, release = load_bundle(blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id]) + context, release = load_bundle(blob, bundle_id) - assert tests.UID in volume['context']['bundle_id']['author'] - assert tests.UID2 not in volume['context']['bundle_id']['author'] - self.assertEqual({'en': 'Activity2'}, volume['context']['bundle_id']['title']) + assert tests.UID in volume['context'][bundle_id]['author'] + assert tests.UID2 not in volume['context'][bundle_id]['author'] + self.assertEqual({'en': 'Activity2'}, volume['context'][bundle_id]['title']) post = volume['post'][release['announce']] assert tests.UID in post['author'] @@ -483,10 +498,9 @@ class ModelTest(tests.Test): def test_load_bundle_PopulateRequires(self): volume = self.start_master() blobs = volume.blobs - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - conn.post(['context'], { - 'guid': 'bundle_id', + bundle_id = conn.post(['context'], { 'type': 'activity', 'title': 'Activity', 'summary': 'summary', @@ -495,7 +509,7 @@ class ModelTest(tests.Test): bundle = self.zips( ('ImageViewer.activity/activity/activity.info', '\n'.join([ '[Activity]', - 'bundle_id = bundle_id', + 'bundle_id = %s' % bundle_id, 'name = Image Viewer', 'activity_version = 22', 'license = GPLv2+', @@ -506,8 +520,9 @@ class ModelTest(tests.Test): ('ImageViewer.activity/activity/activity-imageviewer.svg', ''), ) blob = blobs.post(bundle) - this.request = Request(method='POST', path=['context', 'bundle_id'], principal=tests.UID) - context, release = load_bundle(blob, 'bundle_id') + this.principal = Principal(tests.UID) + this.request = Request(method='POST', path=['context', bundle_id]) + context, release = load_bundle(blob, bundle_id) self.assertEqual({ 'dep5': [([1, 0], [[40], 0])], @@ -522,7 +537,7 @@ class ModelTest(tests.Test): def test_load_bundle_IgnoreNotSupportedContextTypes(self): volume = self.start_master([User, Context]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) context = conn.post(['context'], { 'type': 'package', @@ -533,9 +548,14 @@ class ModelTest(tests.Test): this.request = Request(method='POST', path=['context', context]) aggid = conn.post(['context', context, 'releases'], {}) self.assertEqual({ - aggid: {'seqno': 4, 'value': {}, 'author': {tests.UID: {'role': 3, 'name': tests.UID, 'order': 0}}}, + aggid: {'seqno': 3, 'value': {}, 'author': {tests.UID: {'role': 3, 'name': 'test', 'order': 0}}}, }, volume['context'][context]['releases']) +class Principal(_Principal): + + admin = True + + if __name__ == '__main__': tests.main() diff --git a/tests/units/node/master.py b/tests/units/node/master.py index 2577ba9..ff5bc5d 100755 --- a/tests/units/node/master.py +++ b/tests/units/node/master.py @@ -19,6 +19,7 @@ from __init__ import tests from sugar_network.client import Connection, keyfile, api from sugar_network.db.directory import Directory from sugar_network import db, node, toolkit +from sugar_network.client.auth import SugarCreds from sugar_network.node.master import MasterRoutes from sugar_network.node.model import User from sugar_network.db.volume import Volume @@ -44,7 +45,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) self.touch(('blob1', '1')) self.touch(('blob2', '2')) @@ -94,7 +95,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) patch = ''.join(parcel.encode([ ('push', None, [ @@ -141,7 +142,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) self.touch(('blob', 'blob')) patch = ''.join(parcel.encode([ @@ -194,7 +195,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) self.touch(('blob', 'blob')) patch = ''.join(parcel.encode([ @@ -221,7 +222,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([User, Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['document'].create({'guid': 'guid', 'ctime': 1, 'mtime': 1}) self.utime('master/db/document/gu/guid', 1) @@ -246,7 +247,7 @@ class MasterTest(tests.Test): {'commit': [[1, 3]]}, ]), ], - [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode(response.raw)]) self.assertEqual( 'sugar_network_node=%s; Max-Age=3600; HttpOnly' % b64encode(json.dumps({ 'id': 1, @@ -266,7 +267,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([User, Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['document'].create({'guid': '1', 'ctime': 1, 'mtime': 1}) self.utime('master/db/document/1/1', 1) @@ -306,7 +307,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([User, Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['document'].create({'guid': '1', 'ctime': 1, 'mtime': 1}) self.utime('master/db/document/1/1', 1) @@ -327,7 +328,7 @@ class MasterTest(tests.Test): self.assertEqual([ ({'from': '127.0.0.1:7777', 'packet': 'push'}, [{'resource': 'document'}]), ], - [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode(response.raw)]) response = conn.request('GET', [], params={'cmd': 'pull'}, headers={ 'cookie': 'sugar_network_node=%s' % b64encode(json.dumps({ @@ -349,7 +350,7 @@ class MasterTest(tests.Test): {'commit': [[1, 1]]}, ]), ], - [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode(response.raw)]) response = conn.request('GET', [], params={'cmd': 'pull'}, headers={ 'cookie': 'sugar_network_node=%s' % b64encode(json.dumps({ @@ -367,7 +368,7 @@ class MasterTest(tests.Test): {'commit': [[4, 4]]}, ]), ], - [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode(response.raw)]) def test_pull_ExcludeAckRequests(self): @@ -375,7 +376,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([User, Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['document'].create({'guid': '1', 'ctime': 1, 'mtime': 1}) self.utime('master/db/document/1/1', 1) @@ -398,7 +399,7 @@ class MasterTest(tests.Test): ({'from': '127.0.0.1:7777', 'to': 'node2', 'packet': 'ack', 'ack': [[1, 2]]}, []), ({'from': '127.0.0.1:7777', 'packet': 'push'}, [{'resource': 'document'}]), ], - [(packet.header, [dict(record) for record in packet]) for packet in reply]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in reply]) def test_pull_Limitted(self): RECORD = 1024 * 1024 @@ -410,7 +411,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([User, Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['document'].create({'guid': '1', 'ctime': 1, 'mtime': 1, 'prop': '.' * RECORD}) self.utime('master/db/document/1/1', 1) @@ -429,7 +430,7 @@ class MasterTest(tests.Test): {'resource': 'document'}, ]), ], - [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode(response.raw)]) self.assertEqual( 'sugar_network_node=%s; Max-Age=3600; HttpOnly' % b64encode(json.dumps({ 'id': 1, @@ -452,7 +453,7 @@ class MasterTest(tests.Test): {'commit': [[1, 1]]}, ]), ], - [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode(response.raw)]) self.assertEqual( 'sugar_network_node=%s; Max-Age=3600; HttpOnly' % b64encode(json.dumps({ 'id': 1, @@ -481,7 +482,7 @@ class MasterTest(tests.Test): {'commit': [[2, 3]]}, ]), ], - [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode(response.raw)]) self.assertEqual( 'sugar_network_node=%s; Max-Age=3600; HttpOnly' % b64encode(json.dumps({ 'id': 1, @@ -503,7 +504,7 @@ class MasterTest(tests.Test): pass volume = self.start_master([User, Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['document'].create({'guid': 'guid', 'ctime': 1, 'mtime': 1}) self.utime('master/db/document/gu/guid', 1) @@ -539,7 +540,7 @@ class MasterTest(tests.Test): {'commit': [[1, 2]]}, ]), ], - [(packet.header, [dict(record) for record in packet]) for packet in parcel.decode(response.raw)]) + [(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode(response.raw)]) assert volume['document']['2'].exists self.assertEqual('ccc', ''.join(blob2.iter_content())) diff --git a/tests/units/node/model.py b/tests/units/node/model.py index 4187d3c..a89a92b 100755 --- a/tests/units/node/model.py +++ b/tests/units/node/model.py @@ -3,6 +3,7 @@ import os import time +from cStringIO import StringIO from __init__ import tests @@ -11,8 +12,9 @@ from sugar_network.client import Connection, keyfile, api from sugar_network.model.post import Post from sugar_network.model.context import Context from sugar_network.node import model, obs -from sugar_network.node.model import User +from sugar_network.node.model import User, Volume from sugar_network.node.routes import NodeRoutes +from sugar_network.client.auth import SugarCreds from sugar_network.toolkit.coroutine import this from sugar_network.toolkit.router import Request, Router from sugar_network.toolkit import spec, i18n, http, coroutine, enforce @@ -20,27 +22,193 @@ from sugar_network.toolkit import spec, i18n, http, coroutine, enforce class ModelTest(tests.Test): - def test_IncrementReleasesSeqno(self): + def test_IncrementReleasesSeqnoOnNewReleases(self): events = [] - volume = self.start_master([User, model.Context, Post]) + volume = self.start_master() this.broadcast = lambda x: events.append(x) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) context = conn.post(['context'], { - 'type': 'group', + 'type': 'activity', 'title': 'Activity', 'summary': 'summary', 'description': 'description', }) self.assertEqual([ ], [i for i in events if i['event'] == 'release']) - self.assertEqual(0, volume.releases_seqno.value) + self.assertEqual(0, volume.release_seqno.value) - aggid = conn.post(['context', context, 'releases'], -1) + conn.put(['context', context], { + 'summary': 'summary2', + }) + self.assertEqual([ + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(0, volume.release_seqno.value) + + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 1', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) + self.assertEqual([ + {'event': 'release', 'seqno': 1}, + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(1, volume.release_seqno.value) + + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) self.assertEqual([ {'event': 'release', 'seqno': 1}, + {'event': 'release', 'seqno': 2}, ], [i for i in events if i['event'] == 'release']) - self.assertEqual(1, volume.releases_seqno.value) + self.assertEqual(2, volume.release_seqno.value) + + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) + self.assertEqual([ + {'event': 'release', 'seqno': 1}, + {'event': 'release', 'seqno': 2}, + {'event': 'release', 'seqno': 3}, + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(3, volume.release_seqno.value) + + conn.delete(['context', context, 'releases', release]) + self.assertEqual([ + {'event': 'release', 'seqno': 1}, + {'event': 'release', 'seqno': 2}, + {'event': 'release', 'seqno': 3}, + {'event': 'release', 'seqno': 4}, + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(4, volume.release_seqno.value) + + def test_IncrementReleasesSeqnoOnDependenciesChange(self): + events = [] + volume = self.start_master() + this.broadcast = lambda x: events.append(x) + conn = Connection(creds=SugarCreds(keyfile.value)) + + context = conn.post(['context'], { + 'type': 'activity', + 'title': 'Activity', + 'summary': 'summary', + 'description': 'description', + }) + self.assertEqual([ + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(0, volume.release_seqno.value) + + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) + self.assertEqual([ + {'seqno': 1, 'event': 'release'} + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(1, volume.release_seqno.value) + del events[:] + + conn.put(['context', context], { + 'dependencies': 'dep', + }) + self.assertEqual([ + {'event': 'release', 'seqno': 2}, + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(2, volume.release_seqno.value) + + def test_IncrementReleasesSeqnoOnDeletes(self): + events = [] + volume = self.start_master() + this.broadcast = lambda x: events.append(x) + conn = Connection(creds=SugarCreds(keyfile.value)) + + context = conn.post(['context'], { + 'type': 'activity', + 'title': 'Activity', + 'summary': 'summary', + 'description': 'description', + }) + self.assertEqual([ + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(0, volume.release_seqno.value) + + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) + self.assertEqual([ + {'seqno': 1, 'event': 'release'} + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(1, volume.release_seqno.value) + del events[:] + + conn.delete(['context', context]) + self.assertEqual([ + {'event': 'release', 'seqno': 2}, + ], [i for i in events if i['event'] == 'release']) + self.assertEqual(2, volume.release_seqno.value) + del events[:] + + def test_RestoreReleasesSeqno(self): + events = [] + volume = self.start_master() + this.broadcast = lambda x: events.append(x) + conn = Connection(creds=SugarCreds(keyfile.value)) + + context = conn.post(['context'], { + 'type': 'activity', + 'title': 'Activity', + 'summary': 'summary', + 'description': 'description', + 'dependencies': 'dep', + }) + bundle = self.zips(('topdir/activity/activity.info', '\n'.join([ + '[Activity]', + 'name = Activity', + 'bundle_id = %s' % context, + 'exec = true', + 'icon = icon', + 'activity_version = 2', + 'license = Public Domain', + ]))) + release = conn.upload(['context', context, 'releases'], StringIO(bundle)) + self.assertEqual(1, volume.release_seqno.value) + + volume.close() + volume = Volume('master', []) + self.assertEqual(1, volume.release_seqno.value) def test_Packages(self): self.override(obs, 'get_repos', lambda: [ @@ -51,7 +219,7 @@ class ModelTest(tests.Test): self.override(obs, 'resolve', lambda repo, arch, names: {'version': '1.0'}) volume = self.start_master([User, model.Context]) - conn = http.Connection(api.value, http.SugarAuth(keyfile.value)) + conn = http.Connection(api.value, SugarCreds(keyfile.value)) guid = conn.post(['context'], { 'type': 'package', @@ -65,8 +233,8 @@ class ModelTest(tests.Test): }) self.assertEqual({ '*': { - 'seqno': 4, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 3, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['pkg1.bin', 'pkg2.bin'], 'devel': ['pkg3.devel']}, }, 'resolves': { @@ -89,8 +257,8 @@ class ModelTest(tests.Test): }) self.assertEqual({ 'Gentoo': { - 'seqno': 6, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 5, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['pkg1.bin', 'pkg2.bin'], 'devel': ['pkg3.devel']}, }, 'resolves': { @@ -111,8 +279,8 @@ class ModelTest(tests.Test): }) self.assertEqual({ 'Debian-6.0': { - 'seqno': 8, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 7, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['pkg1.bin', 'pkg2.bin'], 'devel': ['pkg3.devel']}, }, 'resolves': { @@ -128,7 +296,7 @@ class ModelTest(tests.Test): self.override(obs, 'resolve', lambda repo, arch, names: enforce(False, 'resolve failed')) volume = self.start_master([User, model.Context]) - conn = http.Connection(api.value, http.SugarAuth(keyfile.value)) + conn = http.Connection(api.value, SugarCreds(keyfile.value)) guid = conn.post(['context'], { 'type': 'package', @@ -142,8 +310,8 @@ class ModelTest(tests.Test): }) self.assertEqual({ '*': { - 'seqno': 4, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 3, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['pkg1.bin', 'pkg2.bin'], 'devel': ['pkg3.devel']}, }, 'resolves': { @@ -160,7 +328,7 @@ class ModelTest(tests.Test): ]) volume = self.start_master([User, model.Context]) - conn = http.Connection(api.value, http.SugarAuth(keyfile.value)) + conn = http.Connection(api.value, SugarCreds(keyfile.value)) guid = conn.post(['context'], { 'type': 'package', 'title': 'title', @@ -172,8 +340,8 @@ class ModelTest(tests.Test): conn.put(['context', guid, 'releases', '*'], {'binary': '1'}) self.assertEqual({ '*': { - 'seqno': 4, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 3, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['1']}, }, 'resolves': { @@ -188,13 +356,13 @@ class ModelTest(tests.Test): conn.put(['context', guid, 'releases', 'Debian'], {'binary': '2'}) self.assertEqual({ '*': { - 'seqno': 4, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 3, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['1']}, }, 'Debian': { - 'seqno': 5, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 4, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['2']}, }, 'resolves': { @@ -209,18 +377,18 @@ class ModelTest(tests.Test): conn.put(['context', guid, 'releases', 'Debian-6.0'], {'binary': '3'}) self.assertEqual({ '*': { - 'seqno': 4, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 3, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['1']}, }, 'Debian': { - 'seqno': 5, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 4, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['2']}, }, 'Debian-6.0': { - 'seqno': 6, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 5, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['3']}, }, 'resolves': { @@ -235,18 +403,18 @@ class ModelTest(tests.Test): conn.put(['context', guid, 'releases', 'Debian'], {'binary': '4'}) self.assertEqual({ '*': { - 'seqno': 4, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 3, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['1']}, }, 'Debian': { - 'seqno': 7, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 6, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['4']}, }, 'Debian-6.0': { - 'seqno': 6, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 5, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': {'binary': ['3']}, }, 'resolves': { diff --git a/tests/units/node/node.py b/tests/units/node/node.py index 89373dc..9e5206f 100755 --- a/tests/units/node/node.py +++ b/tests/units/node/node.py @@ -18,10 +18,12 @@ from __init__ import tests from sugar_network import db, node, model, client from sugar_network.client import Connection, keyfile, api from sugar_network.toolkit import http, coroutine +from sugar_network.client.auth import SugarCreds from sugar_network.node.routes import NodeRoutes from sugar_network.node.master import MasterRoutes from sugar_network.model.context import Context from sugar_network.node.model import User +from sugar_network.node.auth import Principal from sugar_network.toolkit.router import Router, Request, Response, fallbackroute, ACL, route from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import http @@ -31,9 +33,9 @@ class NodeTest(tests.Test): def test_RegisterUser(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - guid = this.call(method='POST', path=['user'], principal=tests.UID2, content={ + guid = this.call(method='POST', path=['user'], environ=auth_env(tests.UID2), content={ 'name': 'user', 'pubkey': tests.PUBKEY, }) @@ -59,14 +61,14 @@ class NodeTest(tests.Test): pass volume = self.start_master([Document, User], Routes) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - guid = this.call(method='POST', path=['document'], principal=tests.UID, content={}) + guid = this.call(method='POST', path=['document'], environ=auth_env(tests.UID), content={}) this.request = Request() self.assertRaises(http.Unauthorized, this.call, method='GET', cmd='probe1', path=['document', guid]) this.request = Request() - this.call(method='GET', cmd='probe1', path=['document', guid], principal=tests.UID) + this.call(method='GET', cmd='probe1', path=['document', guid], environ=auth_env(tests.UID)) this.request = Request() this.call(method='GET', cmd='probe2', path=['document', guid]) @@ -89,24 +91,24 @@ class NodeTest(tests.Test): pass volume = self.start_master([Document, User], Routes) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) volume['user'].create({'guid': tests.UID2, 'name': 'user2', 'pubkey': tests.PUBKEY2}) - guid = this.call(method='POST', path=['document'], principal=tests.UID, content={}) + guid = this.call(method='POST', path=['document'], environ=auth_env(tests.UID), content={}) - self.assertRaises(http.Forbidden, this.call, method='GET', cmd='probe1', path=['document', guid], principal=tests.UID2) - this.call(method='GET', cmd='probe1', path=['document', guid], principal=tests.UID) + self.assertRaises(http.Forbidden, this.call, method='GET', cmd='probe1', path=['document', guid], environ=auth_env(tests.UID2)) + this.call(method='GET', cmd='probe1', path=['document', guid], environ=auth_env(tests.UID)) - this.call(method='GET', cmd='probe2', path=['document', guid], principal=tests.UID2) + this.call(method='GET', cmd='probe2', path=['document', guid], environ=auth_env(tests.UID2)) this.call(method='GET', cmd='probe2', path=['document', guid]) def test_ForbiddenCommandsForUserResource(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) - this.call(method='POST', path=['user'], principal=tests.UID2, content={ + this.call(method='POST', path=['user'], environ=auth_env(tests.UID2), content={ 'name': 'user1', 'pubkey': tests.PUBKEY, }) @@ -115,16 +117,16 @@ class NodeTest(tests.Test): this.request = Request() self.assertRaises(http.Unauthorized, this.call, method='PUT', path=['user', tests.UID], content={'name': 'user2'}) this.request = Request() - self.assertRaises(http.Forbidden, this.call, method='PUT', path=['user', tests.UID], principal=tests.UID2, content={'name': 'user2'}) + self.assertRaises(http.Unauthorized, this.call, method='PUT', path=['user', tests.UID], environ=auth_env(tests.UID2), content={'name': 'user2'}) this.request = Request() - this.call(method='PUT', path=['user', tests.UID], principal=tests.UID, content={'name': 'user2'}) + this.call(method='PUT', path=['user', tests.UID], environ=auth_env(tests.UID), content={'name': 'user2'}) this.request = Request() self.assertEqual('user2', this.call(method='GET', path=['user', tests.UID, 'name'])) def test_authorize_Config(self): - self.touch(('authorization.conf', [ - '[%s]' % tests.UID, - 'root = True', + self.touch(('master/etc/authorization.conf', [ + '[permissions]', + '%s = admin' % tests.UID, ])) class Routes(NodeRoutes): @@ -137,13 +139,13 @@ class NodeTest(tests.Test): return 'ok' volume = self.start_master([User], Routes) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) volume['user'].create({'guid': tests.UID2, 'name': 'test', 'pubkey': tests.PUBKEY2}) - self.assertRaises(http.Forbidden, this.call, method='PROBE') - self.assertRaises(http.Forbidden, this.call, method='PROBE', principal=tests.UID2) - self.assertEqual('ok', this.call(method='PROBE', principal=tests.UID)) + self.assertRaises(http.Unauthorized, this.call, method='PROBE') + self.assertRaises(http.Forbidden, this.call, method='PROBE', environ=auth_env(tests.UID2)) + self.assertEqual('ok', this.call(method='PROBE', environ=auth_env(tests.UID))) def test_authorize_OnlyAuthros(self): @@ -154,18 +156,18 @@ class NodeTest(tests.Test): return value volume = self.start_master([User, Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) volume['user'].create({'guid': tests.UID2, 'name': 'user', 'pubkey': tests.PUBKEY2}) - guid = this.call(method='POST', path=['document'], principal=tests.UID, content={'prop': '1'}) - self.assertRaises(http.Forbidden, this.call, method='PUT', path=['document', guid], content={'prop': '2'}, principal=tests.UID2) + guid = this.call(method='POST', path=['document'], environ=auth_env(tests.UID), content={'prop': '1'}) + self.assertRaises(http.Forbidden, this.call, method='PUT', path=['document', guid], content={'prop': '2'}, environ=auth_env(tests.UID2)) self.assertEqual('1', volume['document'].get(guid)['prop']) def test_authorize_FullWriteForRoot(self): - self.touch(('authorization.conf', [ - '[%s]' % tests.UID2, - 'root = True', + self.touch(('master/etc/authorization.conf', [ + '[permissions]', + '%s = admin' % tests.UID2, ])) class Document(db.Resource): @@ -175,16 +177,16 @@ class NodeTest(tests.Test): return value volume = self.start_master([User, Document]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) volume['user'].create({'guid': tests.UID2, 'name': 'user', 'pubkey': tests.PUBKEY2}) - guid = this.call(method='POST', path=['document'], principal=tests.UID, content={'prop': '1'}) + guid = this.call(method='POST', path=['document'], environ=auth_env(tests.UID), content={'prop': '1'}) - this.call(method='PUT', path=['document', guid], content={'prop': '2'}, principal=tests.UID) + this.call(method='PUT', path=['document', guid], content={'prop': '2'}, environ=auth_env(tests.UID)) self.assertEqual('2', volume['document'].get(guid)['prop']) - this.call(method='PUT', path=['document', guid], content={'prop': '3'}, principal=tests.UID2) + this.call(method='PUT', path=['document', guid], content={'prop': '3'}, environ=auth_env(tests.UID2)) self.assertEqual('3', volume['document'].get(guid)['prop']) def test_authorize_LiveConfigUpdates(self): @@ -199,15 +201,16 @@ class NodeTest(tests.Test): pass volume = self.start_master([User], Routes) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - self.assertRaises(http.Forbidden, this.call, method='PROBE', principal=tests.UID) - self.touch(('authorization.conf', [ - '[%s]' % tests.UID, - 'root = True', + self.assertRaises(http.Forbidden, this.call, method='PROBE', environ=auth_env(tests.UID)) + self.touch(('master/etc/authorization.conf', [ + '[permissions]', + '%s = admin' % tests.UID, ])) - this.call(method='PROBE', principal=tests.UID) + self.node_routes._auth.reload() + this.call(method='PROBE', environ=auth_env(tests.UID)) def test_authorize_Anonymous(self): @@ -225,25 +228,41 @@ class NodeTest(tests.Test): pass volume = self.start_master([User], Routes) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) self.assertRaises(http.Unauthorized, this.call, method='PROBE1') - self.assertRaises(http.Forbidden, this.call, method='PROBE2') + self.assertRaises(http.Unauthorized, this.call, method='PROBE2') - self.touch(('authorization.conf', [ - '[anonymous]', - 'user = True', - 'root = True', + def test_authorize_DefaultPermissions(self): + + class Routes(NodeRoutes): + + def __init__(self, **kwargs): + NodeRoutes.__init__(self, 'node', **kwargs) + + @route('PROBE', acl=ACL.SUPERUSER) + def probe(self, request): + pass + + volume = self.start_master([User], Routes) + conn = Connection(creds=SugarCreds(keyfile.value)) + volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) + + self.assertRaises(http.Forbidden, this.call, method='PROBE', environ=auth_env(tests.UID)) + + self.touch(('master/etc/authorization.conf', [ + '[permissions]', + 'default = admin', ])) - this.call(method='PROBE1') - this.call(method='PROBE2') + self.node_routes._auth.reload() + this.call(method='PROBE', environ=auth_env(tests.UID)) def test_SetUser(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - guid = this.call(method='POST', path=['context'], principal=tests.UID, content={ + guid = this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'type': 'activity', 'title': 'title', 'summary': 'summary', @@ -255,22 +274,22 @@ class NodeTest(tests.Test): def test_find_MaxLimit(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - this.call(method='POST', path=['context'], principal=tests.UID, content={ + this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'type': 'activity', 'title': 'title1', 'summary': 'summary', 'description': 'description', }) - this.call(method='POST', path=['context'], principal=tests.UID, content={ + this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'type': 'activity', 'title': 'title2', 'summary': 'summary', 'description': 'description', }) - this.call(method='POST', path=['context'], principal=tests.UID, content={ + this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'type': 'activity', 'title': 'title3', 'summary': 'summary', @@ -286,10 +305,10 @@ class NodeTest(tests.Test): def test_DeletedDocuments(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - guid = this.call(method='POST', path=['context'], principal=tests.UID, content={ + guid = this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'type': 'activity', 'title': 'title1', 'summary': 'summary', @@ -310,55 +329,55 @@ class NodeTest(tests.Test): def test_CreateGUID(self): # TODO Temporal security hole, see TODO volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - this.call(method='POST', path=['context'], principal=tests.UID, content={ + this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'guid': 'foo', 'type': 'activity', 'title': 'title', 'summary': 'summary', 'description': 'description', - }) + }, principal=Admin('admin')) self.assertEqual( {'guid': 'foo', 'title': 'title'}, this.call(method='GET', path=['context', 'foo'], reply=['guid', 'title'])) def test_CreateMalformedGUID(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - self.assertRaises(http.BadRequest, this.call, method='POST', path=['context'], principal=tests.UID, content={ + self.assertRaises(http.BadRequest, this.call, method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'guid': '!?', 'type': 'activity', 'title': 'title', 'summary': 'summary', 'description': 'description', - }) + }, principal=Admin('admin')) def test_FailOnExistedGUID(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user', 'pubkey': tests.PUBKEY}) - guid = this.call(method='POST', path=['context'], principal=tests.UID, content={ + guid = this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'type': 'activity', 'title': 'title', 'summary': 'summary', 'description': 'description', }) - self.assertRaises(RuntimeError, this.call, method='POST', path=['context'], principal=tests.UID, content={ + self.assertRaises(RuntimeError, this.call, method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'guid': guid, 'type': 'activity', 'title': 'title', 'summary': 'summary', 'description': 'description', - }) + }, principal=Admin('admin')) def test_PackagesRoute(self): volume = self.start_master() - client = Connection(auth=http.SugarAuth(keyfile.value)) + client = Connection(creds=SugarCreds(keyfile.value)) self.touch(('master/files/packages/repo/arch/package', 'file')) volume.blobs.populate() @@ -370,7 +389,7 @@ class NodeTest(tests.Test): def test_PackageUpdatesRoute(self): volume = self.start_master() - ipc = Connection(auth=http.SugarAuth(keyfile.value)) + ipc = Connection(creds=SugarCreds(keyfile.value)) self.touch('master/files/packages/repo/1', 'master/files/packages/repo/1.1') volume.blobs.populate() @@ -407,7 +426,7 @@ class NodeTest(tests.Test): def test_release(self): volume = self.start_master() - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) activity_info = '\n'.join([ '[Activity]', @@ -429,8 +448,8 @@ class NodeTest(tests.Test): self.assertEqual({ release: { - 'seqno': 9, - 'author': {tests.UID: {'name': tests.UID, 'order': 0, 'role': 3}}, + 'seqno': 8, + 'author': {tests.UID: {'name': 'test', 'order': 0, 'role': 3}}, 'value': { 'license': ['Public Domain'], 'announce': announce, @@ -441,7 +460,7 @@ class NodeTest(tests.Test): 'stability': 'developer', }, }, - }, conn.get(['context', 'bundle_id', 'releases'])) + }, volume['context']['bundle_id']['releases']) post = volume['post'][announce] assert tests.UID in post['author'] @@ -457,7 +476,7 @@ class NodeTest(tests.Test): def test_Solve(self): volume = self.start_master() - conn = http.Connection(api.value, http.SugarAuth(keyfile.value)) + conn = http.Connection(api.value, SugarCreds(keyfile.value)) activity_unpack = '\n'.join([ '[Activity]', @@ -484,13 +503,13 @@ class NodeTest(tests.Test): dep_pack = self.zips(('topdir/activity/activity.info', dep_unpack)) dep_blob = json.load(conn.request('POST', ['context'], dep_pack, params={'cmd': 'submit', 'initial': True}).raw) - this.call(method='POST', path=['context'], principal=tests.UID, content={ + this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'guid': 'package', 'type': 'package', 'title': 'title', 'summary': 'summary', 'description': 'description', - }) + }, principal=Admin('admin')) conn.put(['context', 'package', 'releases', '*'], {'binary': ['package.bin']}) self.assertEqual({ @@ -520,7 +539,7 @@ class NodeTest(tests.Test): def test_SolveWithArguments(self): volume = self.start_master() - conn = http.Connection(api.value, http.SugarAuth(keyfile.value)) + conn = http.Connection(api.value, SugarCreds(keyfile.value)) activity_unpack = '\n'.join([ '[Activity]', @@ -560,13 +579,13 @@ class NodeTest(tests.Test): dep_pack = self.zips(('topdir/activity/activity.info', dep_unpack)) dep_blob = json.load(conn.request('POST', ['context'], dep_pack, params={'cmd': 'submit', 'initial': True}).raw) - this.call(method='POST', path=['context'], principal=tests.UID, content={ + this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'guid': 'package', 'type': 'package', 'title': 'title', 'summary': 'summary', 'description': 'description', - }) + }, principal=Admin('admin')) volume['context'].update('package', {'releases': { 'resolves': { 'Ubuntu-10.04': {'version': [[1], 0], 'packages': ['package.bin']}, @@ -601,7 +620,7 @@ class NodeTest(tests.Test): def test_Resolve(self): volume = self.start_master() - conn = http.Connection(api.value, http.SugarAuth(keyfile.value)) + conn = http.Connection(api.value, SugarCreds(keyfile.value)) activity_info = '\n'.join([ '[Activity]', @@ -626,13 +645,13 @@ class NodeTest(tests.Test): 'license = Public Domain', ]))), params={'cmd': 'submit', 'initial': True}).raw) - this.call(method='POST', path=['context'], principal=tests.UID, content={ + this.call(method='POST', path=['context'], environ=auth_env(tests.UID), content={ 'guid': 'package', 'type': 'package', 'title': 'title', 'summary': 'summary', 'description': 'description', - }) + }, principal=Admin('admin')) conn.put(['context', 'package', 'releases', '*'], {'binary': ['package.bin']}) response = Response() @@ -652,27 +671,27 @@ class NodeTest(tests.Test): return value volume = self.start_master([Document, User]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user1', 'pubkey': tests.PUBKEY}) volume['user'].create({'guid': tests.UID2, 'name': 'user2', 'pubkey': tests.PUBKEY2}) - guid = this.call(method='POST', path=['document'], principal=tests.UID, content={}) + guid = this.call(method='POST', path=['document'], environ=auth_env(tests.UID), content={}) self.override(time, 'time', lambda: 0) - agg1 = this.call(method='POST', path=['document', guid, 'prop1'], principal=tests.UID) - agg2 = this.call(method='POST', path=['document', guid, 'prop1'], principal=tests.UID2) + agg1 = this.call(method='POST', path=['document', guid, 'prop1'], environ=auth_env(tests.UID)) + agg2 = this.call(method='POST', path=['document', guid, 'prop1'], environ=auth_env(tests.UID2)) self.assertEqual({ agg1: {'seqno': 4, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}, 'value': None}, agg2: {'seqno': 5, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 1}}, 'value': None}, }, - this.call(method='GET', path=['document', guid, 'prop1'])) + volume['document'][guid]['prop1']) - agg3 = this.call(method='POST', path=['document', guid, 'prop2'], principal=tests.UID) - self.assertRaises(http. Forbidden, this.call, method='POST', path=['document', guid, 'prop2'], principal=tests.UID2) + agg3 = this.call(method='POST', path=['document', guid, 'prop2'], environ=auth_env(tests.UID)) + self.assertRaises(http. Forbidden, this.call, method='POST', path=['document', guid, 'prop2'], environ=auth_env(tests.UID2)) self.assertEqual({ agg3: {'seqno': 6, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}, 'value': None}, }, - this.call(method='GET', path=['document', guid, 'prop2'])) + volume['document'][guid]['prop2']) def test_AggpropRemoveAccess(self): @@ -687,57 +706,72 @@ class NodeTest(tests.Test): return value volume = self.start_master([Document, User]) - conn = Connection(auth=http.SugarAuth(keyfile.value)) + conn = Connection(creds=SugarCreds(keyfile.value)) volume['user'].create({'guid': tests.UID, 'name': 'user1', 'pubkey': tests.PUBKEY}) volume['user'].create({'guid': tests.UID2, 'name': 'user2', 'pubkey': tests.PUBKEY2}) - guid = this.call(method='POST', path=['document'], principal=tests.UID, content={}) + guid = this.call(method='POST', path=['document'], environ=auth_env(tests.UID), content={}) self.override(time, 'time', lambda: 0) - agg1 = this.call(method='POST', path=['document', guid, 'prop1'], principal=tests.UID, content=True) - agg2 = this.call(method='POST', path=['document', guid, 'prop1'], principal=tests.UID2, content=True) + agg1 = this.call(method='POST', path=['document', guid, 'prop1'], environ=auth_env(tests.UID), content=True) + agg2 = this.call(method='POST', path=['document', guid, 'prop1'], environ=auth_env(tests.UID2), content=True) self.assertEqual({ agg1: {'seqno': 4, 'value': True, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}}, agg2: {'seqno': 5, 'value': True, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 1}}}, }, - this.call(method='GET', path=['document', guid, 'prop1'])) - self.assertRaises(http.Forbidden, this.call, method='DELETE', path=['document', guid, 'prop1', agg1], principal=tests.UID2) - self.assertRaises(http.Forbidden, this.call, method='DELETE', path=['document', guid, 'prop1', agg2], principal=tests.UID) + volume['document'][guid]['prop1']) + self.assertRaises(http.Forbidden, this.call, method='DELETE', path=['document', guid, 'prop1', agg1], environ=auth_env(tests.UID2)) + self.assertRaises(http.Forbidden, this.call, method='DELETE', path=['document', guid, 'prop1', agg2], environ=auth_env(tests.UID)) self.assertEqual({ agg1: {'seqno': 4, 'value': True, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}}, agg2: {'seqno': 5, 'value': True, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 1}}}, }, - this.call(method='GET', path=['document', guid, 'prop1'])) + volume['document'][guid]['prop1']) - this.call(method='DELETE', path=['document', guid, 'prop1', agg1], principal=tests.UID) + this.call(method='DELETE', path=['document', guid, 'prop1', agg1], environ=auth_env(tests.UID)) self.assertEqual({ agg1: {'seqno': 6, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}}, agg2: {'seqno': 5, 'value': True, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 1}}}, }, - this.call(method='GET', path=['document', guid, 'prop1'])) - this.call(method='DELETE', path=['document', guid, 'prop1', agg2], principal=tests.UID2) + volume['document'][guid]['prop1']) + this.call(method='DELETE', path=['document', guid, 'prop1', agg2], environ=auth_env(tests.UID2)) self.assertEqual({ agg1: {'seqno': 6, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}}, agg2: {'seqno': 7, 'author': {tests.UID2: {'name': 'user2', 'order': 0, 'role': 1}}}, }, - this.call(method='GET', path=['document', guid, 'prop1'])) + volume['document'][guid]['prop1']) - agg3 = this.call(method='POST', path=['document', guid, 'prop2'], principal=tests.UID, content=True) + agg3 = this.call(method='POST', path=['document', guid, 'prop2'], environ=auth_env(tests.UID), content=True) self.assertEqual({ agg3: {'seqno': 8, 'value': True, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}}, }, - this.call(method='GET', path=['document', guid, 'prop2'])) + volume['document'][guid]['prop2']) - self.assertRaises(http.Forbidden, this.call, method='DELETE', path=['document', guid, 'prop2', agg3], principal=tests.UID2) + self.assertRaises(http.Forbidden, this.call, method='DELETE', path=['document', guid, 'prop2', agg3], environ=auth_env(tests.UID2)) self.assertEqual({ agg3: {'seqno': 8, 'value': True, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}}, }, - this.call(method='GET', path=['document', guid, 'prop2'])) - this.call(method='DELETE', path=['document', guid, 'prop2', agg3], principal=tests.UID) + volume['document'][guid]['prop2']) + this.call(method='DELETE', path=['document', guid, 'prop2', agg3], environ=auth_env(tests.UID)) self.assertEqual({ agg3: {'seqno': 9, 'author': {tests.UID: {'name': 'user1', 'order': 0, 'role': 3}}}, }, - this.call(method='GET', path=['document', guid, 'prop2'])) + volume['document'][guid]['prop2']) + + +def auth_env(uid): + key = RSA.load_key(join(tests.root, 'data', uid)) + nonce = int(time.time() + 2) + data = hashlib.sha1('%s:%s' % (uid, nonce)).digest() + signature = key.sign(data).encode('hex') + authorization = 'Sugar username="%s",nonce="%s",signature="%s"' % \ + (uid, nonce, signature) + return {'HTTP_AUTHORIZATION': authorization} + + +class Admin(Principal): + + admin = True if __name__ == '__main__': diff --git a/tests/units/node/slave.py b/tests/units/node/slave.py index 3afab04..2b32b70 100755 --- a/tests/units/node/slave.py +++ b/tests/units/node/slave.py @@ -11,9 +11,11 @@ from __init__ import tests from sugar_network import db, toolkit from sugar_network.client import Connection, keyfile +from sugar_network.client.auth import SugarCreds from sugar_network.node import master_api from sugar_network.node.master import MasterRoutes from sugar_network.node.slave import SlaveRoutes +from sugar_network.node.auth import SugarAuth from sugar_network.node.model import User from sugar_network.db.volume import Volume from sugar_network.toolkit.router import Router, File @@ -49,15 +51,15 @@ class SlaveTest(tests.Test): self.Document = Document self.slave_volume = Volume('slave', [User, Document]) - self.slave_routes = SlaveRoutes(volume=self.slave_volume) + self.slave_routes = SlaveRoutes(volume=self.slave_volume, auth=SugarAuth('slave')) self.slave_server = coroutine.WSGIServer(('127.0.0.1', 8888), Router(self.slave_routes)) coroutine.spawn(self.slave_server.serve_forever) coroutine.dispatch() def test_online_sync_Push(self): self.fork_master([User, self.Document]) - master = Connection('http://127.0.0.1:7777', auth=http.SugarAuth(keyfile.value)) - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + master = Connection('http://127.0.0.1:7777', creds=SugarCreds(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) slave.post(cmd='online_sync') self.assertEqual([[1, None]], json.load(file('slave/var/pull.ranges'))) @@ -73,7 +75,7 @@ class SlaveTest(tests.Test): ], master.get(['document'], reply=['guid', 'message'])['result']) self.assertEqual([[2, None]], json.load(file('slave/var/pull.ranges'))) - self.assertEqual([[5, None]], json.load(file('slave/var/push.ranges'))) + self.assertEqual([[4, None]], json.load(file('slave/var/push.ranges'))) guid3 = slave.post(['document'], {'message': '3', 'title': ''}) slave.post(cmd='online_sync') @@ -84,14 +86,14 @@ class SlaveTest(tests.Test): ], master.get(['document'], reply=['guid', 'message'])['result']) self.assertEqual([[3, None]], json.load(file('slave/var/pull.ranges'))) - self.assertEqual([[6, None]], json.load(file('slave/var/push.ranges'))) + self.assertEqual([[5, None]], json.load(file('slave/var/push.ranges'))) coroutine.sleep(1) slave.put(['document', guid2], {'message': '22'}) slave.post(cmd='online_sync') self.assertEqual('22', master.get(['document', guid2, 'message'])) self.assertEqual([[4, None]], json.load(file('slave/var/pull.ranges'))) - self.assertEqual([[7, None]], json.load(file('slave/var/push.ranges'))) + self.assertEqual([[6, None]], json.load(file('slave/var/push.ranges'))) coroutine.sleep(1) slave.delete(['document', guid1]) @@ -102,7 +104,7 @@ class SlaveTest(tests.Test): ], master.get(['document'], reply=['guid', 'message'])['result']) self.assertEqual([[5, None]], json.load(file('slave/var/pull.ranges'))) - self.assertEqual([[8, None]], json.load(file('slave/var/push.ranges'))) + self.assertEqual([[7, None]], json.load(file('slave/var/push.ranges'))) coroutine.sleep(1) slave.put(['document', guid1], {'message': 'a'}) @@ -111,18 +113,18 @@ class SlaveTest(tests.Test): guid4 = slave.post(['document'], {'message': 'd', 'title': ''}) slave.delete(['document', guid2]) slave.post(cmd='online_sync') - self.assertEqual([ + self.assertEqual(sorted([ {'guid': guid3, 'message': 'c'}, {'guid': guid4, 'message': 'd'}, - ], - master.get(['document'], reply=['guid', 'message'])['result']) + ]), + sorted(master.get(['document'], reply=['guid', 'message'])['result'])) self.assertEqual([[6, None]], json.load(file('slave/var/pull.ranges'))) - self.assertEqual([[13, None]], json.load(file('slave/var/push.ranges'))) + self.assertEqual([[12, None]], json.load(file('slave/var/push.ranges'))) def test_online_sync_Pull(self): self.fork_master([User, self.Document]) - master = Connection('http://127.0.0.1:7777', auth=http.SugarAuth(keyfile.value)) - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + master = Connection('http://127.0.0.1:7777', creds=SugarCreds(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) slave.post(cmd='online_sync') self.assertEqual([[1, None]], json.load(file('slave/var/pull.ranges'))) @@ -137,7 +139,7 @@ class SlaveTest(tests.Test): {'guid': guid2, 'message': '2'}, ], slave.get(['document'], reply=['guid', 'message'])['result']) - self.assertEqual([[5, None]], json.load(file('slave/var/pull.ranges'))) + self.assertEqual([[4, None]], json.load(file('slave/var/pull.ranges'))) self.assertEqual([[2, None]], json.load(file('slave/var/push.ranges'))) guid3 = master.post(['document'], {'message': '3', 'title': ''}) @@ -148,14 +150,14 @@ class SlaveTest(tests.Test): {'guid': guid3, 'message': '3'}, ], slave.get(['document'], reply=['guid', 'message'])['result']) - self.assertEqual([[6, None]], json.load(file('slave/var/pull.ranges'))) + self.assertEqual([[5, None]], json.load(file('slave/var/pull.ranges'))) self.assertEqual([[3, None]], json.load(file('slave/var/push.ranges'))) coroutine.sleep(1) master.put(['document', guid2], {'message': '22'}) slave.post(cmd='online_sync') self.assertEqual('22', slave.get(['document', guid2, 'message'])) - self.assertEqual([[7, None]], json.load(file('slave/var/pull.ranges'))) + self.assertEqual([[6, None]], json.load(file('slave/var/pull.ranges'))) self.assertEqual([[4, None]], json.load(file('slave/var/push.ranges'))) coroutine.sleep(1) @@ -166,7 +168,7 @@ class SlaveTest(tests.Test): {'guid': guid3, 'message': '3'}, ], slave.get(['document'], reply=['guid', 'message'])['result']) - self.assertEqual([[8, None]], json.load(file('slave/var/pull.ranges'))) + self.assertEqual([[7, None]], json.load(file('slave/var/pull.ranges'))) self.assertEqual([[5, None]], json.load(file('slave/var/push.ranges'))) coroutine.sleep(1) @@ -181,13 +183,13 @@ class SlaveTest(tests.Test): {'guid': guid4, 'message': 'd'}, ], slave.get(['document'], reply=['guid', 'message'])['result']) - self.assertEqual([[13, None]], json.load(file('slave/var/pull.ranges'))) + self.assertEqual([[12, None]], json.load(file('slave/var/pull.ranges'))) self.assertEqual([[6, None]], json.load(file('slave/var/push.ranges'))) def test_online_sync_PullBlobs(self): self.fork_master([User, self.Document]) - master = Connection('http://127.0.0.1:7777', auth=http.SugarAuth(keyfile.value)) - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + master = Connection('http://127.0.0.1:7777', creds=SugarCreds(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) slave.post(cmd='online_sync') self.assertEqual([[1, None]], json.load(file('slave/var/pull.ranges'))) @@ -203,8 +205,8 @@ class SlaveTest(tests.Test): def test_online_sync_PullFromPreviouslyMergedRecord(self): self.fork_master([User, self.Document]) - master = Connection('http://127.0.0.1:7777', auth=http.SugarAuth(keyfile.value)) - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + master = Connection('http://127.0.0.1:7777', creds=SugarCreds(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) slave.post(cmd='online_sync') self.assertEqual([[1, None]], json.load(file('slave/var/pull.ranges'))) @@ -224,7 +226,7 @@ class SlaveTest(tests.Test): self.assertEqual('1_', slave.get(['document', guid, 'title'])) def test_offline_sync_Import(self): - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) self.touch(('blob1', 'a')) self.touch(('blob2', 'bb')) @@ -276,10 +278,10 @@ class SlaveTest(tests.Test): ({'from': self.slave_routes.guid, 'packet': 'pull', 'ranges': [[3, 100], [104, None]], 'to': '127.0.0.1:7777'}, [ ]), ]), - sorted([(packet.header, [i for i in packet]) for packet in parcel.decode_dir('sync')])) + sorted([(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode_dir('sync')])) def test_offline_sync_ImportPush(self): - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) self.touch(('blob1', 'a')) self.touch(('blob2', 'bb')) @@ -328,10 +330,10 @@ class SlaveTest(tests.Test): ({'from': self.slave_routes.guid, 'packet': 'pull', 'ranges': [[3, None]], 'to': '127.0.0.1:7777'}, [ ]), ]), - sorted([(packet.header, [dict(i) for i in packet]) for packet in parcel.decode_dir('sync')])) + sorted([(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode_dir('sync')])) def test_offline_sync_ImportAck(self): - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) parcel.encode_dir([ ('ack', {'ack': [[101, 103]], 'ranges': [[1, 3]], 'from': '127.0.0.1:7777', 'to': self.slave_routes.guid}, []), @@ -351,10 +353,10 @@ class SlaveTest(tests.Test): ({'from': self.slave_routes.guid, 'packet': 'pull', 'ranges': [[1, 100], [104, None]], 'to': '127.0.0.1:7777'}, [ ]), ]), - sorted([(packet.header, [i for i in packet]) for packet in parcel.decode_dir('sync')])) + sorted([(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode_dir('sync')])) def test_offline_sync_GenerateRequestAfterImport(self): - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) parcel.encode_dir([ ('push', {'from': 'another-slave'}, [ @@ -397,10 +399,10 @@ class SlaveTest(tests.Test): ({'from': self.slave_routes.guid, 'packet': 'pull', 'ranges': [[1, None]], 'to': '127.0.0.1:7777'}, [ ]), ]), - sorted([(packet.header, [i for i in packet]) for packet in parcel.decode_dir('sync')])) + sorted([(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode_dir('sync')])) def test_offline_sync_Export(self): - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) class statvfs(object): @@ -436,10 +438,10 @@ class SlaveTest(tests.Test): ({'from': self.slave_routes.guid, 'packet': 'pull', 'ranges': [[1, None]], 'to': '127.0.0.1:7777'}, [ ]), ]), - sorted([(packet.header, [i for i in packet]) for packet in parcel.decode_dir('sync')])) + sorted([(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode_dir('sync')])) def test_offline_sync_ContinuousExport(self): - slave = Connection('http://127.0.0.1:8888', auth=http.SugarAuth(keyfile.value)) + slave = Connection('http://127.0.0.1:8888', creds=SugarCreds(keyfile.value)) class statvfs(object): @@ -473,7 +475,7 @@ class SlaveTest(tests.Test): ({'from': self.slave_routes.guid, 'packet': 'pull', 'ranges': [[1, None]], 'to': '127.0.0.1:7777'}, [ ]), ]), - sorted([(packet.header, [i for i in packet]) for packet in parcel.decode_dir('sync')])) + sorted([(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode_dir('sync')])) slave.post(cmd='offline_sync', path=tests.tmpdir + '/sync') self.assertEqual( @@ -500,7 +502,7 @@ class SlaveTest(tests.Test): ({'from': self.slave_routes.guid, 'packet': 'pull', 'ranges': [[1, None]], 'to': '127.0.0.1:7777'}, [ ]), ]), - sorted([(packet.header, [i for i in packet]) for packet in parcel.decode_dir('sync')])) + sorted([(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode_dir('sync')])) slave.post(cmd='offline_sync', path=tests.tmpdir + '/sync') self.assertEqual( @@ -533,7 +535,7 @@ class SlaveTest(tests.Test): ({'from': self.slave_routes.guid, 'packet': 'pull', 'ranges': [[1, None]], 'to': '127.0.0.1:7777'}, [ ]), ]), - sorted([(packet.header, [i for i in packet]) for packet in parcel.decode_dir('sync')])) + sorted([(packet.header, [i.meta if isinstance(i, File) else i for i in packet]) for packet in parcel.decode_dir('sync')])) if __name__ == '__main__': diff --git a/tests/units/toolkit/http.py b/tests/units/toolkit/http.py index 2ac3cab..cdf9198 100755 --- a/tests/units/toolkit/http.py +++ b/tests/units/toolkit/http.py @@ -8,6 +8,7 @@ from __init__ import tests from sugar_network import client as local from sugar_network.toolkit.router import route, Router, Request, Response +from sugar_network.toolkit.coroutine import this from sugar_network.toolkit import coroutine, http @@ -59,8 +60,8 @@ class HTTPTest(tests.Test): class Commands(object): @route('FOO', [None, None], cmd='f1', mime_type='application/json') - def f1(self, request): - return request.path + def f1(self): + return this.request.path self.server = coroutine.WSGIServer(('127.0.0.1', local.ipc_port.value), Router(Commands())) coroutine.spawn(self.server.serve_forever) diff --git a/tests/units/toolkit/parcel.py b/tests/units/toolkit/parcel.py index 4f57c44..1a24a3f 100755 --- a/tests/units/toolkit/parcel.py +++ b/tests/units/toolkit/parcel.py @@ -191,13 +191,13 @@ class ParcelTest(tests.Test): (1, hashlib.sha1('a').hexdigest(), 'a'), (2, hashlib.sha1('bb').hexdigest(), 'bb'), ], - [(i['num'], i.digest, file(i.path).read()) for i in packet]) + [(i.meta['num'], i.digest, file(i.path).read()) for i in packet]) with next(packets_iter) as packet: self.assertEqual(2, packet.name) self.assertEqual([ (3, hashlib.sha1('ccc').hexdigest(), 'ccc'), ], - [(i['num'], i.digest, file(i.path).read()) for i in packet]) + [(i.meta['num'], i.digest, file(i.path).read()) for i in packet]) self.assertRaises(StopIteration, packets_iter.next) self.assertEqual(len(stream.getvalue()), stream.tell()) @@ -221,13 +221,13 @@ class ParcelTest(tests.Test): (1, 'a'), (2, ''), ], - [(i['num'], file(i.path).read()) for i in packet]) + [(i.meta['num'], file(i.path).read()) for i in packet]) with next(packets_iter) as packet: self.assertEqual(2, packet.name) self.assertEqual([ (3, 'ccc'), ], - [(i['num'], file(i.path).read()) for i in packet]) + [(i.meta['num'], file(i.path).read()) for i in packet]) self.assertRaises(StopIteration, packets_iter.next) self.assertEqual(len(stream.getvalue()), stream.tell()) @@ -247,10 +247,10 @@ class ParcelTest(tests.Test): packets_iter = parcel.decode(stream) with next(packets_iter) as packet: self.assertEqual(1, packet.name) - self.assertEqual([1, 2], [i['num'] for i in packet]) + self.assertEqual([1, 2], [i.meta['num'] for i in packet]) with next(packets_iter) as packet: self.assertEqual(2, packet.name) - self.assertEqual([3], [i['num'] for i in packet]) + self.assertEqual([3], [i.meta['num'] for i in packet]) self.assertRaises(StopIteration, packets_iter.next) self.assertEqual(len(stream.getvalue()), stream.tell()) @@ -693,7 +693,7 @@ class ParcelTest(tests.Test): self.assertEqual({'packet': 2}, packet.header) items = iter(packet) blob = next(items) - self.assertEqual({'num': 2, 'content-length': '8'}, blob) + self.assertEqual({'num': 2, 'content-length': '8'}, blob.meta) self.assertEqual('content2', file(blob.path).read()) self.assertEqual({'payload': 3}, next(items)) self.assertRaises(StopIteration, items.next) @@ -703,7 +703,7 @@ class ParcelTest(tests.Test): items = iter(packet) self.assertEqual({'payload': 1}, next(items)) blob = next(items) - self.assertEqual({'num': 1, 'content-length': '8'}, blob) + self.assertEqual({'num': 1, 'content-length': '8'}, blob.meta) self.assertEqual('content1', file(blob.path).read()) self.assertEqual({'payload': 2}, next(items)) self.assertRaises(StopIteration, items.next) diff --git a/tests/units/toolkit/router.py b/tests/units/toolkit/router.py index 61d8dff..e9ee798 100755 --- a/tests/units/toolkit/router.py +++ b/tests/units/toolkit/router.py @@ -430,8 +430,8 @@ class RouterTest(tests.Test): class Routes(object): @fallbackroute('PROBE', ['static']) - def fallback(self, request): - return '/'.join(request.path) + def fallback(self): + return '/'.join(this.request.path) router = Router(Routes()) status = [] @@ -504,30 +504,30 @@ class RouterTest(tests.Test): class A(object): @route('PROBE') - def ok(self, request, response): - return request['probe'] + def ok(self): + return this.request['probe'] @preroute - def _(self, op, request, response): - request['probe'] = '_' + def _(self, op): + this.request['probe'] = '_' class B1(A): @preroute - def z(self, op, request, response): - request['probe'] += 'z' + def z(self, op): + this.request['probe'] += 'z' class B2(object): @preroute - def f(self, op, request, response): - request['probe'] += 'f' + def f(self, op): + this.request['probe'] += 'f' class C(B1, B2): @preroute - def a(self, op, request, response): - request['probe'] += 'a' + def a(self, op): + this.request['probe'] += 'a' router = Router(C()) @@ -545,30 +545,29 @@ class RouterTest(tests.Test): return 'ok' @route('FAIL') - def fail(self, request, response): + def fail(self): raise Exception('fail') @postroute - def _(self, request, response, result, exception): - print exception + def _(self, result, exception): postroutes.append(('_', result, str(exception))) class B1(A): @postroute - def z(self, request, response, result, exception): + def z(self, result, exception): postroutes.append(('z', result, str(exception))) class B2(object): @postroute - def f(self, request, response, result, exception): + def f(self, result, exception): postroutes.append(('f', result, str(exception))) class C(B1, B2): @postroute - def a(self, request, response, result, exception): + def a(self, result, exception): postroutes.append(('a', result, str(exception))) router = Router(C()) @@ -883,7 +882,7 @@ class RouterTest(tests.Test): class CommandsProcessor(object): @route('GET') - def get_stream(self, response): + def get_stream(self): return StringIO('stream') router = Router(CommandsProcessor()) @@ -900,15 +899,15 @@ class RouterTest(tests.Test): class CommandsProcessor(object): @route('GET', [], '1', mime_type='application/octet-stream') - def get_binary(self, response): + def get_binary(self): pass @route('GET', [], '2', mime_type='application/json') - def get_json(self, response): + def get_json(self): pass @route('GET', [], '3') - def no_get(self, response): + def no_get(self): pass router = Router(CommandsProcessor()) @@ -946,7 +945,7 @@ class RouterTest(tests.Test): class CommandsProcessor(object): @route('GET') - def get(self, response): + def get(self): raise Status('Status-Error') router = Router(CommandsProcessor()) @@ -973,7 +972,7 @@ class RouterTest(tests.Test): class CommandsProcessor(object): @route('HEAD') - def get(self, response): + def get(self): raise Status('Status-Error') router = Router(CommandsProcessor()) @@ -1001,7 +1000,7 @@ class RouterTest(tests.Test): class CommandsProcessor(object): @route('GET') - def get(self, response): + def get(self): raise StatusPass('Status-Error') router = Router(CommandsProcessor()) @@ -1026,7 +1025,7 @@ class RouterTest(tests.Test): class CommandsProcessor(object): @route('GET') - def get(self, response): + def get(self): return File(None, meta=[('location', URL)]) router = Router(CommandsProcessor()) @@ -1045,13 +1044,36 @@ class RouterTest(tests.Test): ], response) + def test_MissedFiles(self): + + class CommandsProcessor(object): + + @route('GET') + def get(self): + return File.AWAY + + router = Router(CommandsProcessor()) + + response = [] + reply = router({ + 'PATH_INFO': '/', + 'REQUEST_METHOD': 'GET', + }, + lambda status, headers: response.extend([status, dict(headers)])) + self.assertEqual({ + 'request': '/', + 'error': 'No such file', + }, + json.loads(''.join([i for i in reply]))) + self.assertEqual('404 Not Found', response[0]) + def test_LastModified(self): class CommandsProcessor(object): @route('GET') - def get(self, request, response): - response.last_modified = 10 + def get(self): + this.response.last_modified = 10 return 'ok' router = Router(CommandsProcessor()) @@ -1075,8 +1097,8 @@ class RouterTest(tests.Test): class CommandsProcessor(object): @route('GET') - def get(self, request): - if not request.if_modified_since or request.if_modified_since >= 10: + def get(self): + if not this.request.if_modified_since or this.request.if_modified_since >= 10: return 'ok' else: raise http.NotModified() @@ -1256,8 +1278,8 @@ class RouterTest(tests.Test): class CommandsProcessor(object): @route('HEAD', []) - def head(self, request, response): - response.content_length = 100 + def head(self): + this.response.content_length = 100 router = Router(CommandsProcessor()) @@ -1368,9 +1390,9 @@ class RouterTest(tests.Test): class Routes(object): @route('GET', mime_type='text/event-stream') - def get(self, request): + def get(self): yield {'event': 'probe'} - yield {'event': 'probe', 'request': request.content} + yield {'event': 'probe', 'request': this.request.content} events = [] def localcast(event): @@ -1400,9 +1422,9 @@ class RouterTest(tests.Test): class Routes(object): @route('HEAD') - def probe(self, request, response): - response['фоо'] = 'бар' - response[u'йцу'] = u'кен' + def probe(self): + this.response['фоо'] = 'бар' + this.response[u'йцу'] = u'кен' server = coroutine.WSGIServer(('127.0.0.1', client.ipc_port.value), Router(Routes())) coroutine.spawn(server.serve_forever) @@ -1447,7 +1469,7 @@ class RouterTest(tests.Test): 'content-type': 'foo/bar', 'content-disposition': 'attachment; filename="foo"', }, - dict(blob)) + blob.meta) def test_SetCookie(self): |