diff --git a/backend/src/logzen/db/streams.py b/backend/src/logzen/db/streams.py index 00f01e1..73a2a66 100644 --- a/backend/src/logzen/db/streams.py +++ b/backend/src/logzen/db/streams.py @@ -27,7 +27,6 @@ from logzen.db import Entity, JSONDict, DAO - class Stream(Entity): """ The database entity for streams. @@ -57,16 +56,16 @@ class Stream(Entity): nullable=False) - @export() class Streams(DAO): """ DAO for accessing stream entities. """ - def getStream(self, name): + def getStreamByName(self, user, name): try: return self.session \ .query(Stream) \ + .filter(Stream.user == user) \ .filter(Stream.name == name) \ .one() @@ -74,22 +73,30 @@ def getStream(self, name): raise KeyError(name) - def getStreams(self): - """ Returns an iterator over all existing stream entities. + def getStreamsByUser(self, user): + """ Returns all existing stream entities for the passed user. """ - return iter(self.session.query(Stream)) + return self.session \ + .query(Stream) \ + .filter(Stream.user == user) \ + .all() - def createStream(self, name): + def createStream(self, user, **kwargs): """ Create a new stream entity. All parameters are passed as-is to the entity to create. The created entity is attached to the session and returned. """ - stream = Stream(name=name) + stream = Stream(user=user, + **kwargs) self.session.add(stream) return stream + + + def deleteStream(self, stream): + self.session.delete(stream) diff --git a/backend/src/logzen/db/users.py b/backend/src/logzen/db/users.py index cb8271c..7860791 100644 --- a/backend/src/logzen/db/users.py +++ b/backend/src/logzen/db/users.py @@ -78,7 +78,7 @@ class Users(DAO): """ Accessor for user entities. """ - def getUser(self, username): + def getUserByName(self, username): """ Get a user entity with the given username. If a user with such username exists, the user entity is returned. @@ -95,7 +95,7 @@ def getUser(self, username): raise KeyError(username) - def getVerifiedUser(self, username, password): + def getVerifiedUserByName(self, username, password): """ Get a user entity with the given username and a matching password. If a user with such username exists and the users password matches @@ -103,7 +103,7 @@ def getVerifiedUser(self, username, password): """ try: - user = self.getUser(username) + user = self.getUserByName(username) except sqlalchemy.orm.exc.NoResultFound: return None diff --git a/backend/src/logzen/web/api/admin/users.py b/backend/src/logzen/web/api/admin/users.py index 29dadf8..eafe397 100644 --- a/backend/src/logzen/web/api/admin/users.py +++ b/backend/src/logzen/web/api/admin/users.py @@ -38,7 +38,7 @@ def list(users): def get(name, users): try: - user = users.getUser(name) + user = users.getUserByName(name) return {'username': user.username, 'admin': user.admin} @@ -76,12 +76,13 @@ def update(name, request): with session(): try: - user = users.getUser(name) - user.__init__(**request.data) + user = users.getUserByName(name) except KeyError: raise bottle.HTTPError(404, 'User not found: %s' % name) + user.__init__(**request.data) + @resource('/users/', 'DELETE') @require(users='logzen.db.users:Users') @@ -89,9 +90,10 @@ def delete(name, users): with session(): try: - user = users.getUser(name) - users.deleteUser(user) + user = users.getUserByName(name) except KeyError: raise bottle.HTTPError(404, 'User not found: %s' % name) + users.deleteUser(user) + diff --git a/backend/src/logzen/web/api/auth.py b/backend/src/logzen/web/api/auth.py index d59db04..ffdb87a 100644 --- a/backend/src/logzen/web/api/auth.py +++ b/backend/src/logzen/web/api/auth.py @@ -90,7 +90,7 @@ def wrapper(*args, **kwargs): self.logger.debug('Token validated with username: %s', username) # Get the user object for the username - user = self.users.getUser(username=username) + user = self.users.getUserByName(username=username) if user is None: raise bottle.HTTPError(401, 'User does not exist: ' + username) @@ -150,7 +150,7 @@ def install(api, users='logzen.db.users:Users') def login(request, users): - user = users.getVerifiedUser(**request.data) + user = users.getVerifiedUserByName(**request.data) if user is None: raise bottle.HTTPError(401, 'Wrong username or password') diff --git a/backend/src/logzen/web/api/user/streams.py b/backend/src/logzen/web/api/user/streams.py index 5f3e76a..1c964f2 100644 --- a/backend/src/logzen/web/api/user/streams.py +++ b/backend/src/logzen/web/api/user/streams.py @@ -20,24 +20,29 @@ import bottle from require import * +from logzen.db import session from logzen.web.api.user import resource @resource('/streams', 'GET') -@require(user='logzen.web.api.auth:User') -def list(user): - return {key: {'name': stream.name, - 'description': stream.description} - for key, stream - in user.streams.items()} +@require(user='logzen.web.api.auth:User', + streams='logzen.db.streams:Streams') +def list(user, + streams): + return {stream.name: {'description': stream.description, + 'filter': stream.filter} + for stream + in streams.getStreamsByUser(user)} @resource('/streams/', 'GET') -@require(user='logzen.web.api.auth:User') +@require(user='logzen.web.api.auth:User', + streams='logzen.db.streams:Streams') def get(name, - user): + user, + streams): try: - stream = user.streams[name] + stream = streams.getStreamByName(user, name) except KeyError: raise bottle.HTTPError(404, 'Stream not found: %s' % name) @@ -45,3 +50,61 @@ def get(name, return {'name': stream.name, 'description': stream.description, 'filter': stream.filter} + + +@resource('/streams', 'POST', + schema={'type': 'object', + 'properties': {'name': {'type': 'string'}, + 'description': {'type': 'string'}, + 'filter': {'type': 'object'}}, + 'required': ['name', + 'filter']}) +@require(user='logzen.web.api.auth:User', + streams='logzen.db.streams:Streams', + request='logzen.web.api:Request') +def create(user, + streams, + request): + with session(): + streams.createStream(user, **request.data) + + +@resource('/streams/', 'PUT', + schema={'type': 'object', + 'properties': {'name': {'type': 'string'}, + 'description': {'type': 'string'}, + 'filter': {'type': 'object'}}, + 'required': ['name', + 'filter']}) +@require(user='logzen.web.api.auth:User', + streams='logzen.db.streams:Streams', + request='logzen.web.api:Request') +def update(name, + user, + streams, + request): + with session(): + try: + stream = streams.getStreamByName(user, name) + + except KeyError: + raise bottle.HTTPError(404, 'Stream not found: %s' % name) + + stream.__init__(**request.data) + + +@resource('/streams/', 'DELETE') +@require(user='logzen.web.api.auth:User', + streams='logzen.db.streams:Streams') +def delete(name, + user, + streams): + with session(): + try: + stream = streams.getStreamByName(user, name) + + except KeyError: + raise bottle.HTTPError(404, 'Stream not found: %s' % name) + + streams.deleteStream(stream) + diff --git a/backend/tests/test_api.py b/backend/tests/test_api.py index ed3b4c8..b0818f0 100644 --- a/backend/tests/test_api.py +++ b/backend/tests/test_api.py @@ -1,18 +1,14 @@ import unittest +import json from hamcrest import * - import werkzeug.test import werkzeug.wrappers -import json - from require import * - from logzen.db import session - class ApiTestCase(unittest.TestCase): testConnection = require('util:TestConnection') @@ -42,7 +38,6 @@ def tearDown(self): self.transaction.rollback() - class AuthenticationApiTestCase(ApiTestCase): users = require('logzen.db.users:Users') @@ -134,6 +129,114 @@ def testFetchingAccountInfoSucceeds(self): assert_that(json.loads(resp.data.decode('utf8')), is_({'username': 'user'})) +class UserStreamsApiTestCase(ApiTestCase): + users = require('logzen.db.users:Users') + streams = require('logzen.db.streams:Streams') + + + def setUp(self): + super(UserStreamsApiTestCase, self).setUp() + + with session(): + self.user = self.users.createUser(username='user', + password='user') + + self.client.post('/api/v1/token', + data=json.dumps({'username': 'user', + 'password': 'user'})) + + + def testCreatingStreamSucceeds(self): + resp = self.client.post('/api/v1/user/streams', + data=json.dumps({'name': 'test', + 'description': 'For testing purposes only', + 'filter': {'foo': 42, + 'bar': 23}})) + + assert_that(resp.status_code, is_(200)) + + with session(): + user = self.users.getUserByName('user') + + assert_that(user.streams, has_length(1)) + assert_that(user.streams, has_items('test')) + + + def testDeletingStreamSucceeds(self): + with session(): + self.streams.createStream(user=self.user, + name='test', + filter={}) + + resp = self.client.delete('/api/v1/user/streams/test') + + assert_that(resp.status_code, is_(200)) + + with session(): + user = self.users.getUserByName('user') + + assert_that(user.streams, is_(empty())) + + + def testListingStreamsSucceeds(self): + with session(): + self.streams.createStream(user=self.user, + name='test1', + filter={}) + self.streams.createStream(user=self.user, + name='test2', + filter={}) + self.streams.createStream(user=self.user, + name='test3', + filter={}) + + resp = self.client.get('/api/v1/user/streams') + + assert_that(resp.status_code, is_(200)) + assert_that(json.loads(resp.data.decode('utf8')), is_({'test1': {'description': '', + 'filter': {}}, + 'test2': {'description': '', + 'filter': {}}, + 'test3': {'description': '', + 'filter': {}}})) + + + def testFetchingStreamSucceeds(self): + with session(): + self.streams.createStream(user=self.user, + name='test', + filter={}) + + resp = self.client.get('/api/v1/user/streams/test') + + assert_that(resp.status_code, is_(200)) + assert_that(json.loads(resp.data.decode('utf8')), is_({'name': 'test', + 'description': '', + 'filter': {}})) + + + def testUpdatingStreamSucceeds(self): + with session(): + self.streams.createStream(user=self.user, + name='test', + filter={}) + + resp = self.client.put('/api/v1/user/streams/test', + data=json.dumps({'name': 'toast', + 'description': 'x', + 'filter': {'foo': 'bar'}})) + + assert_that(resp.status_code, is_(200)) + + with session(): + user = self.users.getUserByName('user') + stream = user.streams['toast'] + + assert_that(stream.name, is_('toast')) + assert_that(stream.description, is_('x')) + assert_that(stream.filter, is_({'foo': 'bar'})) + + class AdminApiTestCase(ApiTestCase): users = require('logzen.db.users:Users') @@ -225,7 +328,7 @@ def testCreatingUserSucceeds(self): assert_that(resp.status_code, is_(200)) assert_that(resp.data, is_(b'')) - user = self.users.getUser('marvin') + user = self.users.getUserByName('marvin') assert_that(user, is_(not_none())) assert_that(user, has_properties(username='marvin', diff --git a/backend/tests/test_db.py b/backend/tests/test_db.py index 808287f..105155c 100644 --- a/backend/tests/test_db.py +++ b/backend/tests/test_db.py @@ -50,19 +50,19 @@ def testPasswordIsHashedAfterCreation(self): def testPasswordIsHashedAfterFetching(self): - user = self.users.getUser('test') + user = self.users.getUserByName('test') assert_that(user.password, is_(instance_of(Password))) def testPasswordVerificytionSucceeds(self): - user = self.users.getUser('test') + user = self.users.getUserByName('test') assert_that(user.password == 'plain', is_(True)) def testPasswordVerificytionWithWrongPasswordFails(self): - user = self.users.getUser('test') + user = self.users.getUserByName('test') assert_that(user.password == 'WRONG!', is_(False))