diff --git a/.gitignore b/.gitignore index e2548a31..a683cefa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ config.yaml */*_modules/ +.coverage +.cache diff --git a/server/requirements.txt b/server/requirements.txt index 5c0d15ad..c59cf69f 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -3,4 +3,5 @@ pyyaml>=3.11 falcon>=0.3.0 psycopg2>=2.6.1 SQLAlchemy>=1.0.12 -green>=2.4.0 +pytest>=2.9.1 +pytest-cov>=2.2.1 diff --git a/server/szurubooru/tests/__init__.py b/server/szurubooru/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/server/szurubooru/tests/api/__init__.py b/server/szurubooru/tests/api/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/server/szurubooru/tests/api/test_password_reset.py b/server/szurubooru/tests/api/test_password_reset.py new file mode 100644 index 00000000..b29590f0 --- /dev/null +++ b/server/szurubooru/tests/api/test_password_reset.py @@ -0,0 +1,78 @@ +from datetime import datetime +from unittest import mock +import pytest +from szurubooru import api, db, errors +from szurubooru.util import auth, mailer + +def mock_user(name, rank, email): + user = db.User() + user.name = name + user.password = 'dummy' + user.password_salt = 'dummy' + user.password_hash = 'dummy' + user.email = email + user.rank = rank + user.creation_time = datetime(1997, 1, 1) + user.avatar_style = db.User.AVATAR_GRAVATAR + return user + +@pytest.fixture +def password_reset_api(config_injector): + config_injector({ + 'secret': 'x', + 'base_url': 'http://example.com/', + 'name': 'Test instance', + }) + return api.PasswordResetApi() + +def test_reset_non_existing(password_reset_api, context_factory): + with pytest.raises(errors.NotFoundError): + password_reset_api.get(context_factory(), 'u1') + +def test_reset_without_email(password_reset_api, session, context_factory): + user = mock_user('u1', 'regular_user', None) + session.add(user) + with pytest.raises(errors.ValidationError): + password_reset_api.get(context_factory(), 'u1') + +def test_reset_sending_email(password_reset_api, session, context_factory): + user = mock_user('u1', 'regular_user', 'user@example.com') + session.add(user) + for getter in ['u1', 'user@example.com']: + mailer.send_mail = mock.MagicMock() + assert password_reset_api.get(context_factory(), getter) == {} + mailer.send_mail.assert_called_once_with( + 'noreply@Test instance', + 'user@example.com', + 'Password reset for Test instance', + 'You (or someone else) requested to reset your password ' + + 'on Test instance.\nIf you wish to proceed, click this l' + + 'ink: http://example.com/password-reset/u1:4ac0be176fb36' + + '4f13ee6b634c43220e2\nOtherwise, please ignore this email.') + +def test_confirmation_non_existing(password_reset_api, context_factory): + with pytest.raises(errors.NotFoundError): + password_reset_api.post(context_factory(), 'u1') + +def test_confirmation_no_token(password_reset_api, context_factory, session): + user = mock_user('u1', 'regular_user', 'user@example.com') + session.add(user) + with pytest.raises(errors.ValidationError): + password_reset_api.post(context_factory(request={}), 'u1') + +def test_confirmation_bad_token(password_reset_api, context_factory, session): + user = mock_user('u1', 'regular_user', 'user@example.com') + session.add(user) + with pytest.raises(errors.ValidationError): + password_reset_api.post( + context_factory(request={'token': 'bad'}), 'u1') + +def test_confirmation_good_token(password_reset_api, context_factory, session): + user = mock_user('u1', 'regular_user', 'user@example.com') + old_hash = user.password_hash + session.add(user) + context = context_factory( + request={'token': '4ac0be176fb364f13ee6b634c43220e2'}) + result = password_reset_api.post(context, 'u1') + assert user.password_hash != old_hash + assert auth.is_valid_password(user, result['password']) is True diff --git a/server/szurubooru/tests/api/test_password_reset_api.py b/server/szurubooru/tests/api/test_password_reset_api.py deleted file mode 100644 index 93742b96..00000000 --- a/server/szurubooru/tests/api/test_password_reset_api.py +++ /dev/null @@ -1,68 +0,0 @@ -from unittest import mock -from szurubooru import api, errors -from szurubooru.util import auth, mailer -from szurubooru.tests.database_test_case import DatabaseTestCase -from szurubooru.tests.api import util - -class TestPasswordReset(DatabaseTestCase): - def setUp(self): - super().setUp() - util.mock_config({ - 'secret': 'x', - 'base_url': 'http://example.com/', - 'name': 'Test instance', - }) - util.mock_context(self) - self.api = api.PasswordResetApi() - - def test_reset_non_existing(self): - self.assertRaises(errors.NotFoundError, self.api.get, self.context, 'u1') - - def test_reset_without_email(self): - user = util.mock_user('u1', 'regular_user') - user.email = None - self.session.add(user) - self.assertRaises(errors.ValidationError, self.api.get, self.context, 'u1') - - def test_reset_sending_email(self): - user = util.mock_user('u1', 'regular_user') - user.email = 'user@example.com' - self.session.add(user) - for getter in ['u1', 'user@example.com']: - mailer.send_mail = mock.MagicMock() - self.assertEqual({}, self.api.get(self.context, getter)) - mailer.send_mail.assert_called_once_with( - 'noreply@Test instance', - 'user@example.com', - 'Password reset for Test instance', - 'You (or someone else) requested to reset your password ' + - 'on Test instance.\nIf you wish to proceed, click this l' + - 'ink: http://example.com/password-reset/u1:4ac0be176fb36' + - '4f13ee6b634c43220e2\nOtherwise, please ignore this email.') - - def test_confirmation_non_existing(self): - self.assertRaises(errors.NotFoundError, self.api.post, self.context, 'u1') - - def test_confirmation_no_token(self): - user = util.mock_user('u1', 'regular_user') - user.email = 'user@example.com' - self.session.add(user) - self.context.request = {} - self.assertRaises(errors.ValidationError, self.api.post, self.context, 'u1') - - def test_confirmation_bad_token(self): - user = util.mock_user('u1', 'regular_user') - user.email = 'user@example.com' - self.session.add(user) - self.context.request = {'token': 'bad'} - self.assertRaises(errors.ValidationError, self.api.post, self.context, 'u1') - - def test_confirmation_good_token(self): - user = util.mock_user('u1', 'regular_user') - user.email = 'user@example.com' - old_hash = user.password_hash - self.session.add(user) - self.context.request = {'token': '4ac0be176fb364f13ee6b634c43220e2'} - result = self.api.post(self.context, 'u1') - self.assertNotEqual(user.password_hash, old_hash) - self.assertTrue(auth.is_valid_password(user, result['password'])) diff --git a/server/szurubooru/tests/api/test_user_api.py b/server/szurubooru/tests/api/test_user_api.py deleted file mode 100644 index a2d9be2b..00000000 --- a/server/szurubooru/tests/api/test_user_api.py +++ /dev/null @@ -1,366 +0,0 @@ -import tempfile -from datetime import datetime -from szurubooru import api, db, errors -from szurubooru.util import auth -from szurubooru.tests.database_test_case import DatabaseTestCase -from szurubooru.tests.api import util - -class TestRetrievingUsers(DatabaseTestCase): - def setUp(self): - super().setUp() - util.mock_config({ - 'privileges': { - 'users:list': 'regular_user', - }, - 'thumbnails': {'avatar_width': 200}, - 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], - 'rank_names': {}, - }) - util.mock_context(self) - - def test_retrieving_multiple(self): - user1 = util.mock_user('u1', 'mod') - user2 = util.mock_user('u2', 'mod') - self.session.add_all([user1, user2]) - util.mock_params(self.context, {'query': '', 'page': 1}) - self.context.user.rank = 'regular_user' - api_ = api.UserListApi() - result = api_.get(self.context) - self.assertEqual(result['query'], '') - self.assertEqual(result['page'], 1) - self.assertEqual(result['pageSize'], 100) - self.assertEqual(result['total'], 2) - self.assertEqual([u['name'] for u in result['users']], ['u1', 'u2']) - - def test_retrieving_multiple_without_privileges(self): - self.context.user.rank = 'anonymous' - util.mock_params(self.context, {'query': '', 'page': 1}) - api_ = api.UserListApi() - self.assertRaises(errors.AuthError, api_.get, self.context) - - def test_retrieving_non_existing(self): - self.context.user.rank = 'regular_user' - util.mock_params(self.context, {'query': 'asd', 'page': 1}) - api_ = api.UserListApi() - result = api_.get(self.context) - self.assertEqual(result['query'], 'asd') - self.assertEqual(result['page'], 1) - self.assertEqual(result['pageSize'], 100) - self.assertEqual(result['total'], 0) - self.assertEqual([u['name'] for u in result['users']], []) - -class TestRetrievingUser(DatabaseTestCase): - def setUp(self): - super().setUp() - util.mock_config({ - 'privileges': { - 'users:view': 'regular_user', - }, - 'thumbnails': {'avatar_width': 200}, - 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], - 'rank_names': {}, - }) - util.mock_context(self) - - def test_retrieving_single(self): - user = util.mock_user('u1', 'regular_user') - self.session.add(user) - self.context.user.rank = 'regular_user' - util.mock_params(self.context, {'query': '', 'page': 1}) - api_ = api.UserDetailApi() - result = api_.get(self.context, 'u1') - self.assertEqual(result['user']['id'], user.user_id) - self.assertEqual(result['user']['name'], 'u1') - self.assertEqual(result['user']['rank'], 'regular_user') - self.assertEqual(result['user']['creationTime'], datetime(1997, 1, 1)) - self.assertEqual(result['user']['lastLoginTime'], None) - self.assertEqual(result['user']['avatarStyle'], 'gravatar') - - def test_retrieving_non_existing(self): - self.context.user.rank = 'regular_user' - util.mock_params(self.context, {'query': '', 'page': 1}) - api_ = api.UserDetailApi() - self.assertRaises(errors.NotFoundError, api_.get, self.context, '-') - - def test_retrieving_single_without_privileges(self): - self.context.user.rank = 'anonymous' - util.mock_params(self.context, {'query': '', 'page': 1}) - api_ = api.UserDetailApi() - self.assertRaises(errors.AuthError, api_.get, self.context, '-') - -class TestDeletingUser(DatabaseTestCase): - def setUp(self): - super().setUp() - util.mock_config({ - 'privileges': { - 'users:delete:self': 'regular_user', - 'users:delete:any': 'mod', - }, - 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], - 'rank_names': {}, - }) - util.mock_context(self) - - def test_removing_oneself(self): - user1 = util.mock_user('u1', 'regular_user') - user2 = util.mock_user('u2', 'regular_user') - self.session.add_all([user1, user2]) - self.session.commit() - self.context.user.user_id = user1.user_id - self.context.user.rank = 'regular_user' - api_ = api.UserDetailApi() - self.assertRaises(errors.AuthError, api_.delete, self.context, 'u2') - api_.delete(self.context, 'u1') - self.assertEqual(self.session.query(db.User).filter_by(name='u1').all(), []) - - def test_removing_someone_else(self): - user1 = util.mock_user('u1', 'regular_user') - user2 = util.mock_user('u2', 'regular_user') - self.session.add_all([user1, user2]) - self.session.commit() - self.context.user.rank = 'mod' - api_ = api.UserDetailApi() - api_.delete(self.context, 'u1') - api_.delete(self.context, 'u2') - self.assertEqual(self.session.query(db.User).filter_by(name='u1').all(), []) - self.assertEqual(self.session.query(db.User).filter_by(name='u2').all(), []) - - def test_removing_non_existing(self): - self.context.user.rank = 'regular_user' - api_ = api.UserDetailApi() - self.assertRaises(errors.NotFoundError, api_.delete, self.context, 'bad') - -class TestCreatingUser(DatabaseTestCase): - def setUp(self): - super().setUp() - util.mock_config({ - 'secret': '', - 'user_name_regex': '.{3,}', - 'password_regex': '.{3,}', - 'default_rank': 'regular_user', - 'thumbnails': {'avatar_width': 200}, - 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], - 'rank_names': {}, - 'privileges': { - 'users:create': 'anonymous', - }, - }) - self.api = api.UserListApi() - util.mock_context(self) - self.context.user.rank = 'anonymous' - - def test_first_user_becomes_admin(self): - self.context.request = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - } - self.api.post(self.context) - created_user = self.session.query(db.User).filter_by(name='chewie').one() - self.assertEqual(created_user.name, 'chewie') - self.assertEqual(created_user.email, 'asd@asd.asd') - self.assertEqual(created_user.rank, 'admin') - self.assertTrue(auth.is_valid_password(created_user, 'oks')) - self.assertFalse(auth.is_valid_password(created_user, 'invalid')) - - def test_subsequent_users_are_created_normally(self): - self.context.request = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - } - self.api.post(self.context) - self.context.request['name'] = 'chewie2' - self.api.post(self.context) - created_user = self.session.query(db.User).filter_by(name='chewie2').one() - self.assertEqual(created_user.name, 'chewie2') - self.assertEqual(created_user.email, 'asd@asd.asd') - self.assertEqual(created_user.rank, 'regular_user') - self.assertTrue(auth.is_valid_password(created_user, 'oks')) - self.assertFalse(auth.is_valid_password(created_user, 'invalid')) - - def test_creating_user_that_already_exists(self): - self.context.request = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - } - self.api.post(self.context) - self.assertRaises(errors.IntegrityError, self.api.post, self.context) - - def test_creating_user_that_already_exists_insensitive(self): - self.context.request = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - } - self.api.post(self.context) - self.context.name = 'chewie' - self.assertRaises(errors.IntegrityError, self.api.post, self.context) - - def test_missing_field(self): - for key in ['name', 'email', 'password']: - self.context.request = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - } - del self.context.request[key] - self.assertRaises(errors.ValidationError, self.api.post, self.context) - -class TestUpdatingUser(DatabaseTestCase): - def setUp(self): - super().setUp() - util.mock_config({ - 'secret': '', - 'user_name_regex': '.{3,}', - 'password_regex': '.{3,}', - 'data_dir': tempfile.gettempdir(), - 'data_url': 'http://example.com/data/', - 'thumbnails': {'avatar_width': 200, 'avatar_height': 200}, - 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], - 'rank_names': {}, - 'privileges': { - 'users:edit:self:name': 'regular_user', - 'users:edit:self:pass': 'regular_user', - 'users:edit:self:email': 'regular_user', - 'users:edit:self:rank': 'mod', - 'users:edit:self:avatar': 'mod', - 'users:edit:any:name': 'mod', - 'users:edit:any:pass': 'mod', - 'users:edit:any:email': 'mod', - 'users:edit:any:rank': 'admin', - 'users:edit:any:avatar': 'admin', - }, - }) - util.mock_context(self) - self.api = api.UserDetailApi() - - def test_update_changing_nothing(self): - admin_user = util.mock_user('u1', 'admin') - self.session.add(admin_user) - self.context.user = admin_user - self.api.put(self.context, 'u1') - admin_user = self.session.query(db.User).filter_by(name='u1').one() - self.assertEqual(admin_user.name, 'u1') - self.assertEqual(admin_user.email, 'dummy') - self.assertEqual(admin_user.rank, 'admin') - - def test_updating_non_existing_user(self): - admin_user = util.mock_user('u1', 'admin') - self.session.add(admin_user) - self.context.user = admin_user - self.assertRaises(errors.NotFoundError, self.api.put, self.context, 'u2') - - def test_admin_updating_everything_for_themselves(self): - admin_user = util.mock_user('u1', 'admin') - self.session.add(admin_user) - self.context.user = admin_user - self.context.request = { - 'name': 'chewie', - 'email': 'asd@asd.asd', - 'password': 'oks', - 'rank': 'mod', - 'avatarStyle': 'gravatar', - } - self.api.put(self.context, 'u1') - admin_user = self.session.query(db.User).filter_by(name='chewie').one() - self.assertEqual(admin_user.name, 'chewie') - self.assertEqual(admin_user.email, 'asd@asd.asd') - self.assertEqual(admin_user.rank, 'mod') - self.assertEqual(admin_user.avatar_style, admin_user.AVATAR_GRAVATAR) - self.assertTrue(auth.is_valid_password(admin_user, 'oks')) - self.assertFalse(auth.is_valid_password(admin_user, 'invalid')) - - def test_removing_email(self): - admin_user = util.mock_user('u1', 'admin') - self.session.add(admin_user) - self.context.user = admin_user - self.context.request = {'email': ''} - self.api.put(self.context, 'u1') - admin_user = self.session.query(db.User).filter_by(name='u1').one() - self.assertEqual(admin_user.email, None) - - def test_invalid_inputs(self): - admin_user = util.mock_user('u1', 'admin') - self.session.add(admin_user) - self.context.user = admin_user - self.context.request = {'name': '.'} - self.assertRaises( - errors.ValidationError, self.api.put, self.context, 'u1') - self.context.request = {'password': '.'} - self.assertRaises( - errors.ValidationError, self.api.put, self.context, 'u1') - self.context.request = {'rank': '.'} - self.assertRaises( - errors.ValidationError, self.api.put, self.context, 'u1') - self.context.request = {'email': '.'} - self.assertRaises( - errors.ValidationError, self.api.put, self.context, 'u1') - self.context.request = {'avatarStyle': 'manual'} - self.assertRaises( - errors.ValidationError, self.api.put, self.context, 'u1') - - def test_user_trying_to_update_someone_else(self): - user1 = util.mock_user('u1', 'regular_user') - user2 = util.mock_user('u2', 'regular_user') - self.session.add_all([user1, user2]) - self.context.user = user1 - for request in [ - {'name': 'whatever'}, - {'email': 'whatever'}, - {'rank': 'whatever'}, - {'password': 'whatever'}]: - self.context.request = request - self.assertRaises( - errors.AuthError, self.api.put, self.context, user2.name) - - def test_user_trying_to_become_someone_else(self): - user1 = util.mock_user('me', 'regular_user') - user2 = util.mock_user('her', 'regular_user') - self.session.add_all([user1, user2]) - self.context.user = user1 - self.context.request = {'name': 'her'} - self.assertRaises( - errors.IntegrityError, self.api.put, self.context, 'me') - self.session.rollback() - - def test_user_trying_to_become_someone_else_insensitive(self): - user1 = util.mock_user('me', 'regular_user') - user2 = util.mock_user('her', 'regular_user') - self.session.add_all([user1, user2]) - self.context.user = user1 - self.context.request = {'name': 'HER'} - self.assertRaises( - errors.IntegrityError, self.api.put, self.context, 'me') - self.session.rollback() - - def test_mods_trying_to_become_admin(self): - user1 = util.mock_user('u1', 'mod') - user2 = util.mock_user('u2', 'mod') - self.session.add_all([user1, user2]) - self.context.user = user1 - self.context.request = {'rank': 'admin'} - self.assertRaises( - errors.AuthError, self.api.put, self.context, user1.name) - self.assertRaises( - errors.AuthError, self.api.put, self.context, user2.name) - - def test_uploading_avatar(self): - user = util.mock_user('u1', 'mod') - self.session.add(user) - self.context.user = user - self.context.request = { - 'avatarStyle': 'manual', - } - empty_pixel = \ - b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00' \ - b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00' \ - b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b' - self.context.files['avatar'] = empty_pixel - response = self.api.put(self.context, 'u1') - user = self.session.query(db.User).filter_by(name='u1').one() - self.assertEqual(user.avatar_style, user.AVATAR_MANUAL) - self.assertEqual( - response['user']['avatarUrl'], - 'http://example.com/data/avatars/u1.jpg') diff --git a/server/szurubooru/tests/api/test_user_creating.py b/server/szurubooru/tests/api/test_user_creating.py new file mode 100644 index 00000000..b9d1204a --- /dev/null +++ b/server/szurubooru/tests/api/test_user_creating.py @@ -0,0 +1,112 @@ +import pytest +from datetime import datetime +from szurubooru import api, db, errors +from szurubooru.util import auth + +@pytest.fixture +def user_list_api(): + return api.UserListApi() + +def test_creating_users( + session, + config_injector, + context_factory, + user_factory, + user_list_api): + config_injector({ + 'secret': '', + 'user_name_regex': '.{3,}', + 'password_regex': '.{3,}', + 'default_rank': 'regular_user', + 'thumbnails': {'avatar_width': 200}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'rank_names': {}, + 'privileges': {'users:create': 'anonymous'}, + }) + + user_list_api.post( + context_factory( + request={ + 'name': 'chewie1', + 'email': 'asd@asd.asd', + 'password': 'oks', + }, + user=user_factory(rank='regular_user'))) + user_list_api.post( + context_factory( + request={ + 'name': 'chewie2', + 'email': 'asd@asd.asd', + 'password': 'sok', + }, + user=user_factory(rank='regular_user'))) + + first_user = session.query(db.User).filter_by(name='chewie1').one() + other_user = session.query(db.User).filter_by(name='chewie2').one() + assert first_user.name == 'chewie1' + assert first_user.email == 'asd@asd.asd' + assert first_user.rank == 'admin' + assert auth.is_valid_password(first_user, 'oks') is True + assert auth.is_valid_password(first_user, 'invalid') is False + assert other_user.name == 'chewie2' + assert other_user.email == 'asd@asd.asd' + assert other_user.rank == 'regular_user' + assert auth.is_valid_password(other_user, 'sok') is True + assert auth.is_valid_password(other_user, 'invalid') is False + +def test_creating_user_that_already_exists( + config_injector, context_factory, user_factory, user_list_api): + config_injector({ + 'secret': '', + 'user_name_regex': '.{3,}', + 'password_regex': '.{3,}', + 'default_rank': 'regular_user', + 'thumbnails': {'avatar_width': 200}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'rank_names': {}, + 'privileges': {'users:create': 'anonymous'}, + }) + user_list_api.post( + context_factory( + request={ + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + }, + user=user_factory(rank='regular_user'))) + with pytest.raises(errors.IntegrityError): + user_list_api.post( + context_factory( + request={ + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + }, + user=user_factory(rank='regular_user'))) + with pytest.raises(errors.IntegrityError): + user_list_api.post( + context_factory( + request={ + 'name': 'CHEWIE', + 'email': 'asd@asd.asd', + 'password': 'oks', + }, + user=user_factory(rank='regular_user'))) + +@pytest.mark.parametrize('field', ['name', 'email', 'password']) +def test_missing_field( + config_injector, context_factory, user_factory, user_list_api, field): + config_injector({ + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'privileges': {'users:create': 'anonymous'}, + }) + request = { + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + } + del request[field] + with pytest.raises(errors.ValidationError): + user_list_api.post( + context_factory( + request=request, user=user_factory(rank='regular_user'))) diff --git a/server/szurubooru/tests/api/test_user_deletion.py b/server/szurubooru/tests/api/test_user_deletion.py new file mode 100644 index 00000000..d09d6e51 --- /dev/null +++ b/server/szurubooru/tests/api/test_user_deletion.py @@ -0,0 +1,65 @@ +import pytest +from datetime import datetime +from szurubooru import api, db, errors + +@pytest.fixture +def user_detail_api(): + return api.UserDetailApi() + +def test_removing_oneself( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'privileges': { + 'users:delete:self': 'regular_user', + 'users:delete:any': 'mod', + }, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + }) + user1 = user_factory(name='u1', rank='regular_user') + user2 = user_factory(name='u2', rank='regular_user') + session.add_all([user1, user2]) + session.commit() + with pytest.raises(errors.AuthError): + user_detail_api.delete(context_factory(user=user1), 'u2') + user_detail_api.delete(context_factory(user=user1), 'u1') + assert [u.name for u in session.query(db.User).all()] == ['u2'] + +def test_removing_someone_else( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'privileges': { + 'users:delete:self': 'regular_user', + 'users:delete:any': 'mod', + }, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + }) + user1 = user_factory(name='u1', rank='regular_user') + user2 = user_factory(name='u2', rank='regular_user') + mod_user = user_factory(rank='mod') + session.add_all([user1, user2]) + session.commit() + user_detail_api.delete(context_factory(user=mod_user), 'u1') + user_detail_api.delete(context_factory(user=mod_user), 'u2') + assert session.query(db.User).all() == [] + +def test_removing_non_existing( + context_factory, config_injector, user_factory, user_detail_api): + config_injector({ + 'privileges': { + 'users:delete:self': 'regular_user', + 'users:delete:any': 'mod', + }, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + }) + with pytest.raises(errors.NotFoundError): + user_detail_api.delete( + context_factory(user=user_factory(rank='regular_user')), 'bad') + diff --git a/server/szurubooru/tests/api/test_user_retrieval.py b/server/szurubooru/tests/api/test_user_retrieval.py new file mode 100644 index 00000000..6b017dd6 --- /dev/null +++ b/server/szurubooru/tests/api/test_user_retrieval.py @@ -0,0 +1,116 @@ +import pytest +from datetime import datetime +from szurubooru import api, db, errors + +@pytest.fixture +def user_list_api(): + return api.UserListApi() + +@pytest.fixture +def user_detail_api(): + return api.UserDetailApi() + +def test_retrieving_multiple( + session, + config_injector, + context_factory, + user_factory, + user_list_api): + config_injector({ + 'privileges': {'users:list': 'regular_user'}, + 'thumbnails': {'avatar_width': 200}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'rank_names': {}, + }) + user1 = user_factory(name='u1', rank='mod') + user2 = user_factory(name='u2', rank='mod') + session.add_all([user1, user2]) + result = user_list_api.get( + context_factory( + params={'query': '', 'page': 1}, + user=user_factory(rank='regular_user'))) + assert result['query'] == '' + assert result['page'] == 1 + assert result['pageSize'] == 100 + assert result['total'] == 2 + assert [u['name'] for u in result['users']] == ['u1', 'u2'] + +def test_retrieving_multiple_without_privileges( + context_factory, config_injector, user_factory, user_list_api): + config_injector({ + 'privileges': {'users:list': 'regular_user'}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + }) + with pytest.raises(errors.AuthError): + user_list_api.get( + context_factory( + params={'query': '', 'page': 1}, + user=user_factory(rank='anonymous'))) + +def test_retrieving_multiple_with_privileges( + context_factory, config_injector, user_factory, user_list_api): + config_injector({ + 'privileges': {'users:list': 'regular_user'}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + }) + result = user_list_api.get( + context_factory( + params={'query': 'asd', 'page': 1}, + user=user_factory(rank='regular_user'))) + assert result['query'] == 'asd' + assert result['page'] == 1 + assert result['pageSize'] == 100 + assert result['total'] == 0 + assert [u['name'] for u in result['users']] == [] + +def test_retrieving_single( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'privileges': {'users:view': 'regular_user'}, + 'thumbnails': {'avatar_width': 200}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'rank_names': {}, + }) + user = user_factory(name='u1', rank='regular_user') + session.add(user) + result = user_detail_api.get( + context_factory( + params={'query': '', 'page': 1}, + user=user_factory(rank='regular_user')), + 'u1') + assert result['user']['id'] == user.user_id + assert result['user']['name'] == 'u1' + assert result['user']['rank'] == 'regular_user' + assert result['user']['creationTime'] == datetime(1997, 1, 1) + assert result['user']['lastLoginTime'] == None + assert result['user']['avatarStyle'] == 'gravatar' + +def test_retrieving_non_existing( + context_factory, config_injector, user_factory, user_detail_api): + config_injector({ + 'privileges': {'users:view': 'regular_user'}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + }) + with pytest.raises(errors.NotFoundError): + user_detail_api.get( + context_factory( + params={'query': '', 'page': 1}, + user=user_factory(rank='regular_user')), + '-') + +def test_retrieving_single_without_privileges( + context_factory, config_injector, user_factory, user_detail_api): + config_injector({ + 'privileges': {'users:view': 'regular_user'}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + }) + with pytest.raises(errors.AuthError): + user_detail_api.get( + context_factory( + params={'query': '', 'page': 1}, + user=user_factory(rank='anonymous')), + '-') diff --git a/server/szurubooru/tests/api/test_user_updating.py b/server/szurubooru/tests/api/test_user_updating.py new file mode 100644 index 00000000..79205838 --- /dev/null +++ b/server/szurubooru/tests/api/test_user_updating.py @@ -0,0 +1,237 @@ +import pytest +from szurubooru import api, db, errors +from szurubooru.util import auth + +@pytest.fixture +def user_detail_api(): + return api.UserDetailApi() + +def test_updating_user( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'secret': '', + 'user_name_regex': '.{3,}', + 'password_regex': '.{3,}', + 'thumbnails': {'avatar_width': 200}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'rank_names': {}, + 'privileges': { + 'users:edit:self:name': 'regular_user', + 'users:edit:self:pass': 'regular_user', + 'users:edit:self:email': 'regular_user', + 'users:edit:self:rank': 'mod', + 'users:edit:self:avatar': 'mod', + }, + }) + user = user_factory(name='u1', rank='admin') + session.add(user) + user_detail_api.put( + context_factory( + request={ + 'name': 'chewie', + 'email': 'asd@asd.asd', + 'password': 'oks', + 'rank': 'mod', + 'avatarStyle': 'gravatar', + }, + user=user), + 'u1') + user = session.query(db.User).filter_by(name='chewie').one() + assert user.name == 'chewie' + assert user.email == 'asd@asd.asd' + assert user.rank == 'mod' + assert user.avatar_style == user.AVATAR_GRAVATAR + assert auth.is_valid_password(user, 'oks') is True + assert auth.is_valid_password(user, 'invalid') is False + +def test_update_changing_nothing( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'thumbnails': {'avatar_width': 200}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'rank_names': {}, + }) + user = user_factory(name='u1', rank='admin') + session.add(user) + user_detail_api.put(context_factory(user=user), 'u1') + user = session.query(db.User).filter_by(name='u1').one() + assert user.name == 'u1' + assert user.email == 'dummy' + assert user.rank == 'admin' + +def test_updating_non_existing_user( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + }) + user = user_factory(name='u1', rank='admin') + session.add(user) + with pytest.raises(errors.NotFoundError): + user_detail_api.put(context_factory(user=user), 'u2') + +def test_removing_email( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'thumbnails': {'avatar_width': 200}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'rank_names': {}, + 'privileges': {'users:edit:self:email': 'regular_user'}, + }) + user = user_factory(name='u1', rank='admin') + session.add(user) + user_detail_api.put( + context_factory(request={'email': ''}, user=user), 'u1') + assert session.query(db.User).filter_by(name='u1').one().email is None + +@pytest.mark.parametrize('request', [ + {'name': '.'}, + {'password': '.'}, + {'rank': '.'}, + {'email': '.'}, + {'avatarStyle': 'manual'}, +]) +def test_invalid_inputs( + session, + config_injector, + context_factory, + user_factory, + user_detail_api, + request): + config_injector({ + 'user_name_regex': '.{3,}', + 'password_regex': '.{3,}', + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'privileges': { + 'users:edit:self:name': 'regular_user', + 'users:edit:self:pass': 'regular_user', + 'users:edit:self:email': 'regular_user', + 'users:edit:self:rank': 'mod', + 'users:edit:self:avatar': 'mod', + }, + }) + user = user_factory(name='u1', rank='admin') + session.add(user) + with pytest.raises(errors.ValidationError): + user_detail_api.put(context_factory(request=request, user=user), 'u1') + +@pytest.mark.parametrize('request', [ + {'name': 'whatever'}, + {'email': 'whatever'}, + {'rank': 'whatever'}, + {'password': 'whatever'}, + {'avatarStyle': 'whatever'}, +]) +def test_user_trying_to_update_someone_else( + session, + config_injector, + context_factory, + user_factory, + user_detail_api, + request): + config_injector({ + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'privileges': { + 'users:edit:any:name': 'mod', + 'users:edit:any:pass': 'mod', + 'users:edit:any:email': 'mod', + 'users:edit:any:rank': 'admin', + 'users:edit:any:avatar': 'admin', + }, + }) + user1 = user_factory(name='u1', rank='regular_user') + user2 = user_factory(name='u2', rank='regular_user') + session.add_all([user1, user2]) + with pytest.raises(errors.AuthError): + user_detail_api.put( + context_factory(request=request, user=user1), user2.name) + +def test_user_trying_to_become_someone_else( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'privileges': {'users:edit:self:name': 'regular_user'}, + }) + user1 = user_factory(name='me', rank='regular_user') + user2 = user_factory(name='her', rank='regular_user') + session.add_all([user1, user2]) + with pytest.raises(errors.IntegrityError): + user_detail_api.put( + context_factory(request={'name': 'her'}, user=user1), + 'me') + with pytest.raises(errors.IntegrityError): + user_detail_api.put( + context_factory(request={'name': 'HER'}, user=user1), 'me') + +def test_mods_trying_to_become_admin( + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'privileges': { + 'users:edit:self:rank': 'mod', + 'users:edit:any:rank': 'admin', + }, + }) + user1 = user_factory(name='u1', rank='mod') + user2 = user_factory(name='u2', rank='mod') + session.add_all([user1, user2]) + context = context_factory(request={'rank': 'admin'}, user=user1) + with pytest.raises(errors.AuthError): + user_detail_api.put(context, user1.name) + with pytest.raises(errors.AuthError): + user_detail_api.put(context, user2.name) + +def test_uploading_avatar( + tmpdir, + session, + config_injector, + context_factory, + user_factory, + user_detail_api): + config_injector({ + 'data_dir': str(tmpdir.mkdir('data')), + 'data_url': 'http://example.com/data/', + 'thumbnails': {'avatar_width': 200, 'avatar_height': 200}, + 'ranks': ['anonymous', 'regular_user', 'mod', 'admin'], + 'rank_names': {}, + 'privileges': {'users:edit:self:avatar': 'mod'}, + }) + user = user_factory(name='u1', rank='mod') + session.add(user) + empty_pixel = \ + b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00' \ + b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00' \ + b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b' + response = user_detail_api.put( + context_factory( + request={'avatarStyle': 'manual'}, + files={'avatar': empty_pixel}, + user=user), + 'u1') + user = session.query(db.User).filter_by(name='u1').one() + assert user.avatar_style == user.AVATAR_MANUAL + assert response['user']['avatarUrl'] == \ + 'http://example.com/data/avatars/u1.jpg' diff --git a/server/szurubooru/tests/api/util.py b/server/szurubooru/tests/api/util.py deleted file mode 100644 index 3621b39b..00000000 --- a/server/szurubooru/tests/api/util.py +++ /dev/null @@ -1,42 +0,0 @@ -from datetime import datetime -from szurubooru import config, db -from szurubooru.util import misc - -def mock_config(config_mock): - config.config = config_mock - -def mock_user(name, rank='admin'): - user = db.User() - user.name = name - user.password = 'dummy' - user.password_salt = 'dummy' - user.password_hash = 'dummy' - user.email = 'dummy' - user.rank = rank - user.creation_time = datetime(1997, 1, 1) - user.avatar_style = db.User.AVATAR_GRAVATAR - return user - -def mock_context(parent): - context = misc.dotdict() - context.session = parent.session - context.request = {} - context.files = {} - context.user = db.User() - parent.context = context - -def mock_params(context, params): - def get_param_as_string(key, default=None, required=False): - if key not in params: - if required: - raise RuntimeError('Param is missing!') - return default - return params[key] - def get_param_as_int(key, default=None, required=False): - if key not in params: - if required: - raise RuntimeError('Param is missing!') - return default - return int(params[key]) - context.get_param_as_string = get_param_as_string - context.get_param_as_int = get_param_as_int diff --git a/server/szurubooru/tests/conftest.py b/server/szurubooru/tests/conftest.py new file mode 100644 index 00000000..7e1851c4 --- /dev/null +++ b/server/szurubooru/tests/conftest.py @@ -0,0 +1,61 @@ +from datetime import datetime +import pytest +import sqlalchemy +from szurubooru import db, config +from szurubooru.util import misc + +@pytest.fixture +def session(): + engine = sqlalchemy.create_engine('sqlite:///:memory:') + session_maker = sqlalchemy.orm.sessionmaker(bind=engine) + session_instance = sqlalchemy.orm.scoped_session(session_maker) + db.Base.query = session_instance.query_property() + db.Base.metadata.create_all(bind=engine) + return session_instance + +@pytest.fixture +def context_factory(session): + def factory(request=None, params=None, files=None, user=None): + params = params or {} + def get_param_as_string(key, default=None, required=False): + if key not in params: + if required: + raise RuntimeError('Param is missing!') + return default + return params[key] + def get_param_as_int(key, default=None, required=False): + if key not in params: + if required: + raise RuntimeError('Param is missing!') + return default + return int(params[key]) + context = misc.dotdict() + context.session = session + context.request = request or {} + context.files = files or {} + context.user = user or db.User() + context.get_param_as_string = get_param_as_string + context.get_param_as_int = get_param_as_int + return context + return factory + +@pytest.fixture +def config_injector(): + def injector(new_config_content): + config.config = new_config_content + return injector + +@pytest.fixture +def user_factory(): + def factory(name='dummy', rank='regular_user'): + user = db.User() + user.name = name + user.password = 'dummy' + user.password_salt = 'dummy' + user.password_hash = 'dummy' + user.email = 'dummy' + user.rank = rank + user.creation_time = datetime(1997, 1, 1) + user.avatar_style = db.User.AVATAR_GRAVATAR + return user + return factory diff --git a/server/szurubooru/tests/database_test_case.py b/server/szurubooru/tests/database_test_case.py deleted file mode 100644 index e6336d96..00000000 --- a/server/szurubooru/tests/database_test_case.py +++ /dev/null @@ -1,11 +0,0 @@ -import unittest -import sqlalchemy -from szurubooru import db - -class DatabaseTestCase(unittest.TestCase): - def setUp(self): - engine = sqlalchemy.create_engine('sqlite:///:memory:') - session_maker = sqlalchemy.orm.sessionmaker(bind=engine) - self.session = sqlalchemy.orm.scoped_session(session_maker) - db.Base.query = self.session.query_property() - db.Base.metadata.create_all(bind=engine) diff --git a/server/szurubooru/tests/search/__init__.py b/server/szurubooru/tests/search/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/server/szurubooru/tests/search/test_user_search_config.py b/server/szurubooru/tests/search/test_user_search_config.py index c3a9ce84..07c9d009 100644 --- a/server/szurubooru/tests/search/test_user_search_config.py +++ b/server/szurubooru/tests/search/test_user_search_config.py @@ -1,192 +1,170 @@ from datetime import datetime -from szurubooru import errors, search -from szurubooru.tests.database_test_case import DatabaseTestCase -from szurubooru.tests.api import util +import pytest +from szurubooru import db, errors, search -class TestUserSearchExecutor(DatabaseTestCase): - def setUp(self): - super().setUp() - self.search_config = search.UserSearchConfig() - self.executor = search.SearchExecutor(self.search_config) +def mock_user(name): + user = db.User() + user.name = name + user.password = 'dummy' + user.password_salt = 'dummy' + user.password_hash = 'dummy' + user.email = 'dummy' + user.rank = 'dummy' + user.creation_time = datetime(1997, 1, 1) + user.avatar_style = db.User.AVATAR_GRAVATAR + return user - def _test(self, query, page, page_size, expected_count, expected_user_names): - count, users = self.executor.execute(self.session, query, page, page_size) - self.assertEqual(count, expected_count) - self.assertEqual([u.name for u in users], expected_user_names) +@pytest.fixture +def executor(session): + search_config = search.UserSearchConfig() + return search.SearchExecutor(search_config) - def _test_raises(self, query, page, page_size): - self.assertRaises( - errors.SearchError, - self.executor.execute, - self.session, - query, - page, - page_size) +@pytest.fixture +def verify_unpaged(session, executor): + def verify(input, expected_user_names): + actual_count, actual_users = executor.execute( + session, input, page=1, page_size=100) + actual_user_names = [u.name for u in actual_users] + assert actual_count == len(expected_user_names) + assert actual_user_names == expected_user_names + return verify - def test_filter_by_creation_time(self): - user1 = util.mock_user('u1') - user2 = util.mock_user('u2') - user1.creation_time = datetime(2014, 1, 1) - user2.creation_time = datetime(2015, 1, 1) - self.session.add_all([user1, user2]) - for alias in ['creation-time', 'creation-date']: - self._test('%s:2014' % alias, 1, 100, 1, ['u1']) +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize('input,expected_user_names', [ + ('creation-time:2014', ['u1', 'u2']), + ('creation-date:2014', ['u1', 'u2']), + ('-creation-time:2014', ['u3']), + ('-creation-date:2014', ['u3']), + ('creation-time:2014..2014-06', ['u1', 'u2']), + ('creation-time:2014-06..2015-01-01', ['u2', 'u3']), + ('creation-time:2014-06..', ['u2', 'u3']), + ('creation-time:..2014-06', ['u1', 'u2']), + ('-creation-time:2014..2014-06', ['u3']), + ('-creation-time:2014-06..2015-01-01', ['u1']), + ('creation-date:2014..2014-06', ['u1', 'u2']), + ('creation-date:2014-06..2015-01-01', ['u2', 'u3']), + ('creation-date:2014-06..', ['u2', 'u3']), + ('creation-date:..2014-06', ['u1', 'u2']), + ('-creation-date:2014..2014-06', ['u3']), + ('-creation-date:2014-06..2015-01-01', ['u1']), + ('creation-time:2014-01,2015', ['u1', 'u3']), + ('creation-date:2014-01,2015', ['u1', 'u3']), + ('-creation-time:2014-01,2015', ['u2']), + ('-creation-date:2014-01,2015', ['u2']), +]) +def test_filter_by_creation_time( + verify_unpaged, session, input, expected_user_names): + user1 = mock_user('u1') + user2 = mock_user('u2') + user3 = mock_user('u3') + user1.creation_time = datetime(2014, 1, 1) + user2.creation_time = datetime(2014, 6, 1) + user3.creation_time = datetime(2015, 1, 1) + session.add_all([user1, user2, user3]) + verify_unpaged(input, expected_user_names) - def test_filter_by_negated_creation_time(self): - user1 = util.mock_user('u1') - user2 = util.mock_user('u2') - user1.creation_time = datetime(2014, 1, 1) - user2.creation_time = datetime(2015, 1, 1) - self.session.add_all([user1, user2]) - for alias in ['creation-time', 'creation-date']: - self._test('-%s:2014' % alias, 1, 100, 1, ['u2']) +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize('input,expected_user_names', [ + ('name:user1', ['user1']), + ('name:user2', ['user2']), + ('name:none', []), + ('name:', []), + ('name:*1', ['user1']), + ('name:*2', ['user2']), + ('name:*', ['user1', 'user2', 'user3']), + ('name:u*', ['user1', 'user2', 'user3']), + ('name:*ser*', ['user1', 'user2', 'user3']), + ('name:*zer*', []), + ('name:zer*', []), + ('name:*zer', []), + ('-name:user1', ['user2', 'user3']), + ('-name:user2', ['user1', 'user3']), + ('name:user1,user2', ['user1', 'user2']), + ('-name:user1,user3', ['user2']), +]) +def test_filter_by_name(session, verify_unpaged, input, expected_user_names): + session.add(mock_user('user1')) + session.add(mock_user('user2')) + session.add(mock_user('user3')) + verify_unpaged(input, expected_user_names) - def test_filter_by_ranged_creation_time(self): - user1 = util.mock_user('u1') - user2 = util.mock_user('u2') - user3 = util.mock_user('u3') - user1.creation_time = datetime(2014, 1, 1) - user2.creation_time = datetime(2014, 6, 1) - user3.creation_time = datetime(2015, 1, 1) - self.session.add_all([user1, user2, user3]) - for alias in ['creation-time', 'creation-date']: - self._test('%s:2014..2014-06' % alias, 1, 100, 2, ['u1', 'u2']) - self._test('%s:2014-06..2015-01-01' % alias, 1, 100, 2, ['u2', 'u3']) - self._test('%s:2014-06..' % alias, 1, 100, 2, ['u2', 'u3']) - self._test('%s:..2014-06' % alias, 1, 100, 2, ['u1', 'u2']) - self._test_raises('%s:..' % alias, 1, 100) +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize('input,expected_user_names', [ + ('', ['u1', 'u2']), + ('u1', ['u1']), + ('u2', ['u2']), + ('u1,u2', ['u1', 'u2']), +]) +def test_anonymous(session, verify_unpaged, input, expected_user_names): + session.add(mock_user('u1')) + session.add(mock_user('u2')) + verify_unpaged(input, expected_user_names) - def test_filter_by_negated_ranged_creation_time(self): - user1 = util.mock_user('u1') - user2 = util.mock_user('u2') - user3 = util.mock_user('u3') - user1.creation_time = datetime(2014, 1, 1) - user2.creation_time = datetime(2014, 6, 1) - user3.creation_time = datetime(2015, 1, 1) - self.session.add_all([user1, user2, user3]) - for alias in ['creation-time', 'creation-date']: - self._test('-%s:2014..2014-06' % alias, 1, 100, 1, ['u3']) - self._test('-%s:2014-06..2015-01-01' % alias, 1, 100, 1, ['u1']) +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize('input,expected_user_names', [ + ('creation-time:2014 u1', ['u1']), + ('creation-time:2014 u2', ['u2']), + ('creation-time:2016 u2', []), +]) +def test_combining_tokens(session, verify_unpaged, input, expected_user_names): + user1 = mock_user('u1') + user2 = mock_user('u2') + user3 = mock_user('u3') + user1.creation_time = datetime(2014, 1, 1) + user2.creation_time = datetime(2014, 6, 1) + user3.creation_time = datetime(2015, 1, 1) + session.add_all([user1, user2, user3]) + verify_unpaged(input, expected_user_names) - def test_filter_by_composite_creation_time(self): - user1 = util.mock_user('u1') - user2 = util.mock_user('u2') - user3 = util.mock_user('u3') - user1.creation_time = datetime(2014, 1, 1) - user2.creation_time = datetime(2014, 6, 1) - user3.creation_time = datetime(2015, 1, 1) - self.session.add_all([user1, user2, user3]) - for alias in ['creation-time', 'creation-date']: - self._test('%s:2014-01,2015' % alias, 1, 100, 2, ['u1', 'u3']) +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize( + 'page,page_size,expected_total_count,expected_user_names', [ + (1, 1, 2, ['u1']), + (2, 1, 2, ['u2']), + (3, 1, 2, []), + (0, 1, 2, ['u1']), + (0, 0, 2, ['u1']), + ]) +def test_paging( + session, executor, page, page_size, + expected_total_count, expected_user_names): + session.add(mock_user('u1')) + session.add(mock_user('u2')) + actual_count, actual_users = executor.execute( + session, '', page=page, page_size=page_size) + actual_user_names = [u.name for u in actual_users] + assert actual_count == expected_total_count + assert actual_user_names == expected_user_names - def test_filter_by_negated_composite_creation_time(self): - user1 = util.mock_user('u1') - user2 = util.mock_user('u2') - user3 = util.mock_user('u3') - user1.creation_time = datetime(2014, 1, 1) - user2.creation_time = datetime(2014, 6, 1) - user3.creation_time = datetime(2015, 1, 1) - self.session.add_all([user1, user2, user3]) - for alias in ['creation-time', 'creation-date']: - self._test('-%s:2014-01,2015' % alias, 1, 100, 1, ['u2']) +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize('input,expected_user_names', [ + ('', ['u1', 'u2']), + ('order:name', ['u1', 'u2']), + ('-order:name', ['u2', 'u1']), + ('order:name,asc', ['u1', 'u2']), + ('order:name,desc', ['u2', 'u1']), + ('-order:name,asc', ['u2', 'u1']), + ('-order:name,desc', ['u1', 'u2']), +]) +def test_order_by_name(session, verify_unpaged, input, expected_user_names): + session.add(mock_user('u2')) + session.add(mock_user('u1')) + verify_unpaged(input, expected_user_names) - def test_filter_by_name(self): - self.session.add(util.mock_user('u1')) - self.session.add(util.mock_user('u2')) - self._test('name:u1', 1, 100, 1, ['u1']) - self._test('name:u2', 1, 100, 1, ['u2']) - - def test_filter_by_name_wildcards(self): - self.session.add(util.mock_user('user1')) - self.session.add(util.mock_user('user2')) - self._test('name:*1', 1, 100, 1, ['user1']) - self._test('name:*2', 1, 100, 1, ['user2']) - self._test('name:*', 1, 100, 2, ['user1', 'user2']) - self._test('name:u*', 1, 100, 2, ['user1', 'user2']) - self._test('name:*ser*', 1, 100, 2, ['user1', 'user2']) - self._test('name:*zer*', 1, 100, 0, []) - self._test('name:zer*', 1, 100, 0, []) - self._test('name:*zer', 1, 100, 0, []) - - def test_filter_by_negated_name(self): - self.session.add(util.mock_user('u1')) - self.session.add(util.mock_user('u2')) - self._test('-name:u1', 1, 100, 1, ['u2']) - self._test('-name:u2', 1, 100, 1, ['u1']) - - def test_filter_by_composite_name(self): - self.session.add(util.mock_user('u1')) - self.session.add(util.mock_user('u2')) - self.session.add(util.mock_user('u3')) - self._test('name:u1,u2', 1, 100, 2, ['u1', 'u2']) - - def test_filter_by_negated_composite_name(self): - self.session.add(util.mock_user('u1')) - self.session.add(util.mock_user('u2')) - self.session.add(util.mock_user('u3')) - self._test('-name:u1,u3', 1, 100, 1, ['u2']) - - def test_filter_by_ranged_name(self): - self._test_raises('name:u1..u2', 1, 100) - - def test_paging(self): - self.session.add(util.mock_user('u1')) - self.session.add(util.mock_user('u2')) - self._test('', 1, 1, 2, ['u1']) - self._test('', 2, 1, 2, ['u2']) - self._test('', 3, 1, 2, []) - self._test('', 0, 1, 2, ['u1']) - self._test('', 0, 0, 2, ['u1']) - - def test_order_by_name(self): - self.session.add(util.mock_user('u2')) - self.session.add(util.mock_user('u1')) - self._test('', 1, 100, 2, ['u1', 'u2']) - self._test('order:name', 1, 100, 2, ['u1', 'u2']) - self._test('-order:name', 1, 100, 2, ['u2', 'u1']) - self._test('order:name,asc', 1, 100, 2, ['u1', 'u2']) - self._test('order:name,desc', 1, 100, 2, ['u2', 'u1']) - self._test('-order:name,asc', 1, 100, 2, ['u2', 'u1']) - self._test('-order:name,desc', 1, 100, 2, ['u1', 'u2']) - - def test_invalid_tokens(self): - for query in [ - 'order:', - 'order:nam', - 'order:name,as', - 'order:name,asc,desc', - 'bad:x', - 'special:unsupported']: - self._test_raises(query, 1, 100) - - def test_anonymous(self): - self.session.add(util.mock_user('u1')) - self.session.add(util.mock_user('u2')) - self._test('u1', 1, 100, 1, ['u1']) - self._test('u2', 1, 100, 1, ['u2']) - - def test_empty_search(self): - self.session.add(util.mock_user('u1')) - self.session.add(util.mock_user('u2')) - self._test('', 1, 100, 2, ['u1', 'u2']) - - def test_negated_anonymous(self): - self.session.add(util.mock_user('u1')) - self.session.add(util.mock_user('u2')) - self._test('-u1', 1, 100, 1, ['u2']) - self._test('-u2', 1, 100, 1, ['u1']) - - def test_combining(self): - user1 = util.mock_user('u1') - user2 = util.mock_user('u2') - user3 = util.mock_user('u3') - user1.creation_time = datetime(2014, 1, 1) - user2.creation_time = datetime(2014, 6, 1) - user3.creation_time = datetime(2015, 1, 1) - self.session.add_all([user1, user2, user3]) - self._test('creation-time:2014 u1', 1, 100, 1, ['u1']) - self._test('creation-time:2014 u2', 1, 100, 1, ['u2']) - self._test('creation-time:2016 u2', 1, 100, 0, []) - - def test_special(self): - self._test_raises('special:-', 1, 100) +# ----------------------------------------------------------------------------- +@pytest.mark.parametrize('input,expected_error', [ + ('creation-date:..', errors.SearchError), + ('creation-date:bad..', errors.ValidationError), + ('creation-date:..bad', errors.ValidationError), + ('creation-date:bad..bad', errors.ValidationError), + ('name:a..b', errors.SearchError), + ('order:', errors.SearchError), + ('order:nam', errors.SearchError), + ('order:name,as', errors.SearchError), + ('order:name,asc,desc', errors.SearchError), + ('bad:x', errors.SearchError), + ('special:unsupported', errors.SearchError), +]) +def test_bad_tokens(executor, session, input, expected_error): + with pytest.raises(expected_error): + executor.execute(session, input, page=1, page_size=100) diff --git a/server/szurubooru/tests/util/test_misc.py b/server/szurubooru/tests/util/test_misc.py index 171d483b..01c667bd 100644 --- a/server/szurubooru/tests/util/test_misc.py +++ b/server/szurubooru/tests/util/test_misc.py @@ -1,42 +1,30 @@ -import unittest +import pytest from datetime import datetime from szurubooru import errors from szurubooru.util import misc +dt = datetime + class FakeDatetime(datetime): @staticmethod def now(tz=None): return datetime(1997, 1, 2, 3, 4, 5, tzinfo=tz) -class TestParseTime(unittest.TestCase): - def test_empty(self): - self.assertRaises(errors.ValidationError, misc.parse_time_range, '') +def test_parsing_empty_date_time(): + with pytest.raises(errors.ValidationError): + misc.parse_time_range('') - def test_today(self): - misc.datetime.datetime = FakeDatetime - date_min, date_max = misc.parse_time_range('today') - self.assertEqual(date_min, datetime(1997, 1, 2, 0, 0, 0)) - self.assertEqual(date_max, datetime(1997, 1, 2, 23, 59, 59)) - - def test_yesterday(self): - misc.datetime.datetime = FakeDatetime - date_min, date_max = misc.parse_time_range('yesterday') - self.assertEqual(date_min, datetime(1997, 1, 1, 0, 0, 0)) - self.assertEqual(date_max, datetime(1997, 1, 1, 23, 59, 59)) - - def test_year(self): - date_min, date_max = misc.parse_time_range('1999') - self.assertEqual(date_min, datetime(1999, 1, 1, 0, 0, 0)) - self.assertEqual(date_max, datetime(1999, 12, 31, 23, 59, 59)) - - def test_month(self): - for text in ['1999-2', '1999-02']: - date_min, date_max = misc.parse_time_range(text) - self.assertEqual(date_min, datetime(1999, 2, 1, 0, 0, 0)) - self.assertEqual(date_max, datetime(1999, 2, 28, 23, 59, 59)) - - def test_day(self): - for text in ['1999-2-6', '1999-02-6', '1999-2-06', '1999-02-06']: - date_min, date_max = misc.parse_time_range(text) - self.assertEqual(date_min, datetime(1999, 2, 6, 0, 0, 0)) - self.assertEqual(date_max, datetime(1999, 2, 6, 23, 59, 59)) +@pytest.mark.parametrize('input,output', [ + ('today', (dt(1997, 1, 2, 0, 0, 0), dt(1997, 1, 2, 23, 59, 59))), + ('yesterday', (dt(1997, 1, 1, 0, 0, 0), dt(1997, 1, 1, 23, 59, 59))), + ('1999', (dt(1999, 1, 1, 0, 0, 0), dt(1999, 12, 31, 23, 59, 59))), + ('1999-2', (dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59))), + ('1999-02', (dt(1999, 2, 1, 0, 0, 0), dt(1999, 2, 28, 23, 59, 59))), + ('1999-2-6', (dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59))), + ('1999-02-6', (dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59))), + ('1999-2-06', (dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59))), + ('1999-02-06', (dt(1999, 2, 6, 0, 0, 0), dt(1999, 2, 6, 23, 59, 59))), +]) +def test_parsing_date_time(input, output): + misc.datetime.datetime = FakeDatetime + assert misc.parse_time_range(input) == output diff --git a/server/test b/server/test new file mode 100755 index 00000000..c6880876 --- /dev/null +++ b/server/test @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 +import pytest +import sys +pytest.main(' '.join([ + '--cov-report=term-missing', + '--cov=szurubooru', + 'szurubooru'] + sys.argv[1:]))