diff --git a/apiary.apib b/apiary.apib index 58691ca..d49ef3e 100644 --- a/apiary.apib +++ b/apiary.apib @@ -54,12 +54,13 @@ forbidden to create new groups. They can still vote, though. { "status": "ERROR", "error": "username already exists" } -## Single User [/user/{username}/] +## Single User [/user/{token}/] Manage a single user. + Parameters + username ... Username used in the creation process. + + token ... The user token ### Update information [POST] @@ -69,7 +70,7 @@ change will require only the "token" and "password" fields. + Request (application/json) - { "token": "userToken", "full_name": "Full name", "password": "hash" } + { "full_name": "Full name", "password": "hash" } + Response 200 (application/json) @@ -79,18 +80,14 @@ change will require only the "token" and "password" fields. { "status": "ERROR", "error": "Invalid token" } -+ Response 401 (application/json) ++ Response 404 (application/json) - { "status": "ERROR", "error": "User is not admin or not the same user" } + { "status": "ERROR", "error": "User not found (via token)"} ### Remove user [DELETE] User removal is only allowed to the user themselves or by a system admin. -+ Request (application/json) - - { "token": "userToken" } - + Response 200 (application/json) { "status": "OK" } diff --git a/luncho/blueprints/users.py b/luncho/blueprints/users.py index 9b2b8c4..b11405f 100644 --- a/luncho/blueprints/users.py +++ b/luncho/blueprints/users.py @@ -3,6 +3,8 @@ """User management.""" +import logging + from flask import Blueprint from flask import request from flask import jsonify @@ -10,10 +12,13 @@ from flask import jsonify from sqlalchemy.exc import IntegrityError from luncho.helpers import ForceJSON +from luncho.helpers import JSONError from luncho.server import User from luncho.server import db +LOG = logging.getLogger('luncho.blueprints.users') + users = Blueprint('users', __name__) @@ -35,7 +40,31 @@ def create_user(): return jsonify(status='OK') except IntegrityError: - resp = jsonify(status='ERROR', - error='username already exists') - resp.status_code = 409 - return resp + return JSONError(409, 'Username already exists') + +@users.route('/', methods=['POST']) +@ForceJSON() +def update_user(token): + """Update user information. Request can have the following fields: + { "full_name": "Full name", "password": "hash" } + Any other field will be ignored; only fields that need to be changed + must be send.""" + json = request.get_json(force=True) + + user = User.query.filter_by(token=token).first() + if not user: + return JSONError(404, 'User not found (via token)') + + if not user.valid_token(token): + return JSONError(400, 'Invalid token') + + if 'full_name' in json: + LOG.debug('Fullname = {fullname}'.format(fullname=json['full_name'])) + user.fullname = json['full_name'] + + if 'password' in json: + LOG.debug('Passhash = {password}'.format(password=json['password'])) + user.passhash = json['password'] + + db.session.commit() + return jsonify(status='OK') diff --git a/luncho/helpers.py b/luncho/helpers.py index 029b9da..f9370ff 100644 --- a/luncho/helpers.py +++ b/luncho/helpers.py @@ -54,7 +54,7 @@ def JSONError(status, message, **kwargs): :return: A response with the JSON and the status code.""" resp = jsonify(status='ERROR', - message=message, + error=message, **kwargs) resp.status_code = status return resp diff --git a/luncho/server.py b/luncho/server.py index b8a264c..bf991fe 100644 --- a/luncho/server.py +++ b/luncho/server.py @@ -48,18 +48,13 @@ class User(db.Model): self.fullname = fullname self.passhash = passhash self.token = token - self.issued_date = issued_date self.validated = validated self.created_at = datetime.datetime.now() def get_token(self): """Generate a user token or return the current one for the day.""" - if self.token and self.issued_date == datetime.date.today(): - return self.token - # create a token for the day self.token = self._token() - self.issued_date = datetime.date.today() db.session.commit() return self._token() diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 0000000..e91430b --- /dev/null +++ b/tests/base.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- + +import unittest +import json + +from luncho import server + + +class LunchoTests(unittest.TestCase): + """Base testing for all Lunch-o tests.""" + + # ------------------------------------------------------------ + # Test set up and tear down + # ------------------------------------------------------------ + def setUp(self): + # leave the database blank to make it in memory + server.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' + server.app.config['TESTING'] = True + + self.app = server.app.test_client() + server.db.create_all() + + def tearDown(self): + server.db.drop_all(bind=None) + + # ------------------------------------------------------------ + # Common assertions for lunch-o + # ------------------------------------------------------------ + + def assertJson(self, expected, response): + """Compare JSONs.""" + if not isinstance(response, dict): + response = json.loads(response) + + for key in expected: + if not key in response: + self.fail('Key {key} missing in response'.format( + key=key)) + + if response[key] != expected[key]: + self.fail('Key {key} differs: Expected "{expected}", ' + 'response "{response}"'.format( + key=key, + expected=expected[key], + response=response[key])) + + def assertStatusCode(self, response, status): + """Check the status code of the response.""" + self.assertEqual(response.status_code, status) + + # ------------------------------------------------------------ + # Easy way to convert the data to JSON and do requests + # ------------------------------------------------------------ + + def post(self, url, data): + """Send a POST request to the URL.""" + return self.app.post(url, + data=json.dumps(data), + content_type='application/json') + + def put(self, url, data): + """Send a PUT request to the URL.""" + return self.app.put(url, + data=json.dumps(data), + content_type='application/json') diff --git a/tests/users_tests.py b/tests/users_tests.py index c680edb..3c881dc 100644 --- a/tests/users_tests.py +++ b/tests/users_tests.py @@ -8,32 +8,21 @@ from luncho import server from luncho.server import User +from base import LunchoTests -class TestUsers(unittest.TestCase): - """Test users request.""" - - def setUp(self): - # leave the database blank to make it in memory - server.app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' - server.app.config['TESTING'] = True - - self.app = server.app.test_client() - server.db.create_all() - def tearDown(self): - server.db.drop_all(bind=None) +class TestUsers(LunchoTests): + """Test users request.""" def test_create_user(self): """Simple user creation.""" request = {'username': 'username', 'full_name': 'full name', 'password': 'hash'} - rv = self.app.put('/user/', - data=json.dumps(request), - content_type='application/json') + rv = self.put('/user/', request) - self.assertEqual(rv.status_code, 200) - self.assertEqual(json.loads(rv.data), {'status': 'OK'}) + self.assertStatusCode(rv, 200) + self.assertJson({'status': 'OK'}, rv.data) # db check self.assertIsNotNone(User.query.filter_by(username='username').first()) @@ -47,37 +36,92 @@ class TestUsers(unittest.TestCase): request = {'username': 'username', 'full_name': 'full name', 'password': 'hash'} - rv = self.app.put('/user/', - data=json.dumps(request), - content_type='application/json') + rv = self.put('/user/', data=request) expected = {"status": "ERROR", - "error": "username already exists"} - - self.assertEqual(rv.status_code, 409) - self.assertEqual(json.loads(rv.data), expected) + "error": "Username already exists"} + self.assertStatusCode(rv, 409) + self.assertJson(expected, rv.data) def test_no_json(self): """Check the status when doing a request that it's not JSON.""" - rv = self.app.put('/user/', - data='', - content_type='text/html') + rv = self.put('/user/', '') expected = {"error": "Request MUST be in JSON format", "status": "ERROR"} - self.assertEqual(rv.status_code, 400) - self.assertEqual(json.loads(rv.data), expected) + self.assertStatusCode(rv, 400) + self.assertJson(expected, rv.data) def test_missing_fields(self): + """Send a request with missing fields.""" request = {'password': 'hash'} - rv = self.app.put('/user/', - data=json.dumps(request), - content_type='application/json') - - resp = {'error': 'Missing fields: username, full_name', - 'status': 'ERROR'} - self.assertEqual(rv.status_code, 400) - self.assertEqual(json.loads(rv.data), resp) + rv = self.put('/user/', request) + + expected = {'error': 'Missing fields: username, full_name', + 'status': 'ERROR'} + self.assertStatusCode(rv, 400) + self.assertJson(expected, rv.data) + + +class TestExistingUsers(LunchoTests): + """Tests for existing users.""" + def setUp(self): + super(TestExistingUsers, self).setUp() + self.user = User(username='test', + fullname='Test User', + passhash='hash') + server.db.session.add(self.user) + server.db.session.commit() + self.user.get_token() + + def tearDown(self): + super(TestExistingUsers, self).tearDown() + + def test_update_details(self): + """Update user details.""" + request = {'full_name': 'New User Name', + 'password': 'newhash'} + rv = self.post('/user/{token}/'.format(token=self.user.token), + request) + + expected = {'status': 'OK'} + self.assertStatusCode(rv, 200) + self.assertJson(expected, rv.data) + + # check in the database + user = User.query.filter_by(username='test').first() + self.assertEqual(user.fullname, request['full_name']) + self.assertEqual(user.passhash, request['password']) + + def test_wrong_token(self): + """Send a request with an unexisting token.""" + request = {'full_name': 'New User Name', + 'password': 'newhash'} + rv = self.post('/user/{token}/'.format(token='no-token'), + request) + + expected = {'status': 'ERROR', + 'error': 'User not found (via token)'} + self.assertStatusCode(rv, 404) + self.assertJson(expected, rv.data) + + def test_expired_token(self): + """Send a token that exists but it's not valid for today.""" + # the token is not valid by our standards, but it will be found and + # and the token for today will not be valid + self.user.token = 'expired' + server.db.session.commit() + + request = {'full_name': 'New User Name', + 'password': 'newhash'} + rv = self.post('/user/{token}/'.format(token=self.user.token), + request) + + expected = {'status': 'ERROR', + 'error': 'Invalid token'} + self.assertStatusCode(rv, 400) + self.assertJson(expected, rv.data) + if __name__ == '__main__': unittest.main()