Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksey Lim <alsroot@sugarlabs.org>2014-04-08 11:49:53 (GMT)
committer Aleksey Lim <alsroot@sugarlabs.org>2014-04-14 16:06:48 (GMT)
commit71391e654f497234fac0a4602bba769820aa521c (patch)
tree2b5d6d66a4b23f28581adc4079a1aa28f3907407
parent6ec16441c7c133c55385613f1e430c5ea37af632 (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.
-rw-r--r--TODO3
-rwxr-xr-xsugar-network-client15
-rwxr-xr-xsugar-network-node14
-rw-r--r--sugar_network/client/auth.py112
-rw-r--r--sugar_network/client/injector.py2
-rw-r--r--sugar_network/client/journal.py34
-rw-r--r--sugar_network/client/model.py15
-rw-r--r--sugar_network/client/routes.py213
-rw-r--r--sugar_network/db/__init__.py4
-rw-r--r--sugar_network/db/blobs.py8
-rw-r--r--sugar_network/db/directory.py10
-rw-r--r--sugar_network/db/index.py4
-rw-r--r--sugar_network/db/metadata.py5
-rw-r--r--sugar_network/db/resource.py13
-rw-r--r--sugar_network/db/routes.py166
-rw-r--r--sugar_network/db/volume.py46
-rw-r--r--sugar_network/model/__init__.py40
-rw-r--r--sugar_network/model/context.py19
-rw-r--r--sugar_network/model/routes.py52
-rw-r--r--sugar_network/model/user.py2
-rw-r--r--sugar_network/node/auth.py118
-rw-r--r--sugar_network/node/master.py4
-rw-r--r--sugar_network/node/model.py37
-rw-r--r--sugar_network/node/obs.py2
-rw-r--r--sugar_network/node/routes.py161
-rw-r--r--sugar_network/node/slave.py14
-rw-r--r--sugar_network/toolkit/__init__.py88
-rw-r--r--sugar_network/toolkit/gbus.py4
-rw-r--r--sugar_network/toolkit/http.py114
-rw-r--r--sugar_network/toolkit/mountpoints.py4
-rw-r--r--sugar_network/toolkit/parcel.py5
-rw-r--r--sugar_network/toolkit/router.py120
-rw-r--r--sugar_network/toolkit/spec.py3
-rw-r--r--tests/__init__.py53
-rwxr-xr-xtests/units/client/injector.py33
-rwxr-xr-xtests/units/client/journal.py55
-rwxr-xr-xtests/units/client/routes.py37
-rwxr-xr-xtests/units/db/blobs.py54
-rwxr-xr-xtests/units/db/routes.py201
-rwxr-xr-xtests/units/db/volume.py172
-rwxr-xr-xtests/units/model/context.py231
-rwxr-xr-xtests/units/model/model.py182
-rwxr-xr-xtests/units/node/master.py39
-rwxr-xr-xtests/units/node/model.py242
-rwxr-xr-xtests/units/node/node.py242
-rwxr-xr-xtests/units/node/slave.py74
-rwxr-xr-xtests/units/toolkit/http.py5
-rwxr-xr-xtests/units/toolkit/parcel.py16
-rwxr-xr-xtests/units/toolkit/router.py98
49 files changed, 1758 insertions, 1427 deletions
diff --git a/TODO b/TODO
index 5244533..aadb695 100644
--- a/TODO
+++ b/TODO
@@ -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):