server/general: ditch falcon for in-house WSGI app
For quite some time, I hated Falcon's class maps approach that caused more chaos than good for Szurubooru. I've taken a look at the other frameworks (hug, flask, etc) again, but they all looked too bloated/over-engineered. I decided to just talk to WSGI myself. Regex-based routing may not be the fastest in the world, but I'm fine with response time of 10 ms for cached /posts.
This commit is contained in:
		
							parent
							
								
									d102c9bdba
								
							
						
					
					
						commit
						af62f8c45a
					
				| @ -1,6 +1,7 @@ | ||||
| [basic] | ||||
| function-rgx=^_?[a-z_][a-z0-9_]{2,}$|^test_ | ||||
| method-rgx=^[a-z_][a-z0-9_]{2,}$|^test_ | ||||
| const-rgx=^[A-Z_]+$|^_[a-zA-Z_]*$ | ||||
| good-names=ex,_,logger | ||||
| 
 | ||||
| [variables] | ||||
|  | ||||
| @ -10,7 +10,7 @@ import argparse | ||||
| import os.path | ||||
| import sys | ||||
| import waitress | ||||
| from szurubooru.app import create_app | ||||
| from szurubooru.facade import create_app | ||||
| 
 | ||||
| def main(): | ||||
|     parser = argparse.ArgumentParser('Starts szurubooru using waitress.') | ||||
|  | ||||
| @ -1,6 +1,5 @@ | ||||
| alembic>=0.8.5 | ||||
| pyyaml>=3.11 | ||||
| falcon>=0.3.0 | ||||
| psycopg2>=2.6.1 | ||||
| SQLAlchemy>=1.0.12 | ||||
| pytest>=2.9.1 | ||||
|  | ||||
| @ -1,27 +1,8 @@ | ||||
| ''' Falcon-compatible API facades. ''' | ||||
| 
 | ||||
| from szurubooru.api.password_reset_api import PasswordResetApi | ||||
| from szurubooru.api.user_api import UserListApi, UserDetailApi | ||||
| from szurubooru.api.tag_api import ( | ||||
|     TagListApi, | ||||
|     TagDetailApi, | ||||
|     TagMergeApi, | ||||
|     TagSiblingsApi) | ||||
| from szurubooru.api.tag_category_api import ( | ||||
|     TagCategoryListApi, | ||||
|     TagCategoryDetailApi, | ||||
|     DefaultTagCategoryApi) | ||||
| from szurubooru.api.comment_api import ( | ||||
|     CommentListApi, | ||||
|     CommentDetailApi, | ||||
|     CommentScoreApi) | ||||
| from szurubooru.api.post_api import ( | ||||
|     PostListApi, | ||||
|     PostDetailApi, | ||||
|     PostFeatureApi, | ||||
|     PostScoreApi, | ||||
|     PostFavoriteApi, | ||||
|     PostsAroundApi) | ||||
| from szurubooru.api.snapshot_api import SnapshotListApi | ||||
| from szurubooru.api.info_api import InfoApi | ||||
| from szurubooru.api.context import Context, Request | ||||
| import szurubooru.api.info_api | ||||
| import szurubooru.api.user_api | ||||
| import szurubooru.api.post_api | ||||
| import szurubooru.api.tag_api | ||||
| import szurubooru.api.tag_category_api | ||||
| import szurubooru.api.comment_api | ||||
| import szurubooru.api.password_reset_api | ||||
| import szurubooru.api.snapshot_api | ||||
|  | ||||
| @ -1,27 +0,0 @@ | ||||
| import types | ||||
| 
 | ||||
| def _bind_method(target, desired_method_name): | ||||
|     actual_method = getattr(target, desired_method_name) | ||||
|     def _wrapper_method(_self, request, _response, *args, **kwargs): | ||||
|         request.context.output = \ | ||||
|             actual_method(request.context, *args, **kwargs) | ||||
|     return types.MethodType(_wrapper_method, target) | ||||
| 
 | ||||
| class BaseApi(object): | ||||
|     ''' | ||||
|     A wrapper around falcon's API interface that eases input and output | ||||
|     management. | ||||
|     ''' | ||||
| 
 | ||||
|     def __init__(self): | ||||
|         self._translate_routes() | ||||
| 
 | ||||
|     def _translate_routes(self): | ||||
|         for method_name in ['GET', 'PUT', 'POST', 'DELETE']: | ||||
|             desired_method_name = method_name.lower() | ||||
|             falcon_method_name = 'on_%s' % method_name.lower() | ||||
|             if hasattr(self, desired_method_name): | ||||
|                 setattr( | ||||
|                     self, | ||||
|                     falcon_method_name, | ||||
|                     _bind_method(self, desired_method_name)) | ||||
| @ -1,7 +1,9 @@ | ||||
| import datetime | ||||
| from szurubooru import search | ||||
| from szurubooru.api.base_api import BaseApi | ||||
| from szurubooru.func import auth, comments, posts, scores, util | ||||
| from szurubooru.rest import routes | ||||
| 
 | ||||
| _search_executor = search.Executor(search.configs.CommentSearchConfig()) | ||||
| 
 | ||||
| def _serialize(ctx, comment, **kwargs): | ||||
|     return comments.serialize_comment( | ||||
| @ -9,67 +11,65 @@ def _serialize(ctx, comment, **kwargs): | ||||
|         ctx.user, | ||||
|         options=util.get_serialization_options(ctx), **kwargs) | ||||
| 
 | ||||
| class CommentListApi(BaseApi): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self._search_executor = search.Executor( | ||||
|             search.configs.CommentSearchConfig()) | ||||
| @routes.get('/comments/?') | ||||
| def get_comments(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'comments:list') | ||||
|     return _search_executor.execute_and_serialize( | ||||
|         ctx, lambda comment: _serialize(ctx, comment)) | ||||
| 
 | ||||
|     def get(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'comments:list') | ||||
|         return self._search_executor.execute_and_serialize( | ||||
|             ctx, | ||||
|             lambda comment: _serialize(ctx, comment)) | ||||
| @routes.post('/comments/?') | ||||
| def create_comment(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'comments:create') | ||||
|     text = ctx.get_param_as_string('text', required=True) | ||||
|     post_id = ctx.get_param_as_int('postId', required=True) | ||||
|     post = posts.get_post_by_id(post_id) | ||||
|     comment = comments.create_comment(ctx.user, post, text) | ||||
|     ctx.session.add(comment) | ||||
|     ctx.session.commit() | ||||
|     return _serialize(ctx, comment) | ||||
| 
 | ||||
|     def post(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'comments:create') | ||||
|         text = ctx.get_param_as_string('text', required=True) | ||||
|         post_id = ctx.get_param_as_int('postId', required=True) | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         comment = comments.create_comment(ctx.user, post, text) | ||||
|         ctx.session.add(comment) | ||||
|         ctx.session.commit() | ||||
|         return _serialize(ctx, comment) | ||||
| @routes.get('/comment/(?P<comment_id>[^/]+)/?') | ||||
| def get_comment(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'comments:view') | ||||
|     comment = comments.get_comment_by_id(params['comment_id']) | ||||
|     return _serialize(ctx, comment) | ||||
| 
 | ||||
| class CommentDetailApi(BaseApi): | ||||
|     def get(self, ctx, comment_id): | ||||
|         auth.verify_privilege(ctx.user, 'comments:view') | ||||
|         comment = comments.get_comment_by_id(comment_id) | ||||
|         return _serialize(ctx, comment) | ||||
| @routes.put('/comment/(?P<comment_id>[^/]+)/?') | ||||
| def update_comment(ctx, params): | ||||
|     comment = comments.get_comment_by_id(params['comment_id']) | ||||
|     util.verify_version(comment, ctx) | ||||
|     infix = 'own' if ctx.user.user_id == comment.user_id else 'any' | ||||
|     text = ctx.get_param_as_string('text', required=True) | ||||
|     auth.verify_privilege(ctx.user, 'comments:edit:%s' % infix) | ||||
|     comments.update_comment_text(comment, text) | ||||
|     util.bump_version(comment) | ||||
|     comment.last_edit_time = datetime.datetime.utcnow() | ||||
|     ctx.session.commit() | ||||
|     return _serialize(ctx, comment) | ||||
| 
 | ||||
|     def put(self, ctx, comment_id): | ||||
|         comment = comments.get_comment_by_id(comment_id) | ||||
|         util.verify_version(comment, ctx) | ||||
|         infix = 'own' if ctx.user.user_id == comment.user_id else 'any' | ||||
|         text = ctx.get_param_as_string('text', required=True) | ||||
|         auth.verify_privilege(ctx.user, 'comments:edit:%s' % infix) | ||||
|         comments.update_comment_text(comment, text) | ||||
|         util.bump_version(comment) | ||||
|         comment.last_edit_time = datetime.datetime.utcnow() | ||||
|         ctx.session.commit() | ||||
|         return _serialize(ctx, comment) | ||||
| @routes.delete('/comment/(?P<comment_id>[^/]+)/?') | ||||
| def delete_comment(ctx, params): | ||||
|     comment = comments.get_comment_by_id(params['comment_id']) | ||||
|     util.verify_version(comment, ctx) | ||||
|     infix = 'own' if ctx.user.user_id == comment.user_id else 'any' | ||||
|     auth.verify_privilege(ctx.user, 'comments:delete:%s' % infix) | ||||
|     ctx.session.delete(comment) | ||||
|     ctx.session.commit() | ||||
|     return {} | ||||
| 
 | ||||
|     def delete(self, ctx, comment_id): | ||||
|         comment = comments.get_comment_by_id(comment_id) | ||||
|         util.verify_version(comment, ctx) | ||||
|         infix = 'own' if ctx.user.user_id == comment.user_id else 'any' | ||||
|         auth.verify_privilege(ctx.user, 'comments:delete:%s' % infix) | ||||
|         ctx.session.delete(comment) | ||||
|         ctx.session.commit() | ||||
|         return {} | ||||
| @routes.put('/comment/(?P<comment_id>[^/]+)/score/?') | ||||
| def set_comment_score(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'comments:score') | ||||
|     score = ctx.get_param_as_int('score', required=True) | ||||
|     comment = comments.get_comment_by_id(params['comment_id']) | ||||
|     scores.set_score(comment, ctx.user, score) | ||||
|     ctx.session.commit() | ||||
|     return _serialize(ctx, comment) | ||||
| 
 | ||||
| class CommentScoreApi(BaseApi): | ||||
|     def put(self, ctx, comment_id): | ||||
|         auth.verify_privilege(ctx.user, 'comments:score') | ||||
|         score = ctx.get_param_as_int('score', required=True) | ||||
|         comment = comments.get_comment_by_id(comment_id) | ||||
|         scores.set_score(comment, ctx.user, score) | ||||
|         ctx.session.commit() | ||||
|         return _serialize(ctx, comment) | ||||
| 
 | ||||
|     def delete(self, ctx, comment_id): | ||||
|         auth.verify_privilege(ctx.user, 'comments:score') | ||||
|         comment = comments.get_comment_by_id(comment_id) | ||||
|         scores.delete_score(comment, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         return _serialize(ctx, comment) | ||||
| @routes.delete('/comment/(?P<comment_id>[^/]+)/score/?') | ||||
| def delete_comment_score(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'comments:score') | ||||
|     comment = comments.get_comment_by_id(params['comment_id']) | ||||
|     scores.delete_score(comment, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     return _serialize(ctx, comment) | ||||
|  | ||||
| @ -1,47 +1,46 @@ | ||||
| import datetime | ||||
| import os | ||||
| from szurubooru import config | ||||
| from szurubooru.api.base_api import BaseApi | ||||
| from szurubooru.func import posts, users, util | ||||
| from szurubooru.rest import routes | ||||
| 
 | ||||
| class InfoApi(BaseApi): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self._cache_time = None | ||||
|         self._cache_result = None | ||||
| _cache_time = None | ||||
| _cache_result = None | ||||
| 
 | ||||
|     def get(self, ctx): | ||||
|         post_feature = posts.try_get_current_post_feature() | ||||
|         return { | ||||
|             'postCount': posts.get_post_count(), | ||||
|             'diskUsage': self._get_disk_usage(), | ||||
|             'featuredPost': posts.serialize_post(post_feature.post, ctx.user) \ | ||||
|                 if post_feature else None, | ||||
|             'featuringTime': post_feature.time if post_feature else None, | ||||
|             'featuringUser': users.serialize_user(post_feature.user, ctx.user) \ | ||||
|                 if post_feature else None, | ||||
|             'serverTime': datetime.datetime.utcnow(), | ||||
|             'config': { | ||||
|                 'userNameRegex': config.config['user_name_regex'], | ||||
|                 'passwordRegex': config.config['password_regex'], | ||||
|                 'tagNameRegex': config.config['tag_name_regex'], | ||||
|                 'tagCategoryNameRegex': config.config['tag_category_name_regex'], | ||||
|                 'defaultUserRank': config.config['default_rank'], | ||||
|                 'privileges': util.snake_case_to_lower_camel_case_keys( | ||||
|                     config.config['privileges']), | ||||
|             }, | ||||
|         } | ||||
| def _get_disk_usage(): | ||||
|     global _cache_time, _cache_result # pylint: disable=global-statement | ||||
|     threshold = datetime.timedelta(hours=1) | ||||
|     now = datetime.datetime.utcnow() | ||||
|     if _cache_time and _cache_time > now - threshold: | ||||
|         return _cache_result | ||||
|     total_size = 0 | ||||
|     for dir_path, _, file_names in os.walk(config.config['data_dir']): | ||||
|         for file_name in file_names: | ||||
|             file_path = os.path.join(dir_path, file_name) | ||||
|             total_size += os.path.getsize(file_path) | ||||
|     _cache_time = now | ||||
|     _cache_result = total_size | ||||
|     return total_size | ||||
| 
 | ||||
|     def _get_disk_usage(self): | ||||
|         threshold = datetime.timedelta(hours=1) | ||||
|         now = datetime.datetime.utcnow() | ||||
|         if self._cache_time and self._cache_time > now - threshold: | ||||
|             return self._cache_result | ||||
|         total_size = 0 | ||||
|         for dir_path, _, file_names in os.walk(config.config['data_dir']): | ||||
|             for file_name in file_names: | ||||
|                 file_path = os.path.join(dir_path, file_name) | ||||
|                 total_size += os.path.getsize(file_path) | ||||
|         self._cache_time = now | ||||
|         self._cache_result = total_size | ||||
|         return total_size | ||||
| @routes.get('/info/?') | ||||
| def get_info(ctx, _params=None): | ||||
|     post_feature = posts.try_get_current_post_feature() | ||||
|     return { | ||||
|         'postCount': posts.get_post_count(), | ||||
|         'diskUsage': _get_disk_usage(), | ||||
|         'featuredPost': posts.serialize_post(post_feature.post, ctx.user) \ | ||||
|             if post_feature else None, | ||||
|         'featuringTime': post_feature.time if post_feature else None, | ||||
|         'featuringUser': users.serialize_user(post_feature.user, ctx.user) \ | ||||
|             if post_feature else None, | ||||
|         'serverTime': datetime.datetime.utcnow(), | ||||
|         'config': { | ||||
|             'userNameRegex': config.config['user_name_regex'], | ||||
|             'passwordRegex': config.config['password_regex'], | ||||
|             'tagNameRegex': config.config['tag_name_regex'], | ||||
|             'tagCategoryNameRegex': config.config['tag_category_name_regex'], | ||||
|             'defaultUserRank': config.config['default_rank'], | ||||
|             'privileges': util.snake_case_to_lower_camel_case_keys( | ||||
|                 config.config['privileges']), | ||||
|         }, | ||||
|     } | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| from szurubooru import config, errors | ||||
| from szurubooru.api.base_api import BaseApi | ||||
| from szurubooru.func import auth, mailer, users, util | ||||
| from szurubooru.rest import routes | ||||
| 
 | ||||
| MAIL_SUBJECT = 'Password reset for {name}' | ||||
| MAIL_BODY = \ | ||||
| @ -8,32 +8,35 @@ MAIL_BODY = \ | ||||
|     'If you wish to proceed, click this link: {url}\n' \ | ||||
|     'Otherwise, please ignore this email.' | ||||
| 
 | ||||
| class PasswordResetApi(BaseApi): | ||||
|     def get(self, _ctx, user_name): | ||||
|         ''' Send a mail with secure token to the correlated user. ''' | ||||
|         user = users.get_user_by_name_or_email(user_name) | ||||
|         if not user.email: | ||||
|             raise errors.ValidationError( | ||||
|                 'User %r hasn\'t supplied email. Cannot reset password.' % ( | ||||
|                     user_name)) | ||||
|         token = auth.generate_authentication_token(user) | ||||
|         url = '%s/password-reset/%s:%s' % ( | ||||
|             config.config['base_url'].rstrip('/'), user.name, token) | ||||
|         mailer.send_mail( | ||||
|             'noreply@%s' % config.config['name'], | ||||
|             user.email, | ||||
|             MAIL_SUBJECT.format(name=config.config['name']), | ||||
|             MAIL_BODY.format(name=config.config['name'], url=url)) | ||||
|         return {} | ||||
| @routes.get('/password-reset/(?P<user_name>[^/]+)/?') | ||||
| def start_password_reset(_ctx, params): | ||||
|     ''' Send a mail with secure token to the correlated user. ''' | ||||
|     user_name = params['user_name'] | ||||
|     user = users.get_user_by_name_or_email(user_name) | ||||
|     if not user.email: | ||||
|         raise errors.ValidationError( | ||||
|             'User %r hasn\'t supplied email. Cannot reset password.' % ( | ||||
|                 user_name)) | ||||
|     token = auth.generate_authentication_token(user) | ||||
|     url = '%s/password-reset/%s:%s' % ( | ||||
|         config.config['base_url'].rstrip('/'), user.name, token) | ||||
|     mailer.send_mail( | ||||
|         'noreply@%s' % config.config['name'], | ||||
|         user.email, | ||||
|         MAIL_SUBJECT.format(name=config.config['name']), | ||||
|         MAIL_BODY.format(name=config.config['name'], url=url)) | ||||
|     return {} | ||||
| 
 | ||||
|     def post(self, ctx, user_name): | ||||
|         ''' Verify token from mail, generate a new password and return it. ''' | ||||
|         user = users.get_user_by_name_or_email(user_name) | ||||
|         good_token = auth.generate_authentication_token(user) | ||||
|         token = ctx.get_param_as_string('token', required=True) | ||||
|         if token != good_token: | ||||
|             raise errors.ValidationError('Invalid password reset token.') | ||||
|         new_password = users.reset_user_password(user) | ||||
|         util.bump_version(user) | ||||
|         ctx.session.commit() | ||||
|         return {'password': new_password} | ||||
| @routes.post('/password-reset/(?P<user_name>[^/]+)/?') | ||||
| def finish_password_reset(ctx, params): | ||||
|     ''' Verify token from mail, generate a new password and return it. ''' | ||||
|     user_name = params['user_name'] | ||||
|     user = users.get_user_by_name_or_email(user_name) | ||||
|     good_token = auth.generate_authentication_token(user) | ||||
|     token = ctx.get_param_as_string('token', required=True) | ||||
|     if token != good_token: | ||||
|         raise errors.ValidationError('Invalid password reset token.') | ||||
|     new_password = users.reset_user_password(user) | ||||
|     util.bump_version(user) | ||||
|     ctx.session.commit() | ||||
|     return {'password': new_password} | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| import datetime | ||||
| from szurubooru import search | ||||
| from szurubooru.api.base_api import BaseApi | ||||
| from szurubooru.func import auth, tags, posts, snapshots, favorites, scores, util | ||||
| from szurubooru.rest import routes | ||||
| 
 | ||||
| _search_executor = search.Executor(search.configs.PostSearchConfig()) | ||||
| 
 | ||||
| def _serialize_post(ctx, post): | ||||
|     return posts.serialize_post( | ||||
| @ -9,165 +11,161 @@ def _serialize_post(ctx, post): | ||||
|         ctx.user, | ||||
|         options=util.get_serialization_options(ctx)) | ||||
| 
 | ||||
| class PostListApi(BaseApi): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self._search_executor = search.Executor( | ||||
|             search.configs.PostSearchConfig()) | ||||
| @routes.get('/posts/?') | ||||
| def get_posts(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'posts:list') | ||||
|     _search_executor.config.user = ctx.user | ||||
|     return _search_executor.execute_and_serialize( | ||||
|         ctx, lambda post: _serialize_post(ctx, post)) | ||||
| 
 | ||||
|     def get(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'posts:list') | ||||
|         self._search_executor.config.user = ctx.user | ||||
|         return self._search_executor.execute_and_serialize( | ||||
|             ctx, lambda post: _serialize_post(ctx, post)) | ||||
| @routes.post('/posts/?') | ||||
| def create_post(ctx, _params=None): | ||||
|     anonymous = ctx.get_param_as_bool('anonymous', default=False) | ||||
|     if anonymous: | ||||
|         auth.verify_privilege(ctx.user, 'posts:create:anonymous') | ||||
|     else: | ||||
|         auth.verify_privilege(ctx.user, 'posts:create:identified') | ||||
|     content = ctx.get_file('content', required=True) | ||||
|     tag_names = ctx.get_param_as_list('tags', required=True) | ||||
|     safety = ctx.get_param_as_string('safety', required=True) | ||||
|     source = ctx.get_param_as_string('source', required=False, default=None) | ||||
|     if ctx.has_param('contentUrl') and not source: | ||||
|         source = ctx.get_param_as_string('contentUrl') | ||||
|     relations = ctx.get_param_as_list('relations', required=False) or [] | ||||
|     notes = ctx.get_param_as_list('notes', required=False) or [] | ||||
|     flags = ctx.get_param_as_list('flags', required=False) or [] | ||||
| 
 | ||||
|     def post(self, ctx): | ||||
|         anonymous = ctx.get_param_as_bool('anonymous', default=False) | ||||
|         if anonymous: | ||||
|             auth.verify_privilege(ctx.user, 'posts:create:anonymous') | ||||
|         else: | ||||
|             auth.verify_privilege(ctx.user, 'posts:create:identified') | ||||
|         content = ctx.get_file('content', required=True) | ||||
|         tag_names = ctx.get_param_as_list('tags', required=True) | ||||
|         safety = ctx.get_param_as_string('safety', required=True) | ||||
|         source = ctx.get_param_as_string('source', required=False, default=None) | ||||
|         if ctx.has_param('contentUrl') and not source: | ||||
|             source = ctx.get_param_as_string('contentUrl') | ||||
|         relations = ctx.get_param_as_list('relations', required=False) or [] | ||||
|         notes = ctx.get_param_as_list('notes', required=False) or [] | ||||
|         flags = ctx.get_param_as_list('flags', required=False) or [] | ||||
|     post, new_tags = posts.create_post( | ||||
|         content, tag_names, None if anonymous else ctx.user) | ||||
|     if len(new_tags): | ||||
|         auth.verify_privilege(ctx.user, 'tags:create') | ||||
|     posts.update_post_safety(post, safety) | ||||
|     posts.update_post_source(post, source) | ||||
|     posts.update_post_relations(post, relations) | ||||
|     posts.update_post_notes(post, notes) | ||||
|     posts.update_post_flags(post, flags) | ||||
|     if ctx.has_file('thumbnail'): | ||||
|         posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) | ||||
|     ctx.session.add(post) | ||||
|     snapshots.save_entity_creation(post, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
|         post, new_tags = posts.create_post( | ||||
|             content, tag_names, None if anonymous else ctx.user) | ||||
| @routes.get('/post/(?P<post_id>[^/]+)/?') | ||||
| def get_post(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'posts:view') | ||||
|     post = posts.get_post_by_id(params['post_id']) | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
| @routes.put('/post/(?P<post_id>[^/]+)/?') | ||||
| def update_post(ctx, params): | ||||
|     post = posts.get_post_by_id(params['post_id']) | ||||
|     util.verify_version(post, ctx) | ||||
|     if ctx.has_file('content'): | ||||
|         auth.verify_privilege(ctx.user, 'posts:edit:content') | ||||
|         posts.update_post_content(post, ctx.get_file('content')) | ||||
|     if ctx.has_param('tags'): | ||||
|         auth.verify_privilege(ctx.user, 'posts:edit:tags') | ||||
|         new_tags = posts.update_post_tags(post, ctx.get_param_as_list('tags')) | ||||
|         if len(new_tags): | ||||
|             auth.verify_privilege(ctx.user, 'tags:create') | ||||
|         posts.update_post_safety(post, safety) | ||||
|         posts.update_post_source(post, source) | ||||
|         posts.update_post_relations(post, relations) | ||||
|         posts.update_post_notes(post, notes) | ||||
|         posts.update_post_flags(post, flags) | ||||
|         if ctx.has_file('thumbnail'): | ||||
|             posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) | ||||
|         ctx.session.add(post) | ||||
|         snapshots.save_entity_creation(post, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return _serialize_post(ctx, post) | ||||
|     if ctx.has_param('safety'): | ||||
|         auth.verify_privilege(ctx.user, 'posts:edit:safety') | ||||
|         posts.update_post_safety(post, ctx.get_param_as_string('safety')) | ||||
|     if ctx.has_param('source'): | ||||
|         auth.verify_privilege(ctx.user, 'posts:edit:source') | ||||
|         posts.update_post_source(post, ctx.get_param_as_string('source')) | ||||
|     elif ctx.has_param('contentUrl'): | ||||
|         posts.update_post_source(post, ctx.get_param_as_string('contentUrl')) | ||||
|     if ctx.has_param('relations'): | ||||
|         auth.verify_privilege(ctx.user, 'posts:edit:relations') | ||||
|         posts.update_post_relations(post, ctx.get_param_as_list('relations')) | ||||
|     if ctx.has_param('notes'): | ||||
|         auth.verify_privilege(ctx.user, 'posts:edit:notes') | ||||
|         posts.update_post_notes(post, ctx.get_param_as_list('notes')) | ||||
|     if ctx.has_param('flags'): | ||||
|         auth.verify_privilege(ctx.user, 'posts:edit:flags') | ||||
|         posts.update_post_flags(post, ctx.get_param_as_list('flags')) | ||||
|     if ctx.has_file('thumbnail'): | ||||
|         auth.verify_privilege(ctx.user, 'posts:edit:thumbnail') | ||||
|         posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) | ||||
|     util.bump_version(post) | ||||
|     post.last_edit_time = datetime.datetime.utcnow() | ||||
|     ctx.session.flush() | ||||
|     snapshots.save_entity_modification(post, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
| class PostDetailApi(BaseApi): | ||||
|     def get(self, ctx, post_id): | ||||
|         auth.verify_privilege(ctx.user, 'posts:view') | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         return _serialize_post(ctx, post) | ||||
| @routes.delete('/post/(?P<post_id>[^/]+)/?') | ||||
| def delete_post(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'posts:delete') | ||||
|     post = posts.get_post_by_id(params['post_id']) | ||||
|     util.verify_version(post, ctx) | ||||
|     snapshots.save_entity_deletion(post, ctx.user) | ||||
|     posts.delete(post) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return {} | ||||
| 
 | ||||
|     def put(self, ctx, post_id): | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         util.verify_version(post, ctx) | ||||
|         if ctx.has_file('content'): | ||||
|             auth.verify_privilege(ctx.user, 'posts:edit:content') | ||||
|             posts.update_post_content(post, ctx.get_file('content')) | ||||
|         if ctx.has_param('tags'): | ||||
|             auth.verify_privilege(ctx.user, 'posts:edit:tags') | ||||
|             new_tags = posts.update_post_tags(post, ctx.get_param_as_list('tags')) | ||||
|             if len(new_tags): | ||||
|                 auth.verify_privilege(ctx.user, 'tags:create') | ||||
|         if ctx.has_param('safety'): | ||||
|             auth.verify_privilege(ctx.user, 'posts:edit:safety') | ||||
|             posts.update_post_safety(post, ctx.get_param_as_string('safety')) | ||||
|         if ctx.has_param('source'): | ||||
|             auth.verify_privilege(ctx.user, 'posts:edit:source') | ||||
|             posts.update_post_source(post, ctx.get_param_as_string('source')) | ||||
|         elif ctx.has_param('contentUrl'): | ||||
|             posts.update_post_source(post, ctx.get_param_as_string('contentUrl')) | ||||
|         if ctx.has_param('relations'): | ||||
|             auth.verify_privilege(ctx.user, 'posts:edit:relations') | ||||
|             posts.update_post_relations(post, ctx.get_param_as_list('relations')) | ||||
|         if ctx.has_param('notes'): | ||||
|             auth.verify_privilege(ctx.user, 'posts:edit:notes') | ||||
|             posts.update_post_notes(post, ctx.get_param_as_list('notes')) | ||||
|         if ctx.has_param('flags'): | ||||
|             auth.verify_privilege(ctx.user, 'posts:edit:flags') | ||||
|             posts.update_post_flags(post, ctx.get_param_as_list('flags')) | ||||
|         if ctx.has_file('thumbnail'): | ||||
|             auth.verify_privilege(ctx.user, 'posts:edit:thumbnail') | ||||
|             posts.update_post_thumbnail(post, ctx.get_file('thumbnail')) | ||||
|         util.bump_version(post) | ||||
|         post.last_edit_time = datetime.datetime.utcnow() | ||||
|         ctx.session.flush() | ||||
|         snapshots.save_entity_modification(post, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return _serialize_post(ctx, post) | ||||
| @routes.get('/featured-post/?') | ||||
| def get_featured_post(ctx, _params=None): | ||||
|     post = posts.try_get_featured_post() | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
|     def delete(self, ctx, post_id): | ||||
|         auth.verify_privilege(ctx.user, 'posts:delete') | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         util.verify_version(post, ctx) | ||||
|         snapshots.save_entity_deletion(post, ctx.user) | ||||
|         posts.delete(post) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return {} | ||||
| @routes.post('/featured-post/?') | ||||
| def set_featured_post(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'posts:feature') | ||||
|     post_id = ctx.get_param_as_int('id', required=True) | ||||
|     post = posts.get_post_by_id(post_id) | ||||
|     featured_post = posts.try_get_featured_post() | ||||
|     if featured_post and featured_post.post_id == post.post_id: | ||||
|         raise posts.PostAlreadyFeaturedError( | ||||
|             'Post %r is already featured.' % post_id) | ||||
|     posts.feature_post(post, ctx.user) | ||||
|     if featured_post: | ||||
|         snapshots.save_entity_modification(featured_post, ctx.user) | ||||
|     snapshots.save_entity_modification(post, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
| class PostFeatureApi(BaseApi): | ||||
|     def post(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'posts:feature') | ||||
|         post_id = ctx.get_param_as_int('id', required=True) | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         featured_post = posts.try_get_featured_post() | ||||
|         if featured_post and featured_post.post_id == post.post_id: | ||||
|             raise posts.PostAlreadyFeaturedError( | ||||
|                 'Post %r is already featured.' % post_id) | ||||
|         posts.feature_post(post, ctx.user) | ||||
|         if featured_post: | ||||
|             snapshots.save_entity_modification(featured_post, ctx.user) | ||||
|         snapshots.save_entity_modification(post, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         return _serialize_post(ctx, post) | ||||
| @routes.put('/post/(?P<post_id>[^/]+)/score/?') | ||||
| def set_post_score(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'posts:score') | ||||
|     post = posts.get_post_by_id(params['post_id']) | ||||
|     score = ctx.get_param_as_int('score', required=True) | ||||
|     scores.set_score(post, ctx.user, score) | ||||
|     ctx.session.commit() | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
|     def get(self, ctx): | ||||
|         post = posts.try_get_featured_post() | ||||
|         return _serialize_post(ctx, post) | ||||
| @routes.delete('/post/(?P<post_id>[^/]+)/score/?') | ||||
| def delete_post_score(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'posts:score') | ||||
|     post = posts.get_post_by_id(params['post_id']) | ||||
|     scores.delete_score(post, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
| class PostScoreApi(BaseApi): | ||||
|     def put(self, ctx, post_id): | ||||
|         auth.verify_privilege(ctx.user, 'posts:score') | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         score = ctx.get_param_as_int('score', required=True) | ||||
|         scores.set_score(post, ctx.user, score) | ||||
|         ctx.session.commit() | ||||
|         return _serialize_post(ctx, post) | ||||
| @routes.post('/post/(?P<post_id>[^/]+)/favorite/?') | ||||
| def add_post_to_favorites(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'posts:favorite') | ||||
|     post = posts.get_post_by_id(params['post_id']) | ||||
|     favorites.set_favorite(post, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
|     def delete(self, ctx, post_id): | ||||
|         auth.verify_privilege(ctx.user, 'posts:score') | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         scores.delete_score(post, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         return _serialize_post(ctx, post) | ||||
| @routes.delete('/post/(?P<post_id>[^/]+)/favorite/?') | ||||
| def delete_post_from_favorites(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'posts:favorite') | ||||
|     post = posts.get_post_by_id(params['post_id']) | ||||
|     favorites.unset_favorite(post, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     return _serialize_post(ctx, post) | ||||
| 
 | ||||
| class PostFavoriteApi(BaseApi): | ||||
|     def post(self, ctx, post_id): | ||||
|         auth.verify_privilege(ctx.user, 'posts:favorite') | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         favorites.set_favorite(post, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         return _serialize_post(ctx, post) | ||||
| 
 | ||||
|     def delete(self, ctx, post_id): | ||||
|         auth.verify_privilege(ctx.user, 'posts:favorite') | ||||
|         post = posts.get_post_by_id(post_id) | ||||
|         favorites.unset_favorite(post, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         return _serialize_post(ctx, post) | ||||
| 
 | ||||
| class PostsAroundApi(BaseApi): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self._search_executor = search.Executor( | ||||
|             search.configs.PostSearchConfig()) | ||||
| 
 | ||||
|     def get(self, ctx, post_id): | ||||
|         auth.verify_privilege(ctx.user, 'posts:list') | ||||
|         self._search_executor.config.user = ctx.user | ||||
|         return self._search_executor.get_around_and_serialize( | ||||
|             ctx, post_id, lambda post: _serialize_post(ctx, post)) | ||||
| @routes.get('/post/(?P<post_id>[^/]+)/around/?') | ||||
| def get_posts_around(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'posts:list') | ||||
|     _search_executor.config.user = ctx.user | ||||
|     return _search_executor.get_around_and_serialize( | ||||
|         ctx, params['post_id'], lambda post: _serialize_post(ctx, post)) | ||||
|  | ||||
| @ -1,14 +1,12 @@ | ||||
| from szurubooru import search | ||||
| from szurubooru.api.base_api import BaseApi | ||||
| from szurubooru.func import auth, snapshots | ||||
| from szurubooru.rest import routes | ||||
| 
 | ||||
| class SnapshotListApi(BaseApi): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self._search_executor = search.Executor( | ||||
|             search.configs.SnapshotSearchConfig()) | ||||
| _search_executor = search.Executor( | ||||
|     search.configs.SnapshotSearchConfig()) | ||||
| 
 | ||||
|     def get(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'snapshots:list') | ||||
|         return self._search_executor.execute_and_serialize( | ||||
|             ctx, snapshots.serialize_snapshot) | ||||
| @routes.get('/snapshots/?') | ||||
| def get_snapshots(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'snapshots:list') | ||||
|     return _search_executor.execute_and_serialize( | ||||
|         ctx, snapshots.serialize_snapshot) | ||||
|  | ||||
| @ -1,7 +1,9 @@ | ||||
| import datetime | ||||
| from szurubooru import db, search | ||||
| from szurubooru.api.base_api import BaseApi | ||||
| from szurubooru.func import auth, tags, util, snapshots | ||||
| from szurubooru.rest import routes | ||||
| 
 | ||||
| _search_executor = search.Executor(search.configs.TagSearchConfig()) | ||||
| 
 | ||||
| def _serialize(ctx, tag): | ||||
|     return tags.serialize_tag( | ||||
| @ -17,116 +19,112 @@ def _create_if_needed(tag_names, user): | ||||
|     for tag in new_tags: | ||||
|         snapshots.save_entity_creation(tag, user) | ||||
| 
 | ||||
| class TagListApi(BaseApi): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self._search_executor = search.Executor( | ||||
|             search.configs.TagSearchConfig()) | ||||
| @routes.get('/tags/?') | ||||
| def get_tags(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'tags:list') | ||||
|     return _search_executor.execute_and_serialize( | ||||
|         ctx, lambda tag: _serialize(ctx, tag)) | ||||
| 
 | ||||
|     def get(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'tags:list') | ||||
|         return self._search_executor.execute_and_serialize( | ||||
|             ctx, lambda tag: _serialize(ctx, tag)) | ||||
| @routes.post('/tags/?') | ||||
| def create_tag(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'tags:create') | ||||
| 
 | ||||
|     def post(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'tags:create') | ||||
|     names = ctx.get_param_as_list('names', required=True) | ||||
|     category = ctx.get_param_as_string('category', required=True) | ||||
|     description = ctx.get_param_as_string( | ||||
|         'description', required=False, default=None) | ||||
|     suggestions = ctx.get_param_as_list( | ||||
|         'suggestions', required=False, default=[]) | ||||
|     implications = ctx.get_param_as_list( | ||||
|         'implications', required=False, default=[]) | ||||
| 
 | ||||
|         names = ctx.get_param_as_list('names', required=True) | ||||
|         category = ctx.get_param_as_string('category', required=True) or '' | ||||
|         description = ctx.get_param_as_string( | ||||
|             'description', required=False, default=None) | ||||
|         suggestions = ctx.get_param_as_list( | ||||
|             'suggestions', required=False, default=[]) | ||||
|         implications = ctx.get_param_as_list( | ||||
|             'implications', required=False, default=[]) | ||||
|     _create_if_needed(suggestions, ctx.user) | ||||
|     _create_if_needed(implications, ctx.user) | ||||
| 
 | ||||
|     tag = tags.create_tag(names, category, suggestions, implications) | ||||
|     tags.update_tag_description(tag, description) | ||||
|     ctx.session.add(tag) | ||||
|     ctx.session.flush() | ||||
|     snapshots.save_entity_creation(tag, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return _serialize(ctx, tag) | ||||
| 
 | ||||
| @routes.get('/tag/(?P<tag_name>[^/]+)/?') | ||||
| def get_tag(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'tags:view') | ||||
|     tag = tags.get_tag_by_name(params['tag_name']) | ||||
|     return _serialize(ctx, tag) | ||||
| 
 | ||||
| @routes.put('/tag/(?P<tag_name>[^/]+)/?') | ||||
| def update_tag(ctx, params): | ||||
|     tag = tags.get_tag_by_name(params['tag_name']) | ||||
|     util.verify_version(tag, ctx) | ||||
|     if ctx.has_param('names'): | ||||
|         auth.verify_privilege(ctx.user, 'tags:edit:names') | ||||
|         tags.update_tag_names(tag, ctx.get_param_as_list('names')) | ||||
|     if ctx.has_param('category'): | ||||
|         auth.verify_privilege(ctx.user, 'tags:edit:category') | ||||
|         tags.update_tag_category_name( | ||||
|             tag, ctx.get_param_as_string('category')) | ||||
|     if ctx.has_param('description'): | ||||
|         auth.verify_privilege(ctx.user, 'tags:edit:description') | ||||
|         tags.update_tag_description( | ||||
|             tag, ctx.get_param_as_string('description', default=None)) | ||||
|     if ctx.has_param('suggestions'): | ||||
|         auth.verify_privilege(ctx.user, 'tags:edit:suggestions') | ||||
|         suggestions = ctx.get_param_as_list('suggestions') | ||||
|         _create_if_needed(suggestions, ctx.user) | ||||
|         tags.update_tag_suggestions(tag, suggestions) | ||||
|     if ctx.has_param('implications'): | ||||
|         auth.verify_privilege(ctx.user, 'tags:edit:implications') | ||||
|         implications = ctx.get_param_as_list('implications') | ||||
|         _create_if_needed(implications, ctx.user) | ||||
|         tags.update_tag_implications(tag, implications) | ||||
|     util.bump_version(tag) | ||||
|     tag.last_edit_time = datetime.datetime.utcnow() | ||||
|     ctx.session.flush() | ||||
|     snapshots.save_entity_modification(tag, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return _serialize(ctx, tag) | ||||
| 
 | ||||
|         tag = tags.create_tag(names, category, suggestions, implications) | ||||
|         tags.update_tag_description(tag, description) | ||||
|         ctx.session.add(tag) | ||||
|         ctx.session.flush() | ||||
|         snapshots.save_entity_creation(tag, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return _serialize(ctx, tag) | ||||
| @routes.delete('/tag/(?P<tag_name>[^/]+)/?') | ||||
| def delete_tag(ctx, params): | ||||
|     tag = tags.get_tag_by_name(params['tag_name']) | ||||
|     util.verify_version(tag, ctx) | ||||
|     auth.verify_privilege(ctx.user, 'tags:delete') | ||||
|     snapshots.save_entity_deletion(tag, ctx.user) | ||||
|     tags.delete(tag) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return {} | ||||
| 
 | ||||
| class TagDetailApi(BaseApi): | ||||
|     def get(self, ctx, tag_name): | ||||
|         auth.verify_privilege(ctx.user, 'tags:view') | ||||
|         tag = tags.get_tag_by_name(tag_name) | ||||
|         return _serialize(ctx, tag) | ||||
| @routes.post('/tag-merge/?') | ||||
| def merge_tags(ctx, _params=None): | ||||
|     source_tag_name = ctx.get_param_as_string('remove', required=True) or '' | ||||
|     target_tag_name = ctx.get_param_as_string('mergeTo', required=True) or '' | ||||
|     source_tag = tags.get_tag_by_name(source_tag_name) | ||||
|     target_tag = tags.get_tag_by_name(target_tag_name) | ||||
|     util.verify_version(source_tag, ctx, 'removeVersion') | ||||
|     util.verify_version(target_tag, ctx, 'mergeToVersion') | ||||
|     auth.verify_privilege(ctx.user, 'tags:merge') | ||||
|     tags.merge_tags(source_tag, target_tag) | ||||
|     snapshots.save_entity_deletion(source_tag, ctx.user) | ||||
|     util.bump_version(target_tag) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return _serialize(ctx, target_tag) | ||||
| 
 | ||||
|     def put(self, ctx, tag_name): | ||||
|         tag = tags.get_tag_by_name(tag_name) | ||||
|         util.verify_version(tag, ctx) | ||||
|         if ctx.has_param('names'): | ||||
|             auth.verify_privilege(ctx.user, 'tags:edit:names') | ||||
|             tags.update_tag_names(tag, ctx.get_param_as_list('names')) | ||||
|         if ctx.has_param('category'): | ||||
|             auth.verify_privilege(ctx.user, 'tags:edit:category') | ||||
|             tags.update_tag_category_name( | ||||
|                 tag, ctx.get_param_as_string('category') or '') | ||||
|         if ctx.has_param('description'): | ||||
|             auth.verify_privilege(ctx.user, 'tags:edit:description') | ||||
|             tags.update_tag_description( | ||||
|                 tag, ctx.get_param_as_string('description', default=None)) | ||||
|         if ctx.has_param('suggestions'): | ||||
|             auth.verify_privilege(ctx.user, 'tags:edit:suggestions') | ||||
|             suggestions = ctx.get_param_as_list('suggestions') | ||||
|             _create_if_needed(suggestions, ctx.user) | ||||
|             tags.update_tag_suggestions(tag, suggestions) | ||||
|         if ctx.has_param('implications'): | ||||
|             auth.verify_privilege(ctx.user, 'tags:edit:implications') | ||||
|             implications = ctx.get_param_as_list('implications') | ||||
|             _create_if_needed(implications, ctx.user) | ||||
|             tags.update_tag_implications(tag, implications) | ||||
|         util.bump_version(tag) | ||||
|         tag.last_edit_time = datetime.datetime.utcnow() | ||||
|         ctx.session.flush() | ||||
|         snapshots.save_entity_modification(tag, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return _serialize(ctx, tag) | ||||
| 
 | ||||
|     def delete(self, ctx, tag_name): | ||||
|         tag = tags.get_tag_by_name(tag_name) | ||||
|         util.verify_version(tag, ctx) | ||||
|         auth.verify_privilege(ctx.user, 'tags:delete') | ||||
|         snapshots.save_entity_deletion(tag, ctx.user) | ||||
|         tags.delete(tag) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return {} | ||||
| 
 | ||||
| class TagMergeApi(BaseApi): | ||||
|     def post(self, ctx): | ||||
|         source_tag_name = ctx.get_param_as_string('remove', required=True) or '' | ||||
|         target_tag_name = ctx.get_param_as_string('mergeTo', required=True) or '' | ||||
|         source_tag = tags.get_tag_by_name(source_tag_name) | ||||
|         target_tag = tags.get_tag_by_name(target_tag_name) | ||||
|         util.verify_version(source_tag, ctx, 'removeVersion') | ||||
|         util.verify_version(target_tag, ctx, 'mergeToVersion') | ||||
|         if source_tag.tag_id == target_tag.tag_id: | ||||
|             raise tags.InvalidTagRelationError('Cannot merge tag with itself.') | ||||
|         auth.verify_privilege(ctx.user, 'tags:merge') | ||||
|         snapshots.save_entity_deletion(source_tag, ctx.user) | ||||
|         tags.merge_tags(source_tag, target_tag) | ||||
|         util.bump_version(target_tag) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return _serialize(ctx, target_tag) | ||||
| 
 | ||||
| class TagSiblingsApi(BaseApi): | ||||
|     def get(self, ctx, tag_name): | ||||
|         auth.verify_privilege(ctx.user, 'tags:view') | ||||
|         tag = tags.get_tag_by_name(tag_name) | ||||
|         result = tags.get_tag_siblings(tag) | ||||
|         serialized_siblings = [] | ||||
|         for sibling, occurrences in result: | ||||
|             serialized_siblings.append({ | ||||
|                 'tag': _serialize(ctx, sibling), | ||||
|                 'occurrences': occurrences | ||||
|             }) | ||||
|         return {'results': serialized_siblings} | ||||
| @routes.get('/tag-siblings/(?P<tag_name>[^/]+)/?') | ||||
| def get_tag_siblings(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'tags:view') | ||||
|     tag = tags.get_tag_by_name(params['tag_name']) | ||||
|     result = tags.get_tag_siblings(tag) | ||||
|     serialized_siblings = [] | ||||
|     for sibling, occurrences in result: | ||||
|         serialized_siblings.append({ | ||||
|             'tag': _serialize(ctx, sibling), | ||||
|             'occurrences': occurrences | ||||
|         }) | ||||
|     return {'results': serialized_siblings} | ||||
|  | ||||
| @ -1,70 +1,73 @@ | ||||
| from szurubooru.api.base_api import BaseApi | ||||
| from szurubooru.rest import routes | ||||
| from szurubooru.func import auth, tags, tag_categories, util, snapshots | ||||
| 
 | ||||
| def _serialize(ctx, category): | ||||
|     return tag_categories.serialize_category( | ||||
|         category, options=util.get_serialization_options(ctx)) | ||||
| 
 | ||||
| class TagCategoryListApi(BaseApi): | ||||
|     def get(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'tag_categories:list') | ||||
|         categories = tag_categories.get_all_categories() | ||||
|         return { | ||||
|             'results': [_serialize(ctx, category) for category in categories], | ||||
|         } | ||||
| @routes.get('/tag-categories/?') | ||||
| def get_tag_categories(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'tag_categories:list') | ||||
|     categories = tag_categories.get_all_categories() | ||||
|     return { | ||||
|         'results': [_serialize(ctx, category) for category in categories], | ||||
|     } | ||||
| 
 | ||||
|     def post(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'tag_categories:create') | ||||
|         name = ctx.get_param_as_string('name', required=True) | ||||
|         color = ctx.get_param_as_string('color', required=True) | ||||
|         category = tag_categories.create_category(name, color) | ||||
|         ctx.session.add(category) | ||||
|         ctx.session.flush() | ||||
|         snapshots.save_entity_creation(category, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return _serialize(ctx, category) | ||||
| @routes.post('/tag-categories/?') | ||||
| def create_tag_category(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'tag_categories:create') | ||||
|     name = ctx.get_param_as_string('name', required=True) | ||||
|     color = ctx.get_param_as_string('color', required=True) | ||||
|     category = tag_categories.create_category(name, color) | ||||
|     ctx.session.add(category) | ||||
|     ctx.session.flush() | ||||
|     snapshots.save_entity_creation(category, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return _serialize(ctx, category) | ||||
| 
 | ||||
| class TagCategoryDetailApi(BaseApi): | ||||
|     def get(self, ctx, category_name): | ||||
|         auth.verify_privilege(ctx.user, 'tag_categories:view') | ||||
|         category = tag_categories.get_category_by_name(category_name) | ||||
|         return _serialize(ctx, category) | ||||
| @routes.get('/tag-category/(?P<category_name>[^/]+)/?') | ||||
| def get_tag_category(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'tag_categories:view') | ||||
|     category = tag_categories.get_category_by_name(params['category_name']) | ||||
|     return _serialize(ctx, category) | ||||
| 
 | ||||
|     def put(self, ctx, category_name): | ||||
|         category = tag_categories.get_category_by_name(category_name) | ||||
|         util.verify_version(category, ctx) | ||||
|         if ctx.has_param('name'): | ||||
|             auth.verify_privilege(ctx.user, 'tag_categories:edit:name') | ||||
|             tag_categories.update_category_name( | ||||
|                 category, ctx.get_param_as_string('name')) | ||||
|         if ctx.has_param('color'): | ||||
|             auth.verify_privilege(ctx.user, 'tag_categories:edit:color') | ||||
|             tag_categories.update_category_color( | ||||
|                 category, ctx.get_param_as_string('color')) | ||||
|         util.bump_version(category) | ||||
|         ctx.session.flush() | ||||
|         snapshots.save_entity_modification(category, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return _serialize(ctx, category) | ||||
| @routes.put('/tag-category/(?P<category_name>[^/]+)/?') | ||||
| def update_tag_category(ctx, params): | ||||
|     category = tag_categories.get_category_by_name(params['category_name']) | ||||
|     util.verify_version(category, ctx) | ||||
|     if ctx.has_param('name'): | ||||
|         auth.verify_privilege(ctx.user, 'tag_categories:edit:name') | ||||
|         tag_categories.update_category_name( | ||||
|             category, ctx.get_param_as_string('name')) | ||||
|     if ctx.has_param('color'): | ||||
|         auth.verify_privilege(ctx.user, 'tag_categories:edit:color') | ||||
|         tag_categories.update_category_color( | ||||
|             category, ctx.get_param_as_string('color')) | ||||
|     util.bump_version(category) | ||||
|     ctx.session.flush() | ||||
|     snapshots.save_entity_modification(category, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return _serialize(ctx, category) | ||||
| 
 | ||||
|     def delete(self, ctx, category_name): | ||||
|         category = tag_categories.get_category_by_name(category_name) | ||||
|         util.verify_version(category, ctx) | ||||
|         auth.verify_privilege(ctx.user, 'tag_categories:delete') | ||||
|         tag_categories.delete_category(category) | ||||
|         snapshots.save_entity_deletion(category, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return {} | ||||
| @routes.delete('/tag-category/(?P<category_name>[^/]+)/?') | ||||
| def delete_tag_category(ctx, params): | ||||
|     category = tag_categories.get_category_by_name(params['category_name']) | ||||
|     util.verify_version(category, ctx) | ||||
|     auth.verify_privilege(ctx.user, 'tag_categories:delete') | ||||
|     tag_categories.delete_category(category) | ||||
|     snapshots.save_entity_deletion(category, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return {} | ||||
| 
 | ||||
| class DefaultTagCategoryApi(BaseApi): | ||||
|     def put(self, ctx, category_name): | ||||
|         auth.verify_privilege(ctx.user, 'tag_categories:set_default') | ||||
|         category = tag_categories.get_category_by_name(category_name) | ||||
|         tag_categories.set_default_category(category) | ||||
|         snapshots.save_entity_modification(category, ctx.user) | ||||
|         ctx.session.commit() | ||||
|         tags.export_to_json() | ||||
|         return _serialize(ctx, category) | ||||
| @routes.put('/tag-category/(?P<category_name>[^/]+)/default/?') | ||||
| def set_tag_category_as_default(ctx, params): | ||||
|     auth.verify_privilege(ctx.user, 'tag_categories:set_default') | ||||
|     category = tag_categories.get_category_by_name(params['category_name']) | ||||
|     tag_categories.set_default_category(category) | ||||
|     snapshots.save_entity_modification(category, ctx.user) | ||||
|     ctx.session.commit() | ||||
|     tags.export_to_json() | ||||
|     return _serialize(ctx, category) | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| from szurubooru import search | ||||
| from szurubooru.api.base_api import BaseApi | ||||
| from szurubooru.func import auth, users, util | ||||
| from szurubooru.rest import routes | ||||
| 
 | ||||
| _search_executor = search.Executor(search.configs.UserSearchConfig()) | ||||
| 
 | ||||
| def _serialize(ctx, user, **kwargs): | ||||
|     return users.serialize_user( | ||||
| @ -9,75 +11,73 @@ def _serialize(ctx, user, **kwargs): | ||||
|         options=util.get_serialization_options(ctx), | ||||
|         **kwargs) | ||||
| 
 | ||||
| class UserListApi(BaseApi): | ||||
|     def __init__(self): | ||||
|         super().__init__() | ||||
|         self._search_executor = search.Executor( | ||||
|             search.configs.UserSearchConfig()) | ||||
| @routes.get('/users/?') | ||||
| def get_users(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'users:list') | ||||
|     return _search_executor.execute_and_serialize( | ||||
|         ctx, lambda user: _serialize(ctx, user)) | ||||
| 
 | ||||
|     def get(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'users:list') | ||||
|         return self._search_executor.execute_and_serialize( | ||||
|             ctx, lambda user: _serialize(ctx, user)) | ||||
| @routes.post('/users/?') | ||||
| def create_user(ctx, _params=None): | ||||
|     auth.verify_privilege(ctx.user, 'users:create') | ||||
|     name = ctx.get_param_as_string('name', required=True) | ||||
|     password = ctx.get_param_as_string('password', required=True) | ||||
|     email = ctx.get_param_as_string('email', required=False, default='') | ||||
|     user = users.create_user(name, password, email) | ||||
|     if ctx.has_param('rank'): | ||||
|         users.update_user_rank( | ||||
|             user, ctx.get_param_as_string('rank'), ctx.user) | ||||
|     if ctx.has_param('avatarStyle'): | ||||
|         users.update_user_avatar( | ||||
|             user, | ||||
|             ctx.get_param_as_string('avatarStyle'), | ||||
|             ctx.get_file('avatar')) | ||||
|     ctx.session.add(user) | ||||
|     ctx.session.commit() | ||||
|     return _serialize(ctx, user, force_show_email=True) | ||||
| 
 | ||||
|     def post(self, ctx): | ||||
|         auth.verify_privilege(ctx.user, 'users:create') | ||||
|         name = ctx.get_param_as_string('name', required=True) | ||||
|         password = ctx.get_param_as_string('password', required=True) | ||||
|         email = ctx.get_param_as_string('email', required=False, default='') | ||||
|         user = users.create_user(name, password, email) | ||||
|         if ctx.has_param('rank'): | ||||
|             users.update_user_rank( | ||||
|                 user, ctx.get_param_as_string('rank'), ctx.user) | ||||
|         if ctx.has_param('avatarStyle'): | ||||
|             users.update_user_avatar( | ||||
|                 user, | ||||
|                 ctx.get_param_as_string('avatarStyle'), | ||||
|                 ctx.get_file('avatar')) | ||||
|         ctx.session.add(user) | ||||
|         ctx.session.commit() | ||||
|         return _serialize(ctx, user, force_show_email=True) | ||||
| @routes.get('/user/(?P<user_name>[^/]+)/?') | ||||
| def get_user(ctx, params): | ||||
|     user = users.get_user_by_name(params['user_name']) | ||||
|     if ctx.user.user_id != user.user_id: | ||||
|         auth.verify_privilege(ctx.user, 'users:view') | ||||
|     return _serialize(ctx, user) | ||||
| 
 | ||||
| class UserDetailApi(BaseApi): | ||||
|     def get(self, ctx, user_name): | ||||
|         user = users.get_user_by_name(user_name) | ||||
|         if ctx.user.user_id != user.user_id: | ||||
|             auth.verify_privilege(ctx.user, 'users:view') | ||||
|         return _serialize(ctx, user) | ||||
| @routes.put('/user/(?P<user_name>[^/]+)/?') | ||||
| def update_user(ctx, params): | ||||
|     user = users.get_user_by_name(params['user_name']) | ||||
|     util.verify_version(user, ctx) | ||||
|     infix = 'self' if ctx.user.user_id == user.user_id else 'any' | ||||
|     if ctx.has_param('name'): | ||||
|         auth.verify_privilege(ctx.user, 'users:edit:%s:name' % infix) | ||||
|         users.update_user_name(user, ctx.get_param_as_string('name')) | ||||
|     if ctx.has_param('password'): | ||||
|         auth.verify_privilege(ctx.user, 'users:edit:%s:pass' % infix) | ||||
|         users.update_user_password( | ||||
|             user, ctx.get_param_as_string('password')) | ||||
|     if ctx.has_param('email'): | ||||
|         auth.verify_privilege(ctx.user, 'users:edit:%s:email' % infix) | ||||
|         users.update_user_email(user, ctx.get_param_as_string('email')) | ||||
|     if ctx.has_param('rank'): | ||||
|         auth.verify_privilege(ctx.user, 'users:edit:%s:rank' % infix) | ||||
|         users.update_user_rank( | ||||
|             user, ctx.get_param_as_string('rank'), ctx.user) | ||||
|     if ctx.has_param('avatarStyle'): | ||||
|         auth.verify_privilege(ctx.user, 'users:edit:%s:avatar' % infix) | ||||
|         users.update_user_avatar( | ||||
|             user, | ||||
|             ctx.get_param_as_string('avatarStyle'), | ||||
|             ctx.get_file('avatar')) | ||||
|     util.bump_version(user) | ||||
|     ctx.session.commit() | ||||
|     return _serialize(ctx, user) | ||||
| 
 | ||||
|     def put(self, ctx, user_name): | ||||
|         user = users.get_user_by_name(user_name) | ||||
|         util.verify_version(user, ctx) | ||||
|         infix = 'self' if ctx.user.user_id == user.user_id else 'any' | ||||
|         if ctx.has_param('name'): | ||||
|             auth.verify_privilege(ctx.user, 'users:edit:%s:name' % infix) | ||||
|             users.update_user_name(user, ctx.get_param_as_string('name')) | ||||
|         if ctx.has_param('password'): | ||||
|             auth.verify_privilege(ctx.user, 'users:edit:%s:pass' % infix) | ||||
|             users.update_user_password( | ||||
|                 user, ctx.get_param_as_string('password')) | ||||
|         if ctx.has_param('email'): | ||||
|             auth.verify_privilege(ctx.user, 'users:edit:%s:email' % infix) | ||||
|             users.update_user_email(user, ctx.get_param_as_string('email')) | ||||
|         if ctx.has_param('rank'): | ||||
|             auth.verify_privilege(ctx.user, 'users:edit:%s:rank' % infix) | ||||
|             users.update_user_rank( | ||||
|                 user, ctx.get_param_as_string('rank'), ctx.user) | ||||
|         if ctx.has_param('avatarStyle'): | ||||
|             auth.verify_privilege(ctx.user, 'users:edit:%s:avatar' % infix) | ||||
|             users.update_user_avatar( | ||||
|                 user, | ||||
|                 ctx.get_param_as_string('avatarStyle'), | ||||
|                 ctx.get_file('avatar')) | ||||
|         util.bump_version(user) | ||||
|         ctx.session.commit() | ||||
|         return _serialize(ctx, user) | ||||
| 
 | ||||
|     def delete(self, ctx, user_name): | ||||
|         user = users.get_user_by_name(user_name) | ||||
|         util.verify_version(user, ctx) | ||||
|         infix = 'self' if ctx.user.user_id == user.user_id else 'any' | ||||
|         auth.verify_privilege(ctx.user, 'users:delete:%s' % infix) | ||||
|         ctx.session.delete(user) | ||||
|         ctx.session.commit() | ||||
|         return {} | ||||
| @routes.delete('/user/(?P<user_name>[^/]+)/?') | ||||
| def delete_user(ctx, params): | ||||
|     user = users.get_user_by_name(params['user_name']) | ||||
|     util.verify_version(user, ctx) | ||||
|     infix = 'self' if ctx.user.user_id == user.user_id else 'any' | ||||
|     auth.verify_privilege(ctx.user, 'users:delete:%s' % infix) | ||||
|     ctx.session.delete(user) | ||||
|     ctx.session.commit() | ||||
|     return {} | ||||
|  | ||||
| @ -1,124 +0,0 @@ | ||||
| ''' Exports create_app. ''' | ||||
| 
 | ||||
| import os | ||||
| import logging | ||||
| import coloredlogs | ||||
| import falcon | ||||
| from szurubooru import api, config, errors, middleware | ||||
| 
 | ||||
| def _on_auth_error(ex, _request, _response, _params): | ||||
|     raise falcon.HTTPForbidden( | ||||
|         title='Authentication error', description=str(ex)) | ||||
| 
 | ||||
| def _on_validation_error(ex, _request, _response, _params): | ||||
|     raise falcon.HTTPBadRequest(title='Validation error', description=str(ex)) | ||||
| 
 | ||||
| def _on_search_error(ex, _request, _response, _params): | ||||
|     raise falcon.HTTPBadRequest(title='Search error', description=str(ex)) | ||||
| 
 | ||||
| def _on_integrity_error(ex, _request, _response, _params): | ||||
|     raise falcon.HTTPConflict( | ||||
|         title='Integrity violation', description=ex.args[0]) | ||||
| 
 | ||||
| def _on_not_found_error(ex, _request, _response, _params): | ||||
|     raise falcon.HTTPNotFound(title='Not found', description=str(ex)) | ||||
| 
 | ||||
| def _on_processing_error(ex, _request, _response, _params): | ||||
|     raise falcon.HTTPBadRequest(title='Processing error', description=str(ex)) | ||||
| 
 | ||||
| def create_method_not_allowed(allowed_methods): | ||||
|     allowed = ', '.join(allowed_methods) | ||||
|     def method_not_allowed(request, response, **_kwargs): | ||||
|         response.status = falcon.status_codes.HTTP_405 | ||||
|         response.set_header('Allow', allowed) | ||||
|         request.context.output = { | ||||
|             'title': 'Method not allowed', | ||||
|             'description': 'Allowed methods: %r' % allowed_methods, | ||||
|         } | ||||
|     return method_not_allowed | ||||
| 
 | ||||
| def validate_config(): | ||||
|     ''' | ||||
|     Check whether config doesn't contain errors that might prove | ||||
|     lethal at runtime. | ||||
|     ''' | ||||
|     from szurubooru.func.auth import RANK_MAP | ||||
|     for privilege, rank in config.config['privileges'].items(): | ||||
|         if rank not in RANK_MAP.values(): | ||||
|             raise errors.ConfigError( | ||||
|                 'Rank %r for privilege %r is missing' % (rank, privilege)) | ||||
|     if config.config['default_rank'] not in RANK_MAP.values(): | ||||
|         raise errors.ConfigError( | ||||
|             'Default rank %r is not on the list of known ranks' % ( | ||||
|                 config.config['default_rank'])) | ||||
| 
 | ||||
|     for key in ['base_url', 'api_url', 'data_url', 'data_dir']: | ||||
|         if not config.config[key]: | ||||
|             raise errors.ConfigError( | ||||
|                 'Service is not configured: %r is missing' % key) | ||||
| 
 | ||||
|     if not os.path.isabs(config.config['data_dir']): | ||||
|         raise errors.ConfigError( | ||||
|             'data_dir must be an absolute path') | ||||
| 
 | ||||
|     for key in ['schema', 'host', 'port', 'user', 'pass', 'name']: | ||||
|         if not config.config['database'][key]: | ||||
|             raise errors.ConfigError( | ||||
|                 'Database is not configured: %r is missing' % key) | ||||
| 
 | ||||
| def create_app(): | ||||
|     ''' Create a WSGI compatible App object. ''' | ||||
|     validate_config() | ||||
|     falcon.responders.create_method_not_allowed = create_method_not_allowed | ||||
| 
 | ||||
|     coloredlogs.install(fmt='[%(asctime)-15s] %(name)s %(message)s') | ||||
|     if config.config['debug']: | ||||
|         logging.getLogger('szurubooru').setLevel(logging.INFO) | ||||
|     if config.config['show_sql']: | ||||
|         logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) | ||||
| 
 | ||||
|     app = falcon.API( | ||||
|         request_type=api.Request, | ||||
|         middleware=[ | ||||
|             middleware.RequireJson(), | ||||
|             middleware.CachePurger(), | ||||
|             middleware.ContextAdapter(), | ||||
|             middleware.DbSession(), | ||||
|             middleware.Authenticator(), | ||||
|             middleware.RequestLogger(), | ||||
|         ]) | ||||
| 
 | ||||
|     app.add_error_handler(errors.AuthError, _on_auth_error) | ||||
|     app.add_error_handler(errors.IntegrityError, _on_integrity_error) | ||||
|     app.add_error_handler(errors.ValidationError, _on_validation_error) | ||||
|     app.add_error_handler(errors.SearchError, _on_search_error) | ||||
|     app.add_error_handler(errors.NotFoundError, _on_not_found_error) | ||||
|     app.add_error_handler(errors.ProcessingError, _on_processing_error) | ||||
| 
 | ||||
|     app.add_route('/users/', api.UserListApi()) | ||||
|     app.add_route('/user/{user_name}', api.UserDetailApi()) | ||||
|     app.add_route('/password-reset/{user_name}', api.PasswordResetApi()) | ||||
| 
 | ||||
|     app.add_route('/tag-categories/', api.TagCategoryListApi()) | ||||
|     app.add_route('/tag-category/{category_name}', api.TagCategoryDetailApi()) | ||||
|     app.add_route('/tag-category/{category_name}/default', api.DefaultTagCategoryApi()) | ||||
|     app.add_route('/tags/', api.TagListApi()) | ||||
|     app.add_route('/tag/{tag_name}', api.TagDetailApi()) | ||||
|     app.add_route('/tag-merge/', api.TagMergeApi()) | ||||
|     app.add_route('/tag-siblings/{tag_name}', api.TagSiblingsApi()) | ||||
| 
 | ||||
|     app.add_route('/posts/', api.PostListApi()) | ||||
|     app.add_route('/post/{post_id}', api.PostDetailApi()) | ||||
|     app.add_route('/post/{post_id}/score', api.PostScoreApi()) | ||||
|     app.add_route('/post/{post_id}/favorite', api.PostFavoriteApi()) | ||||
|     app.add_route('/post/{post_id}/around', api.PostsAroundApi()) | ||||
| 
 | ||||
|     app.add_route('/comments/', api.CommentListApi()) | ||||
|     app.add_route('/comment/{comment_id}', api.CommentDetailApi()) | ||||
|     app.add_route('/comment/{comment_id}/score', api.CommentScoreApi()) | ||||
| 
 | ||||
|     app.add_route('/info/', api.InfoApi()) | ||||
|     app.add_route('/featured-post/', api.PostFeatureApi()) | ||||
|     app.add_route('/snapshots/', api.SnapshotListApi()) | ||||
| 
 | ||||
|     return app | ||||
							
								
								
									
										79
									
								
								server/szurubooru/facade.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								server/szurubooru/facade.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,79 @@ | ||||
| ''' Exports create_app. ''' | ||||
| 
 | ||||
| import os | ||||
| import logging | ||||
| import coloredlogs | ||||
| from szurubooru import config, errors, rest | ||||
| # pylint: disable=unused-import | ||||
| from szurubooru import api, middleware | ||||
| 
 | ||||
| def _on_auth_error(ex): | ||||
|     raise rest.errors.HttpForbidden( | ||||
|         title='Authentication error', description=str(ex)) | ||||
| 
 | ||||
| def _on_validation_error(ex): | ||||
|     raise rest.errors.HttpBadRequest( | ||||
|         title='Validation error', description=str(ex)) | ||||
| 
 | ||||
| def _on_search_error(ex): | ||||
|     raise rest.errors.HttpBadRequest( | ||||
|         title='Search error', description=str(ex)) | ||||
| 
 | ||||
| def _on_integrity_error(ex): | ||||
|     raise rest.errors.HttpConflict( | ||||
|         title='Integrity violation', description=ex.args[0]) | ||||
| 
 | ||||
| def _on_not_found_error(ex): | ||||
|     raise rest.errors.HttpNotFound( | ||||
|         title='Not found', description=str(ex)) | ||||
| 
 | ||||
| def _on_processing_error(ex): | ||||
|     raise rest.errors.HttpBadRequest( | ||||
|         title='Processing error', description=str(ex)) | ||||
| 
 | ||||
| def validate_config(): | ||||
|     ''' | ||||
|     Check whether config doesn't contain errors that might prove | ||||
|     lethal at runtime. | ||||
|     ''' | ||||
|     from szurubooru.func.auth import RANK_MAP | ||||
|     for privilege, rank in config.config['privileges'].items(): | ||||
|         if rank not in RANK_MAP.values(): | ||||
|             raise errors.ConfigError( | ||||
|                 'Rank %r for privilege %r is missing' % (rank, privilege)) | ||||
|     if config.config['default_rank'] not in RANK_MAP.values(): | ||||
|         raise errors.ConfigError( | ||||
|             'Default rank %r is not on the list of known ranks' % ( | ||||
|                 config.config['default_rank'])) | ||||
| 
 | ||||
|     for key in ['base_url', 'api_url', 'data_url', 'data_dir']: | ||||
|         if not config.config[key]: | ||||
|             raise errors.ConfigError( | ||||
|                 'Service is not configured: %r is missing' % key) | ||||
| 
 | ||||
|     if not os.path.isabs(config.config['data_dir']): | ||||
|         raise errors.ConfigError( | ||||
|             'data_dir must be an absolute path') | ||||
| 
 | ||||
|     for key in ['schema', 'host', 'port', 'user', 'pass', 'name']: | ||||
|         if not config.config['database'][key]: | ||||
|             raise errors.ConfigError( | ||||
|                 'Database is not configured: %r is missing' % key) | ||||
| 
 | ||||
| def create_app(): | ||||
|     ''' Create a WSGI compatible App object. ''' | ||||
|     validate_config() | ||||
|     coloredlogs.install(fmt='[%(asctime)-15s] %(name)s %(message)s') | ||||
|     if config.config['debug']: | ||||
|         logging.getLogger('szurubooru').setLevel(logging.INFO) | ||||
|     if config.config['show_sql']: | ||||
|         logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) | ||||
| 
 | ||||
|     rest.errors.handle(errors.AuthError, _on_auth_error) | ||||
|     rest.errors.handle(errors.ValidationError, _on_validation_error) | ||||
|     rest.errors.handle(errors.SearchError, _on_search_error) | ||||
|     rest.errors.handle(errors.IntegrityError, _on_integrity_error) | ||||
|     rest.errors.handle(errors.NotFoundError, _on_not_found_error) | ||||
|     rest.errors.handle(errors.ProcessingError, _on_processing_error) | ||||
| 
 | ||||
|     return rest.application | ||||
| @ -32,13 +32,6 @@ def get_tag_category_snapshot(category): | ||||
|         'default': True if category.default else False, | ||||
|     } | ||||
| 
 | ||||
| # pylint: disable=invalid-name | ||||
| serializers = { | ||||
|     'tag': get_tag_snapshot, | ||||
|     'tag_category': get_tag_category_snapshot, | ||||
|     'post': get_post_snapshot, | ||||
| } | ||||
| 
 | ||||
| def get_previous_snapshot(snapshot): | ||||
|     assert snapshot | ||||
|     return db.session \ | ||||
| @ -87,6 +80,12 @@ def get_serialized_history(entity): | ||||
| def _save(operation, entity, auth_user): | ||||
|     assert operation | ||||
|     assert entity | ||||
|     serializers = { | ||||
|         'tag': get_tag_snapshot, | ||||
|         'tag_category': get_tag_category_snapshot, | ||||
|         'post': get_post_snapshot, | ||||
|     } | ||||
| 
 | ||||
|     resource_type, resource_id, resource_repr = db.util.get_resource_info(entity) | ||||
|     now = datetime.datetime.utcnow() | ||||
| 
 | ||||
|  | ||||
| @ -11,6 +11,9 @@ def snake_case_to_lower_camel_case(text): | ||||
|     return components[0].lower() + \ | ||||
|         ''.join(word[0].upper() + word[1:].lower() for word in components[1:]) | ||||
| 
 | ||||
| def snake_case_to_upper_train_case(text): | ||||
|     return '-'.join(word[0].upper() + word[1:].lower() for word in text.split('_')) | ||||
| 
 | ||||
| def snake_case_to_lower_camel_case_keys(source): | ||||
|     target = {} | ||||
|     for key, value in source.items(): | ||||
|  | ||||
| @ -1,8 +1,6 @@ | ||||
| ''' Various hooks that get executed for each request. ''' | ||||
| 
 | ||||
| from szurubooru.middleware.authenticator import Authenticator | ||||
| from szurubooru.middleware.context_adapter import ContextAdapter | ||||
| from szurubooru.middleware.require_json import RequireJson | ||||
| from szurubooru.middleware.db_session import DbSession | ||||
| from szurubooru.middleware.cache_purger import CachePurger | ||||
| from szurubooru.middleware.request_logger import RequestLogger | ||||
| import szurubooru.middleware.db_session | ||||
| import szurubooru.middleware.authenticator | ||||
| import szurubooru.middleware.cache_purger | ||||
| import szurubooru.middleware.request_logger | ||||
|  | ||||
| @ -1,51 +1,44 @@ | ||||
| import base64 | ||||
| import falcon | ||||
| from szurubooru import db, errors | ||||
| from szurubooru.func import auth, users | ||||
| from szurubooru.rest import middleware | ||||
| from szurubooru.rest.errors import HttpBadRequest | ||||
| 
 | ||||
| class Authenticator(object): | ||||
|     ''' | ||||
|     Authenticates every request and put information on active user in the | ||||
|     request context. | ||||
|     ''' | ||||
| def _authenticate(username, password): | ||||
|     ''' Try to authenticate user. Throw AuthError for invalid users. ''' | ||||
|     user = users.get_user_by_name(username) | ||||
|     if not auth.is_valid_password(user, password): | ||||
|         raise errors.AuthError('Invalid password.') | ||||
|     return user | ||||
| 
 | ||||
|     def process_request(self, request, _response): | ||||
|         ''' Bind the user to request. Update last login time if needed. ''' | ||||
|         request.context.user = self._get_user(request) | ||||
|         if request.get_param_as_bool('bump-login') \ | ||||
|                 and request.context.user.user_id: | ||||
|             users.bump_user_login_time(request.context.user) | ||||
|             request.context.session.commit() | ||||
| def _create_anonymous_user(): | ||||
|     user = db.User() | ||||
|     user.name = None | ||||
|     user.rank = 'anonymous' | ||||
|     return user | ||||
| 
 | ||||
|     def _get_user(self, request): | ||||
|         if not request.auth: | ||||
|             return self._create_anonymous_user() | ||||
| def _get_user(ctx): | ||||
|     if not ctx.has_header('Authorization'): | ||||
|         return _create_anonymous_user() | ||||
| 
 | ||||
|         try: | ||||
|             auth_type, user_and_password = request.auth.split(' ', 1) | ||||
|             if auth_type.lower() != 'basic': | ||||
|                 raise falcon.HTTPBadRequest( | ||||
|                     'Invalid authentication type', | ||||
|                     'Only basic authorization is supported.') | ||||
|             username, password = base64.decodebytes( | ||||
|                 user_and_password.encode('ascii')).decode('utf8').split(':') | ||||
|             return self._authenticate(username, password) | ||||
|         except ValueError as err: | ||||
|             msg = 'Basic authentication header value not properly formed. ' \ | ||||
|                 + 'Supplied header {0}. Got error: {1}' | ||||
|             raise falcon.HTTPBadRequest( | ||||
|                 'Malformed authentication request', | ||||
|                 msg.format(request.auth, str(err))) | ||||
|     try: | ||||
|         auth_type, user_and_password = ctx.get_header('Authorization').split(' ', 1) | ||||
|         if auth_type.lower() != 'basic': | ||||
|             raise HttpBadRequest( | ||||
|                 'Only basic HTTP authentication is supported.') | ||||
|         username, password = base64.decodebytes( | ||||
|             user_and_password.encode('ascii')).decode('utf8').split(':') | ||||
|         return _authenticate(username, password) | ||||
|     except ValueError as err: | ||||
|         msg = 'Basic authentication header value are not properly formed. ' \ | ||||
|             + 'Supplied header {0}. Got error: {1}' | ||||
|         raise HttpBadRequest( | ||||
|             msg.format(ctx.get_header('Authorization'), str(err))) | ||||
| 
 | ||||
|     def _authenticate(self, username, password): | ||||
|         ''' Try to authenticate user. Throw AuthError for invalid users. ''' | ||||
|         user = users.get_user_by_name(username) | ||||
|         if not auth.is_valid_password(user, password): | ||||
|             raise errors.AuthError('Invalid password.') | ||||
|         return user | ||||
| 
 | ||||
|     def _create_anonymous_user(self): | ||||
|         user = db.User() | ||||
|         user.name = None | ||||
|         user.rank = 'anonymous' | ||||
|         return user | ||||
| @middleware.pre_hook | ||||
| def process_request(ctx): | ||||
|     ''' Bind the user to request. Update last login time if needed. ''' | ||||
|     ctx.user = _get_user(ctx) | ||||
|     if ctx.get_param_as_bool('bump-login') and ctx.user.user_id: | ||||
|         users.bump_user_login_time(ctx.user) | ||||
|         ctx.session.commit() | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| from szurubooru.func import cache | ||||
| from szurubooru.rest import middleware | ||||
| 
 | ||||
| class CachePurger(object): | ||||
|     def process_request(self, request, _response): | ||||
|         if request.method != 'GET': | ||||
|             cache.purge() | ||||
| @middleware.pre_hook | ||||
| def process_request(ctx): | ||||
|     if ctx.method != 'GET': | ||||
|         cache.purge() | ||||
|  | ||||
| @ -1,65 +0,0 @@ | ||||
| import cgi | ||||
| import datetime | ||||
| import json | ||||
| import falcon | ||||
| 
 | ||||
| def json_serializer(obj): | ||||
|     ''' JSON serializer for objects not serializable by default JSON code ''' | ||||
|     if isinstance(obj, datetime.datetime): | ||||
|         serial = obj.isoformat('T') + 'Z' | ||||
|         return serial | ||||
|     raise TypeError('Type not serializable') | ||||
| 
 | ||||
| class ContextAdapter(object): | ||||
|     ''' | ||||
|     1. Deserialize API requests into the context: | ||||
|         - Pass GET parameters | ||||
|         - Handle multipart/form-data file uploads | ||||
|         - Handle JSON requests | ||||
|     2. Serialize API responses from the context as JSON. | ||||
|     ''' | ||||
|     def process_request(self, request, _response): | ||||
|         request.context.files = {} | ||||
|         request.context.input = {} | ||||
|         request.context.output = None | ||||
|         # pylint: disable=protected-access | ||||
|         for key, value in request._params.items(): | ||||
|             request.context.input[key] = value | ||||
| 
 | ||||
|         if request.content_length in (None, 0): | ||||
|             return | ||||
| 
 | ||||
|         if request.content_type and 'multipart/form-data' in request.content_type: | ||||
|             # obscure, claims to "avoid a bug in cgi.FieldStorage" | ||||
|             request.env.setdefault('QUERY_STRING', '') | ||||
| 
 | ||||
|             form = cgi.FieldStorage(fp=request.stream, environ=request.env) | ||||
|             for key in form: | ||||
|                 if key != 'metadata': | ||||
|                     _original_file_name = getattr(form[key], 'filename', None) | ||||
|                     request.context.files[key] = form.getvalue(key) | ||||
|             body = form.getvalue('metadata') | ||||
|         else: | ||||
|             body = request.stream.read() | ||||
| 
 | ||||
|         if not body: | ||||
|             raise falcon.HTTPBadRequest( | ||||
|                 'Empty request body', | ||||
|                 'A valid JSON document is required.') | ||||
| 
 | ||||
|         try: | ||||
|             if isinstance(body, bytes): | ||||
|                 body = body.decode('utf-8') | ||||
| 
 | ||||
|             for key, value in json.loads(body).items(): | ||||
|                 request.context.input[key] = value | ||||
|         except (ValueError, UnicodeDecodeError): | ||||
|             raise falcon.HTTPBadRequest( | ||||
|                 'Malformed JSON', | ||||
|                 'Could not decode the request body. The ' | ||||
|                 'JSON was incorrect or not encoded as UTF-8.') | ||||
| 
 | ||||
|     def process_response(self, request, response, _resource): | ||||
|         if request.context.output: | ||||
|             response.body = json.dumps( | ||||
|                 request.context.output, default=json_serializer, indent=2) | ||||
| @ -1,14 +1,11 @@ | ||||
| import logging | ||||
| from szurubooru import db | ||||
| from szurubooru.rest import middleware | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| @middleware.pre_hook | ||||
| def _process_request(ctx): | ||||
|     ctx.session = db.session() | ||||
|     db.reset_query_count() | ||||
| 
 | ||||
| class DbSession(object): | ||||
|     ''' Attaches database session to the context of every request. ''' | ||||
| 
 | ||||
|     def process_request(self, request, _response): | ||||
|         request.context.session = db.session() | ||||
|         db.reset_query_count() | ||||
| 
 | ||||
|     def process_response(self, _request, _response, _resource): | ||||
|         db.session.remove() | ||||
| @middleware.post_hook | ||||
| def _process_response(_ctx): | ||||
|     db.session.remove() | ||||
|  | ||||
| @ -1,16 +1,14 @@ | ||||
| import logging | ||||
| from szurubooru import db | ||||
| from szurubooru.rest import middleware | ||||
| 
 | ||||
| logger = logging.getLogger(__name__) | ||||
| 
 | ||||
| class RequestLogger(object): | ||||
|     def process_request(self, request, _response): | ||||
|         pass | ||||
| 
 | ||||
|     def process_response(self, request, _response, _resource): | ||||
|         logger.info( | ||||
|             '%s %s (user=%s, queries=%d)', | ||||
|             request.method, | ||||
|             request.url, | ||||
|             request.context.user.name, | ||||
|             db.get_query_count()) | ||||
| @middleware.post_hook | ||||
| def process_response(ctx): | ||||
|     logger.info( | ||||
|         '%s %s (user=%s, queries=%d)', | ||||
|         ctx.method, | ||||
|         ctx.url, | ||||
|         ctx.user.name, | ||||
|         db.get_query_count()) | ||||
|  | ||||
| @ -1,9 +0,0 @@ | ||||
| import falcon | ||||
| 
 | ||||
| class RequireJson(object): | ||||
|     ''' Sanitizes requests so that only JSON is accepted. ''' | ||||
| 
 | ||||
|     def process_request(self, request, _response): | ||||
|         if not request.client_accepts_json: | ||||
|             raise falcon.HTTPNotAcceptable( | ||||
|                 'This API only supports responses encoded as JSON.') | ||||
							
								
								
									
										2
									
								
								server/szurubooru/rest/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								server/szurubooru/rest/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,2 @@ | ||||
| from szurubooru.rest.app import application | ||||
| from szurubooru.rest.context import Context | ||||
							
								
								
									
										124
									
								
								server/szurubooru/rest/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								server/szurubooru/rest/app.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,124 @@ | ||||
| import cgi | ||||
| import io | ||||
| import json | ||||
| import re | ||||
| from datetime import datetime | ||||
| from szurubooru.func import util | ||||
| from szurubooru.rest import errors, middleware, routes, context | ||||
| 
 | ||||
| def _json_serializer(obj): | ||||
|     ''' JSON serializer for objects not serializable by default JSON code ''' | ||||
|     if isinstance(obj, datetime): | ||||
|         serial = obj.isoformat('T') + 'Z' | ||||
|         return serial | ||||
|     raise TypeError('Type not serializable') | ||||
| 
 | ||||
| def _dump_json(obj): | ||||
|     return json.dumps(obj, default=_json_serializer, indent=2) | ||||
| 
 | ||||
| def _read(env): | ||||
|     length = int(env.get('CONTENT_LENGTH', 0)) | ||||
|     output = io.BytesIO() | ||||
|     while length > 0: | ||||
|         part = env['wsgi.input'].read(min(length, 1024*200)) | ||||
|         if not part: | ||||
|             break | ||||
|         output.write(part) | ||||
|         length -= len(part) | ||||
|     output.seek(0) | ||||
|     return output | ||||
| 
 | ||||
| def _get_headers(env): | ||||
|     headers = {} | ||||
|     for key, value in env.items(): | ||||
|         if key.startswith('HTTP_'): | ||||
|             key = util.snake_case_to_upper_train_case(key[5:]) | ||||
|             headers[key] = value | ||||
|     return headers | ||||
| 
 | ||||
| def _create_context(env): | ||||
|     method = env['REQUEST_METHOD'] | ||||
|     path = '/' + env['PATH_INFO'].lstrip('/') | ||||
|     headers = _get_headers(env) | ||||
| 
 | ||||
|     # obscure, claims to "avoid a bug in cgi.FieldStorage" | ||||
|     env.setdefault('QUERY_STRING', '') | ||||
| 
 | ||||
|     files = {} | ||||
|     params = {} | ||||
| 
 | ||||
|     request_stream = _read(env) | ||||
|     form = cgi.FieldStorage(fp=request_stream, environ=env) | ||||
| 
 | ||||
|     if form.list: | ||||
|         for key in form: | ||||
|             if key != 'metadata': | ||||
|                 if isinstance(form[key], cgi.MiniFieldStorage): | ||||
|                     params[key] = form.getvalue(key) | ||||
|                 else: | ||||
|                     _original_file_name = getattr(form[key], 'filename', None) | ||||
|                     files[key] = form.getvalue(key) | ||||
|         if 'metadata' in form: | ||||
|             body = form.getvalue('metadata') | ||||
|         else: | ||||
|             body = request_stream.read() | ||||
|     else: | ||||
|         body = None | ||||
| 
 | ||||
|     if body: | ||||
|         try: | ||||
|             if isinstance(body, bytes): | ||||
|                 body = body.decode('utf-8') | ||||
| 
 | ||||
|             for key, value in json.loads(body).items(): | ||||
|                 params[key] = value | ||||
|         except (ValueError, UnicodeDecodeError): | ||||
|             raise errors.HttpBadRequest( | ||||
|                 'Could not decode the request body. The JSON ' | ||||
|                 'was incorrect or was not encoded as UTF-8.') | ||||
| 
 | ||||
|     return context.Context(method, path, headers, params, files) | ||||
| 
 | ||||
| def application(env, start_response): | ||||
|     try: | ||||
|         ctx = _create_context(env) | ||||
|         if not 'application/json' in ctx.get_header('Accept'): | ||||
|             raise errors.HttpNotAcceptable( | ||||
|                 'This API only supports JSON responses.') | ||||
| 
 | ||||
|         for url, allowed_methods in routes.routes.items(): | ||||
|             match = re.fullmatch(url, ctx.url) | ||||
|             if not match: | ||||
|                 continue | ||||
|             if ctx.method not in allowed_methods: | ||||
|                 raise errors.HttpMethodNotAllowed( | ||||
|                     'Allowed methods: %r' % allowed_methods) | ||||
| 
 | ||||
|             for hook in middleware.pre_hooks: | ||||
|                 hook(ctx) | ||||
|             handler = allowed_methods[ctx.method] | ||||
|             try: | ||||
|                 response = handler(ctx, match.groupdict()) | ||||
|             except Exception as ex: | ||||
|                 for exception_type, handler in errors.error_handlers.items(): | ||||
|                     if isinstance(ex, exception_type): | ||||
|                         handler(ex) | ||||
|                 raise | ||||
|             finally: | ||||
|                 for hook in middleware.post_hooks: | ||||
|                     hook(ctx) | ||||
| 
 | ||||
|             start_response('200', [('content-type', 'application/json')]) | ||||
|             return (_dump_json(response).encode('utf-8'),) | ||||
| 
 | ||||
|         raise errors.HttpNotFound( | ||||
|             'Requested path ' + ctx.url + ' was not found.') | ||||
| 
 | ||||
|     except errors.BaseHttpError as ex: | ||||
|         start_response( | ||||
|             '%d %s' % (ex.code, ex.reason), | ||||
|             [('content-type', 'application/json')]) | ||||
|         return (_dump_json({ | ||||
|             'title': ex.title, | ||||
|             'description': ex.description, | ||||
|         }).encode('utf-8'),) | ||||
| @ -1,4 +1,3 @@ | ||||
| import falcon | ||||
| from szurubooru import errors | ||||
| from szurubooru.func import net | ||||
| 
 | ||||
| @ -7,8 +6,9 @@ def _lower_first(source): | ||||
| 
 | ||||
| def _param_wrapper(func): | ||||
|     def wrapper(self, name, required=False, default=None, **kwargs): | ||||
|         if name in self.input: | ||||
|             value = self.input[name] | ||||
|         # pylint: disable=protected-access | ||||
|         if name in self._params: | ||||
|             value = self._params[name] | ||||
|             try: | ||||
|                 value = func(self, value, **kwargs) | ||||
|             except errors.InvalidParameterError as ex: | ||||
| @ -22,34 +22,46 @@ def _param_wrapper(func): | ||||
|             'Required parameter %r is missing.' % name) | ||||
|     return wrapper | ||||
| 
 | ||||
| class Context(object): | ||||
|     def __init__(self): | ||||
|         self.session = None | ||||
|         self.user = None | ||||
|         self.files = {} | ||||
|         self.input = {} | ||||
|         self.output = None | ||||
|         self.settings = {} | ||||
| class Context(): | ||||
|     # pylint: disable=too-many-arguments | ||||
|     def __init__(self, method, url, headers=None, params=None, files=None): | ||||
|         self.method = method | ||||
|         self.url = url | ||||
|         self._headers = headers or {} | ||||
|         self._params = params or {} | ||||
|         self._files = files or {} | ||||
| 
 | ||||
|     def has_param(self, name): | ||||
|         return name in self.input | ||||
|         # provided by middleware | ||||
|         # self.session = None | ||||
|         # self.user = None | ||||
| 
 | ||||
|     def has_header(self, name): | ||||
|         return name in self._headers | ||||
| 
 | ||||
|     def get_header(self, name): | ||||
|         return self._headers.get(name, None) | ||||
| 
 | ||||
|     def has_file(self, name): | ||||
|         return name in self.files or name + 'Url' in self.input | ||||
|         return name in self._files or name + 'Url' in self._params | ||||
| 
 | ||||
|     def get_file(self, name, required=False): | ||||
|         if name in self.files: | ||||
|             return self.files[name] | ||||
|         if name + 'Url' in self.input: | ||||
|             return net.download(self.input[name + 'Url']) | ||||
|         if name in self._files: | ||||
|             return self._files[name] | ||||
|         if name + 'Url' in self._params: | ||||
|             return net.download(self._params[name + 'Url']) | ||||
|         if not required: | ||||
|             return None | ||||
|         raise errors.MissingRequiredFileError( | ||||
|             'Required file %r is missing.' % name) | ||||
| 
 | ||||
|     def has_param(self, name): | ||||
|         return name in self._params | ||||
| 
 | ||||
|     @_param_wrapper | ||||
|     def get_param_as_list(self, value): | ||||
|         if not isinstance(value, list): | ||||
|             if ',' in value: | ||||
|                 return value.split(',') | ||||
|             return [value] | ||||
|         return value | ||||
| 
 | ||||
| @ -86,6 +98,3 @@ class Context(object): | ||||
|         if value in ['0', 'n', 'no', 'nope', 'f', 'false']: | ||||
|             return False | ||||
|         raise errors.InvalidParameterError('The value must be a boolean value.') | ||||
| 
 | ||||
| class Request(falcon.Request): | ||||
|     context_type = Context | ||||
							
								
								
									
										37
									
								
								server/szurubooru/rest/errors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								server/szurubooru/rest/errors.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| error_handlers = {} # pylint: disable=invalid-name | ||||
| 
 | ||||
| class BaseHttpError(RuntimeError): | ||||
|     code = None | ||||
|     reason = None | ||||
| 
 | ||||
|     def __init__(self, description, title=None): | ||||
|         super().__init__() | ||||
|         self.description = description | ||||
|         self.title = title or self.reason | ||||
| 
 | ||||
| class HttpBadRequest(BaseHttpError): | ||||
|     code = 400 | ||||
|     reason = 'Bad Request' | ||||
| 
 | ||||
| class HttpForbidden(BaseHttpError): | ||||
|     code = 403 | ||||
|     reason = 'Forbidden' | ||||
| 
 | ||||
| class HttpNotFound(BaseHttpError): | ||||
|     code = 404 | ||||
|     reason = 'Not Found' | ||||
| 
 | ||||
| class HttpNotAcceptable(BaseHttpError): | ||||
|     code = 406 | ||||
|     reason = 'Not Acceptable' | ||||
| 
 | ||||
| class HttpConflict(BaseHttpError): | ||||
|     code = 409 | ||||
|     reason = 'Conflict' | ||||
| 
 | ||||
| class HttpMethodNotAllowed(BaseHttpError): | ||||
|     code = 405 | ||||
|     reason = 'Method Not Allowed' | ||||
| 
 | ||||
| def handle(exception_type, handler): | ||||
|     error_handlers[exception_type] = handler | ||||
							
								
								
									
										9
									
								
								server/szurubooru/rest/middleware.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								server/szurubooru/rest/middleware.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| # pylint: disable=invalid-name | ||||
| pre_hooks = [] | ||||
| post_hooks = [] | ||||
| 
 | ||||
| def pre_hook(handler): | ||||
|     pre_hooks.append(handler) | ||||
| 
 | ||||
| def post_hook(handler): | ||||
|     post_hooks.insert(0, handler) | ||||
							
								
								
									
										27
									
								
								server/szurubooru/rest/routes.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								server/szurubooru/rest/routes.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | ||||
| from collections import defaultdict | ||||
| 
 | ||||
| routes = defaultdict(dict) # pylint: disable=invalid-name | ||||
| 
 | ||||
| def get(url): | ||||
|     def wrapper(handler): | ||||
|         routes[url]['GET'] = handler | ||||
|         return handler | ||||
|     return wrapper | ||||
| 
 | ||||
| def put(url): | ||||
|     def wrapper(handler): | ||||
|         routes[url]['PUT'] = handler | ||||
|         return handler | ||||
|     return wrapper | ||||
| 
 | ||||
| def post(url): | ||||
|     def wrapper(handler): | ||||
|         routes[url]['POST'] = handler | ||||
|         return handler | ||||
|     return wrapper | ||||
| 
 | ||||
| def delete(url): | ||||
|     def wrapper(handler): | ||||
|         routes[url]['DELETE'] = handler | ||||
|         return handler | ||||
|     return wrapper | ||||
| @ -1,89 +1,78 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, posts | ||||
| from szurubooru.func import comments, posts | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, post_factory, user_factory): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'data_url': 'http://example.com', | ||||
|         'privileges': {'comments:create': db.User.RANK_REGULAR}, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.post_factory = post_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.api = api.CommentListApi() | ||||
|     return ret | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'comments:create': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| def test_creating_comment(test_ctx, fake_datetime): | ||||
|     post = test_ctx.post_factory() | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
| def test_creating_comment( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     post = post_factory() | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     db.session.add_all([post, user]) | ||||
|     db.session.flush() | ||||
|     with fake_datetime('1997-01-01'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'text': 'input', 'postId': post.post_id}, | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'), \ | ||||
|             fake_datetime('1997-01-01'): | ||||
|         comments.serialize_comment.return_value = 'serialized comment' | ||||
|         result = api.comment_api.create_comment( | ||||
|             context_factory( | ||||
|                 params={'text': 'input', 'postId': post.post_id}, | ||||
|                 user=user)) | ||||
|     assert result['text'] == 'input' | ||||
|     assert 'id' in result | ||||
|     assert 'user' in result | ||||
|     assert 'name' in result['user'] | ||||
|     assert 'postId' in result | ||||
|     comment = db.session.query(db.Comment).one() | ||||
|     assert comment.text == 'input' | ||||
|     assert comment.creation_time == datetime.datetime(1997, 1, 1) | ||||
|     assert comment.last_edit_time is None | ||||
|     assert comment.user and comment.user.user_id == user.user_id | ||||
|     assert comment.post and comment.post.post_id == post.post_id | ||||
|         assert result == 'serialized comment' | ||||
|         comment = db.session.query(db.Comment).one() | ||||
|         assert comment.text == 'input' | ||||
|         assert comment.creation_time == datetime(1997, 1, 1) | ||||
|         assert comment.last_edit_time is None | ||||
|         assert comment.user and comment.user.user_id == user.user_id | ||||
|         assert comment.post and comment.post.post_id == post.post_id | ||||
| 
 | ||||
| @pytest.mark.parametrize('input', [ | ||||
| @pytest.mark.parametrize('params', [ | ||||
|     {'text': None}, | ||||
|     {'text': ''}, | ||||
|     {'text': [None]}, | ||||
|     {'text': ['']}, | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input): | ||||
|     post = test_ctx.post_factory() | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
| def test_trying_to_pass_invalid_params( | ||||
|         user_factory, post_factory, context_factory, params): | ||||
|     post = post_factory() | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     db.session.add_all([post, user]) | ||||
|     db.session.flush() | ||||
|     real_input = {'text': 'input', 'postId': post.post_id} | ||||
|     for key, value in input.items(): | ||||
|         real_input[key] = value | ||||
|     real_params = {'text': 'input', 'postId': post.post_id} | ||||
|     for key, value in params.items(): | ||||
|         real_params[key] = value | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory(input=real_input, user=user)) | ||||
|         api.comment_api.create_comment( | ||||
|             context_factory(params=real_params, user=user)) | ||||
| 
 | ||||
| @pytest.mark.parametrize('field', ['text', 'postId']) | ||||
| def test_trying_to_omit_mandatory_field(test_ctx, field): | ||||
|     input = { | ||||
| def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): | ||||
|     params = { | ||||
|         'text': 'input', | ||||
|         'postId': 1, | ||||
|     } | ||||
|     del input[field] | ||||
|     del params[field] | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         api.comment_api.create_comment( | ||||
|             context_factory( | ||||
|                 params={}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_trying_to_comment_non_existing(test_ctx): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
| def test_trying_to_comment_non_existing(user_factory, context_factory): | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     db.session.add_all([user]) | ||||
|     db.session.flush() | ||||
|     with pytest.raises(posts.PostNotFoundError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'text': 'bad', 'postId': 5}, user=user)) | ||||
|         api.comment_api.create_comment( | ||||
|             context_factory( | ||||
|                 params={'text': 'bad', 'postId': 5}, user=user)) | ||||
| 
 | ||||
| def test_trying_to_create_without_privileges(test_ctx): | ||||
| def test_trying_to_create_without_privileges(user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.comment_api.create_comment( | ||||
|             context_factory( | ||||
|                 params={}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|  | ||||
| @ -1,61 +1,56 @@ | ||||
| import pytest | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, comments | ||||
| from szurubooru.func import comments | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx(config_injector, context_factory, user_factory, comment_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'comments:delete:own': db.User.RANK_REGULAR, | ||||
|             'comments:delete:any': db.User.RANK_MODERATOR, | ||||
|         }, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.comment_factory = comment_factory | ||||
|     ret.api = api.CommentDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_deleting_own_comment(test_ctx): | ||||
|     user = test_ctx.user_factory() | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_deleting_own_comment(user_factory, comment_factory, context_factory): | ||||
|     user = user_factory() | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     result = test_ctx.api.delete( | ||||
|         test_ctx.context_factory(input={'version': 1}, user=user), | ||||
|         comment.comment_id) | ||||
|     result = api.comment_api.delete_comment( | ||||
|         context_factory(params={'version': 1}, user=user), | ||||
|         {'comment_id': comment.comment_id}) | ||||
|     assert result == {} | ||||
|     assert db.session.query(db.Comment).count() == 0 | ||||
| 
 | ||||
| def test_deleting_someones_else_comment(test_ctx): | ||||
|     user1 = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(rank=db.User.RANK_MODERATOR) | ||||
|     comment = test_ctx.comment_factory(user=user1) | ||||
| def test_deleting_someones_else_comment( | ||||
|         user_factory, comment_factory, context_factory): | ||||
|     user1 = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = user_factory(rank=db.User.RANK_MODERATOR) | ||||
|     comment = comment_factory(user=user1) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     result = test_ctx.api.delete( | ||||
|         test_ctx.context_factory(input={'version': 1}, user=user2), | ||||
|         comment.comment_id) | ||||
|     result = api.comment_api.delete_comment( | ||||
|         context_factory(params={'version': 1}, user=user2), | ||||
|         {'comment_id': comment.comment_id}) | ||||
|     assert db.session.query(db.Comment).count() == 0 | ||||
| 
 | ||||
| def test_trying_to_delete_someones_else_comment_without_privileges(test_ctx): | ||||
|     user1 = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = test_ctx.comment_factory(user=user1) | ||||
| def test_trying_to_delete_someones_else_comment_without_privileges( | ||||
|         user_factory, comment_factory, context_factory): | ||||
|     user1 = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = comment_factory(user=user1) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory(input={'version': 1}, user=user2), | ||||
|             comment.comment_id) | ||||
|         api.comment_api.delete_comment( | ||||
|             context_factory(params={'version': 1}, user=user2), | ||||
|             {'comment_id': comment.comment_id}) | ||||
|     assert db.session.query(db.Comment).count() == 1 | ||||
| 
 | ||||
| def test_trying_to_delete_non_existing(test_ctx): | ||||
| def test_trying_to_delete_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(comments.CommentNotFoundError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'version': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             1) | ||||
|         api.comment_api.delete_comment( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'comment_id': 1}) | ||||
|  | ||||
| @ -1,152 +1,134 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, comments, scores | ||||
| from szurubooru.func import comments, scores | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, user_factory, comment_factory): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'data_url': 'http://example.com', | ||||
|         'privileges': { | ||||
|             'comments:score': db.User.RANK_REGULAR, | ||||
|             'users:edit:any:email': db.User.RANK_MODERATOR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     db.session.flush() | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.comment_factory = comment_factory | ||||
|     ret.api = api.CommentScoreApi() | ||||
|     return ret | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'comments:score': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| def test_simple_rating(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_simple_rating( | ||||
|         user_factory, comment_factory, context_factory, fake_datetime): | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user), | ||||
|             comment.comment_id) | ||||
|     assert 'text' in result | ||||
|     assert db.session.query(db.CommentScore).count() == 1 | ||||
|     assert comment is not None | ||||
|     assert comment.score == 1 | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'): | ||||
|         comments.serialize_comment.return_value = 'serialized comment' | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.comment_api.set_comment_score( | ||||
|                 context_factory(params={'score': 1}, user=user), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         assert result == 'serialized comment' | ||||
|         assert db.session.query(db.CommentScore).count() == 1 | ||||
|         assert comment is not None | ||||
|         assert comment.score == 1 | ||||
| 
 | ||||
| def test_updating_rating(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_updating_rating( | ||||
|         user_factory, comment_factory, context_factory, fake_datetime): | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user), | ||||
|             comment.comment_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': -1}, user=user), | ||||
|             comment.comment_id) | ||||
|     comment = db.session.query(db.Comment).one() | ||||
|     assert db.session.query(db.CommentScore).count() == 1 | ||||
|     assert comment.score == -1 | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.comment_api.set_comment_score( | ||||
|                 context_factory(params={'score': 1}, user=user), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             result = api.comment_api.set_comment_score( | ||||
|                 context_factory(params={'score': -1}, user=user), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         comment = db.session.query(db.Comment).one() | ||||
|         assert db.session.query(db.CommentScore).count() == 1 | ||||
|         assert comment.score == -1 | ||||
| 
 | ||||
| def test_updating_rating_to_zero(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_updating_rating_to_zero( | ||||
|         user_factory, comment_factory, context_factory, fake_datetime): | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user), | ||||
|             comment.comment_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 0}, user=user), | ||||
|             comment.comment_id) | ||||
|     comment = db.session.query(db.Comment).one() | ||||
|     assert db.session.query(db.CommentScore).count() == 0 | ||||
|     assert comment.score == 0 | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.comment_api.set_comment_score( | ||||
|                 context_factory(params={'score': 1}, user=user), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             result = api.comment_api.set_comment_score( | ||||
|                 context_factory(params={'score': 0}, user=user), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         comment = db.session.query(db.Comment).one() | ||||
|         assert db.session.query(db.CommentScore).count() == 0 | ||||
|         assert comment.score == 0 | ||||
| 
 | ||||
| def test_deleting_rating(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_deleting_rating( | ||||
|         user_factory, comment_factory, context_factory, fake_datetime): | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user), | ||||
|             comment.comment_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.delete( | ||||
|             test_ctx.context_factory(user=user), comment.comment_id) | ||||
|     comment = db.session.query(db.Comment).one() | ||||
|     assert db.session.query(db.CommentScore).count() == 0 | ||||
|     assert comment.score == 0 | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.comment_api.set_comment_score( | ||||
|                 context_factory(params={'score': 1}, user=user), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             result = api.comment_api.delete_comment_score( | ||||
|                 context_factory(user=user), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         comment = db.session.query(db.Comment).one() | ||||
|         assert db.session.query(db.CommentScore).count() == 0 | ||||
|         assert comment.score == 0 | ||||
| 
 | ||||
| def test_ratings_from_multiple_users(test_ctx, fake_datetime): | ||||
|     user1 = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = test_ctx.comment_factory() | ||||
| def test_ratings_from_multiple_users( | ||||
|         user_factory, comment_factory, context_factory, fake_datetime): | ||||
|     user1 = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = comment_factory() | ||||
|     db.session.add_all([user1, user2, comment]) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user1), | ||||
|             comment.comment_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': -1}, user=user2), | ||||
|             comment.comment_id) | ||||
|     comment = db.session.query(db.Comment).one() | ||||
|     assert db.session.query(db.CommentScore).count() == 2 | ||||
|     assert comment.score == 0 | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.comment_api.set_comment_score( | ||||
|                 context_factory(params={'score': 1}, user=user1), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             result = api.comment_api.set_comment_score( | ||||
|                 context_factory(params={'score': -1}, user=user2), | ||||
|                 {'comment_id': comment.comment_id}) | ||||
|         comment = db.session.query(db.Comment).one() | ||||
|         assert db.session.query(db.CommentScore).count() == 2 | ||||
|         assert comment.score == 0 | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_exception', [ | ||||
|     ({'score': None}, errors.ValidationError), | ||||
|     ({'score': ''}, errors.ValidationError), | ||||
|     ({'score': -2}, scores.InvalidScoreValueError), | ||||
|     ({'score': 2}, scores.InvalidScoreValueError), | ||||
|     ({'score': [1]}, errors.ValidationError), | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception): | ||||
|     user = test_ctx.user_factory() | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(expected_exception): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory(input=input, user=user), | ||||
|             comment.comment_id) | ||||
| 
 | ||||
| def test_trying_to_omit_mandatory_field(test_ctx): | ||||
|     user = test_ctx.user_factory() | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_trying_to_omit_mandatory_field( | ||||
|         user_factory, comment_factory, context_factory): | ||||
|     user = user_factory() | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={}, user=user), | ||||
|             comment.comment_id) | ||||
|         api.comment_api.set_comment_score( | ||||
|             context_factory(params={}, user=user), | ||||
|             {'comment_id': comment.comment_id}) | ||||
| 
 | ||||
| def test_trying_to_update_non_existing(test_ctx): | ||||
| def test_trying_to_update_non_existing( | ||||
|         user_factory, comment_factory, context_factory): | ||||
|     with pytest.raises(comments.CommentNotFoundError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'score': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             5) | ||||
|         api.comment_api.set_comment_score( | ||||
|             context_factory( | ||||
|                 params={'score': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'comment_id': 5}) | ||||
| 
 | ||||
| def test_trying_to_rate_without_privileges(test_ctx): | ||||
|     comment = test_ctx.comment_factory() | ||||
| def test_trying_to_rate_without_privileges( | ||||
|         user_factory, comment_factory, context_factory): | ||||
|     comment = comment_factory() | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'score': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             comment.comment_id) | ||||
|         api.comment_api.set_comment_score( | ||||
|             context_factory( | ||||
|                 params={'score': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'comment_id': comment.comment_id}) | ||||
|  | ||||
| @ -1,76 +1,65 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, comments | ||||
| from szurubooru.func import comments | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, context_factory, config_injector, user_factory, comment_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'data_url': 'http://example.com', | ||||
|         'privileges': { | ||||
|             'comments:list': db.User.RANK_REGULAR, | ||||
|             'comments:view': db.User.RANK_REGULAR, | ||||
|             'users:edit:any:email': db.User.RANK_MODERATOR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.comment_factory = comment_factory | ||||
|     ret.list_api = api.CommentListApi() | ||||
|     ret.detail_api = api.CommentDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_retrieving_multiple(test_ctx): | ||||
|     comment1 = test_ctx.comment_factory(text='text 1') | ||||
|     comment2 = test_ctx.comment_factory(text='text 2') | ||||
| def test_retrieving_multiple(user_factory, comment_factory, context_factory): | ||||
|     comment1 = comment_factory(text='text 1') | ||||
|     comment2 = comment_factory(text='text 2') | ||||
|     db.session.add_all([comment1, comment2]) | ||||
|     result = test_ctx.list_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             input={'query': '', 'page': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result['query'] == '' | ||||
|     assert result['page'] == 1 | ||||
|     assert result['pageSize'] == 100 | ||||
|     assert result['total'] == 2 | ||||
|     assert [c['text'] for c in result['results']] == ['text 1', 'text 2'] | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'): | ||||
|         comments.serialize_comment.return_value = 'serialized comment' | ||||
|         result = api.comment_api.get_comments( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == { | ||||
|             'query': '', | ||||
|             'page': 1, | ||||
|             'pageSize': 100, | ||||
|             'total': 2, | ||||
|             'results': ['serialized comment', 'serialized comment'], | ||||
|         } | ||||
| 
 | ||||
| def test_trying_to_retrieve_multiple_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_multiple_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.list_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'query': '', 'page': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.comment_api.get_comments( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
| 
 | ||||
| def test_retrieving_single(test_ctx): | ||||
|     comment = test_ctx.comment_factory(text='dummy text') | ||||
| def test_retrieving_single(user_factory, comment_factory, context_factory): | ||||
|     comment = comment_factory(text='dummy text') | ||||
|     db.session.add(comment) | ||||
|     db.session.flush() | ||||
|     result = test_ctx.detail_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         comment.comment_id) | ||||
|     assert 'id' in result | ||||
|     assert 'lastEditTime' in result | ||||
|     assert 'creationTime' in result | ||||
|     assert 'text' in result | ||||
|     assert 'user' in result | ||||
|     assert 'name' in result['user'] | ||||
|     assert 'postId' in result | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'): | ||||
|         comments.serialize_comment.return_value = 'serialized comment' | ||||
|         result = api.comment_api.get_comment( | ||||
|             context_factory( | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'comment_id': comment.comment_id}) | ||||
|         assert result == 'serialized comment' | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_non_existing(test_ctx): | ||||
| def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(comments.CommentNotFoundError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             5) | ||||
|         api.comment_api.get_comment( | ||||
|             context_factory( | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'comment_id': 5}) | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_single_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             5) | ||||
|         api.comment_api.get_comment( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'comment_id': 5}) | ||||
|  | ||||
| @ -1,103 +1,94 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, comments | ||||
| from szurubooru.func import comments | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, user_factory, comment_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'data_url': 'http://example.com', | ||||
|         'privileges': { | ||||
|             'comments:edit:own': db.User.RANK_REGULAR, | ||||
|             'comments:edit:any': db.User.RANK_MODERATOR, | ||||
|             'users:edit:any:email': db.User.RANK_MODERATOR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     db.session.flush() | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.comment_factory = comment_factory | ||||
|     ret.api = api.CommentDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_simple_updating(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_simple_updating( | ||||
|         user_factory, comment_factory, context_factory, fake_datetime): | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'text': 'new text', 'version': 1}, user=user), | ||||
|             comment.comment_id) | ||||
|     assert result['text'] == 'new text' | ||||
|     comment = db.session.query(db.Comment).one() | ||||
|     assert comment is not None | ||||
|     assert comment.text == 'new text' | ||||
|     assert comment.last_edit_time is not None | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'), \ | ||||
|             fake_datetime('1997-12-01'): | ||||
|         comments.serialize_comment.return_value = 'serialized comment' | ||||
|         result = api.comment_api.update_comment( | ||||
|             context_factory( | ||||
|                 params={'text': 'new text', 'version': 1}, user=user), | ||||
|             {'comment_id': comment.comment_id}) | ||||
|         assert result == 'serialized comment' | ||||
|         assert comment.last_edit_time == datetime(1997, 12, 1) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_exception', [ | ||||
| @pytest.mark.parametrize('params,expected_exception', [ | ||||
|     ({'text': None}, comments.EmptyCommentTextError), | ||||
|     ({'text': ''}, comments.EmptyCommentTextError), | ||||
|     ({'text': []}, comments.EmptyCommentTextError), | ||||
|     ({'text': [None]}, errors.ValidationError), | ||||
|     ({'text': ['']}, comments.EmptyCommentTextError), | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception): | ||||
|     user = test_ctx.user_factory() | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_trying_to_pass_invalid_params( | ||||
|         user_factory, comment_factory, context_factory, params, expected_exception): | ||||
|     user = user_factory() | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(expected_exception): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={**input, **{'version': 1}}, user=user), | ||||
|             comment.comment_id) | ||||
|         api.comment_api.update_comment( | ||||
|             context_factory( | ||||
|                 params={**params, **{'version': 1}}, user=user), | ||||
|             {'comment_id': comment.comment_id}) | ||||
| 
 | ||||
| def test_trying_to_omit_mandatory_field(test_ctx): | ||||
|     user = test_ctx.user_factory() | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_trying_to_omit_mandatory_field( | ||||
|         user_factory, comment_factory, context_factory): | ||||
|     user = user_factory() | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'version': 1}, user=user), | ||||
|             comment.comment_id) | ||||
|         api.comment_api.update_comment( | ||||
|             context_factory(params={'version': 1}, user=user), | ||||
|             {'comment_id': comment.comment_id}) | ||||
| 
 | ||||
| def test_trying_to_update_non_existing(test_ctx): | ||||
| def test_trying_to_update_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(comments.CommentNotFoundError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'text': 'new text'}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             5) | ||||
|         api.comment_api.update_comment( | ||||
|             context_factory( | ||||
|                 params={'text': 'new text'}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'comment_id': 5}) | ||||
| 
 | ||||
| def test_trying_to_update_someones_comment_without_privileges(test_ctx): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_trying_to_update_someones_comment_without_privileges( | ||||
|         user_factory, comment_factory, context_factory): | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'text': 'new text', 'version': 1}, user=user2), | ||||
|             comment.comment_id) | ||||
|         api.comment_api.update_comment( | ||||
|             context_factory( | ||||
|                 params={'text': 'new text', 'version': 1}, user=user2), | ||||
|             {'comment_id': comment.comment_id}) | ||||
| 
 | ||||
| def test_updating_someones_comment_with_privileges(test_ctx): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(rank=db.User.RANK_MODERATOR) | ||||
|     comment = test_ctx.comment_factory(user=user) | ||||
| def test_updating_someones_comment_with_privileges( | ||||
|         user_factory, comment_factory, context_factory): | ||||
|     user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     user2 = user_factory(rank=db.User.RANK_MODERATOR) | ||||
|     comment = comment_factory(user=user) | ||||
|     db.session.add(comment) | ||||
|     db.session.commit() | ||||
|     try: | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'text': 'new text', 'version': 1}, user=user2), | ||||
|             comment.comment_id) | ||||
|     except: | ||||
|         pytest.fail() | ||||
|     with unittest.mock.patch('szurubooru.func.comments.serialize_comment'): | ||||
|         api.comment_api.update_comment( | ||||
|             context_factory( | ||||
|                 params={'text': 'new text', 'version': 1}, user=user2), | ||||
|             {'comment_id': comment.comment_id}) | ||||
|  | ||||
| @ -31,9 +31,8 @@ def test_info_api( | ||||
|         }, | ||||
|     } | ||||
| 
 | ||||
|     info_api = api.InfoApi() | ||||
|     with fake_datetime('2016-01-01 13:00'): | ||||
|         assert info_api.get(context_factory()) == { | ||||
|         assert api.info_api.get_info(context_factory()) == { | ||||
|             'postCount': 2, | ||||
|             'diskUsage': 3, | ||||
|             'featuredPost': None, | ||||
| @ -44,7 +43,7 @@ def test_info_api( | ||||
|         } | ||||
|     directory.join('test2.txt').write('abc') | ||||
|     with fake_datetime('2016-01-01 13:59'): | ||||
|         assert info_api.get(context_factory()) == { | ||||
|         assert api.info_api.get_info(context_factory()) == { | ||||
|             'postCount': 2, | ||||
|             'diskUsage': 3, # still 3 - it's cached | ||||
|             'featuredPost': None, | ||||
| @ -54,7 +53,7 @@ def test_info_api( | ||||
|             'config': expected_config_key, | ||||
|         } | ||||
|     with fake_datetime('2016-01-01 14:01'): | ||||
|         assert info_api.get(context_factory()) == { | ||||
|         assert api.info_api.get_info(context_factory()) == { | ||||
|             'postCount': 2, | ||||
|             'diskUsage': 6, # cache expired | ||||
|             'featuredPost': None, | ||||
|  | ||||
| @ -1,71 +1,70 @@ | ||||
| from datetime import datetime | ||||
| from unittest import mock | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import auth, mailer | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def password_reset_api(config_injector): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(tmpdir, config_injector): | ||||
|     config_injector({ | ||||
|         'secret': 'x', | ||||
|         'base_url': 'http://example.com/', | ||||
|         'name': 'Test instance', | ||||
|     }) | ||||
|     return api.PasswordResetApi() | ||||
| 
 | ||||
| def test_reset_sending_email( | ||||
|         password_reset_api, context_factory, user_factory): | ||||
| def test_reset_sending_email(context_factory, user_factory): | ||||
|     db.session.add(user_factory( | ||||
|         name='u1', rank=db.User.RANK_REGULAR, email='user@example.com')) | ||||
|     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.') | ||||
|     for initiating_user in ['u1', 'user@example.com']: | ||||
|         with unittest.mock.patch('szurubooru.func.mailer.send_mail'): | ||||
|             assert api.password_reset_api.start_password_reset( | ||||
|                 context_factory(), {'user_name': initiating_user}) == {} | ||||
|             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_trying_to_reset_non_existing(password_reset_api, context_factory): | ||||
| def test_trying_to_reset_non_existing(context_factory): | ||||
|     with pytest.raises(errors.NotFoundError): | ||||
|         password_reset_api.get(context_factory(), 'u1') | ||||
|         api.password_reset_api.start_password_reset( | ||||
|             context_factory(), {'user_name': 'u1'}) | ||||
| 
 | ||||
| def test_trying_to_reset_without_email( | ||||
|         password_reset_api, context_factory, user_factory): | ||||
| def test_trying_to_reset_without_email(context_factory, user_factory): | ||||
|     db.session.add(user_factory(name='u1', rank=db.User.RANK_REGULAR, email=None)) | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         password_reset_api.get(context_factory(), 'u1') | ||||
|         api.password_reset_api.start_password_reset( | ||||
|             context_factory(), {'user_name': 'u1'}) | ||||
| 
 | ||||
| def test_confirming_with_good_token( | ||||
|         password_reset_api, context_factory, user_factory): | ||||
| def test_confirming_with_good_token(context_factory, user_factory): | ||||
|     user = user_factory( | ||||
|         name='u1', rank=db.User.RANK_REGULAR, email='user@example.com') | ||||
|     old_hash = user.password_hash | ||||
|     db.session.add(user) | ||||
|     context = context_factory( | ||||
|         input={'token': '4ac0be176fb364f13ee6b634c43220e2'}) | ||||
|     result = password_reset_api.post(context, 'u1') | ||||
|         params={'token': '4ac0be176fb364f13ee6b634c43220e2'}) | ||||
|     result = api.password_reset_api.finish_password_reset( | ||||
|         context, {'user_name': 'u1'}) | ||||
|     assert user.password_hash != old_hash | ||||
|     assert auth.is_valid_password(user, result['password']) is True | ||||
| 
 | ||||
| def test_trying_to_confirm_non_existing(password_reset_api, context_factory): | ||||
| def test_trying_to_confirm_non_existing(context_factory): | ||||
|     with pytest.raises(errors.NotFoundError): | ||||
|         password_reset_api.post(context_factory(), 'u1') | ||||
|         api.password_reset_api.finish_password_reset( | ||||
|             context_factory(), {'user_name': 'u1'}) | ||||
| 
 | ||||
| def test_trying_to_confirm_without_token( | ||||
|         password_reset_api, context_factory, user_factory): | ||||
| def test_trying_to_confirm_without_token(context_factory, user_factory): | ||||
|     db.session.add(user_factory( | ||||
|         name='u1', rank=db.User.RANK_REGULAR, email='user@example.com')) | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         password_reset_api.post(context_factory(input={}), 'u1') | ||||
|         api.password_reset_api.finish_password_reset( | ||||
|             context_factory(params={}), {'user_name': 'u1'}) | ||||
| 
 | ||||
| def test_trying_to_confirm_with_bad_token( | ||||
|         password_reset_api, context_factory, user_factory): | ||||
| def test_trying_to_confirm_with_bad_token(context_factory, user_factory): | ||||
|     db.session.add(user_factory( | ||||
|         name='u1', rank=db.User.RANK_REGULAR, email='user@example.com')) | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         password_reset_api.post( | ||||
|             context_factory(input={'token': 'bad'}), 'u1') | ||||
|         api.password_reset_api.finish_password_reset( | ||||
|             context_factory(params={'token': 'bad'}), {'user_name': 'u1'}) | ||||
|  | ||||
| @ -1,7 +1,5 @@ | ||||
| import datetime | ||||
| import os | ||||
| import unittest.mock | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import posts, tags, snapshots, net | ||||
| 
 | ||||
| @ -35,9 +33,9 @@ def test_creating_minimal_posts( | ||||
|         posts.create_post.return_value = (post, []) | ||||
|         posts.serialize_post.return_value = 'serialized post' | ||||
| 
 | ||||
|         result = api.PostListApi().post( | ||||
|         result = api.post_api.create_post( | ||||
|             context_factory( | ||||
|                 input={ | ||||
|                 params={ | ||||
|                     'safety': 'safe', | ||||
|                     'tags': ['tag1', 'tag2'], | ||||
|                 }, | ||||
| @ -79,9 +77,9 @@ def test_creating_full_posts(context_factory, post_factory, user_factory): | ||||
|         posts.create_post.return_value = (post, []) | ||||
|         posts.serialize_post.return_value = 'serialized post' | ||||
| 
 | ||||
|         result = api.PostListApi().post( | ||||
|         result = api.post_api.create_post( | ||||
|             context_factory( | ||||
|                 input={ | ||||
|                 params={ | ||||
|                     'safety': 'safe', | ||||
|                     'tags': ['tag1', 'tag2'], | ||||
|                     'relations': [1, 2], | ||||
| @ -122,9 +120,9 @@ def test_anonymous_uploads( | ||||
|             'privileges': {'posts:create:anonymous': db.User.RANK_REGULAR}, | ||||
|         }) | ||||
|         posts.create_post.return_value = [post, []] | ||||
|         api.PostListApi().post( | ||||
|         api.post_api.create_post( | ||||
|             context_factory( | ||||
|                 input={ | ||||
|                 params={ | ||||
|                     'safety': 'safe', | ||||
|                     'tags': ['tag1', 'tag2'], | ||||
|                     'anonymous': 'True', | ||||
| @ -154,9 +152,9 @@ def test_creating_from_url_saves_source( | ||||
|         }) | ||||
|         net.download.return_value = b'content' | ||||
|         posts.create_post.return_value = [post, []] | ||||
|         api.PostListApi().post( | ||||
|         api.post_api.create_post( | ||||
|             context_factory( | ||||
|                 input={ | ||||
|                 params={ | ||||
|                     'safety': 'safe', | ||||
|                     'tags': ['tag1', 'tag2'], | ||||
|                     'contentUrl': 'example.com', | ||||
| @ -185,9 +183,9 @@ def test_creating_from_url_with_source_specified( | ||||
|         }) | ||||
|         net.download.return_value = b'content' | ||||
|         posts.create_post.return_value = [post, []] | ||||
|         api.PostListApi().post( | ||||
|         api.post_api.create_post( | ||||
|             context_factory( | ||||
|                 input={ | ||||
|                 params={ | ||||
|                     'safety': 'safe', | ||||
|                     'tags': ['tag1', 'tag2'], | ||||
|                     'contentUrl': 'example.com', | ||||
| @ -201,23 +199,23 @@ def test_creating_from_url_with_source_specified( | ||||
| 
 | ||||
| @pytest.mark.parametrize('field', ['tags', 'safety']) | ||||
| def test_trying_to_omit_mandatory_field(context_factory, user_factory, field): | ||||
|     input = { | ||||
|     params = { | ||||
|         'safety': 'safe', | ||||
|         'tags': ['tag1', 'tag2'], | ||||
|     } | ||||
|     del input[field] | ||||
|     del params[field] | ||||
|     with pytest.raises(errors.MissingRequiredParameterError): | ||||
|         api.PostListApi().post( | ||||
|         api.post_api.create_post( | ||||
|             context_factory( | ||||
|                 input=input, | ||||
|                 params=params, | ||||
|                 files={'content': '...'}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_trying_to_omit_content(context_factory, user_factory): | ||||
|     with pytest.raises(errors.MissingRequiredFileError): | ||||
|         api.PostListApi().post( | ||||
|         api.post_api.create_post( | ||||
|             context_factory( | ||||
|                 input={ | ||||
|                 params={ | ||||
|                     'safety': 'safe', | ||||
|                     'tags': ['tag1', 'tag2'], | ||||
|                 }, | ||||
| @ -225,10 +223,9 @@ def test_trying_to_omit_content(context_factory, user_factory): | ||||
| 
 | ||||
| def test_trying_to_create_post_without_privileges(context_factory, user_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         api.PostListApi().post( | ||||
|             context_factory( | ||||
|                 input='whatever', | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.post_api.create_post(context_factory( | ||||
|             params='whatever', | ||||
|             user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
| 
 | ||||
| def test_trying_to_create_tags_without_privileges( | ||||
|         config_injector, context_factory, user_factory): | ||||
| @ -243,9 +240,9 @@ def test_trying_to_create_tags_without_privileges( | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_content'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_tags'): | ||||
|         posts.update_post_tags.return_value = ['new-tag'] | ||||
|         api.PostListApi().post( | ||||
|         api.post_api.create_post( | ||||
|             context_factory( | ||||
|                 input={ | ||||
|                 params={ | ||||
|                     'safety': 'safe', | ||||
|                     'tags': ['tag1', 'tag2'], | ||||
|                 }, | ||||
|  | ||||
| @ -1,49 +1,37 @@ | ||||
| import pytest | ||||
| import os | ||||
| from datetime import datetime | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import util, posts | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import posts, tags | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, post_factory, user_factory): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'privileges': { | ||||
|             'posts:delete': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.post_factory = post_factory | ||||
|     ret.api = api.PostDetailApi() | ||||
|     return ret | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'posts:delete': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| def test_deleting(test_ctx): | ||||
|     db.session.add(test_ctx.post_factory(id=1)) | ||||
| def test_deleting(user_factory, post_factory, context_factory): | ||||
|     db.session.add(post_factory(id=1)) | ||||
|     db.session.commit() | ||||
|     result = test_ctx.api.delete( | ||||
|         test_ctx.context_factory( | ||||
|             input={'version': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         1) | ||||
|     assert result == {} | ||||
|     assert db.session.query(db.Post).count() == 0 | ||||
|     assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json')) | ||||
|     with unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         result = api.post_api.delete_post( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'post_id': 1}) | ||||
|         assert result == {} | ||||
|         assert db.session.query(db.Post).count() == 0 | ||||
|         tags.export_to_json.assert_called_once_with() | ||||
| 
 | ||||
| def test_trying_to_delete_non_existing(test_ctx): | ||||
| def test_trying_to_delete_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(posts.PostNotFoundError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), '999') | ||||
|         api.post_api.delete_post( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'post_id': 999}) | ||||
| 
 | ||||
| def test_trying_to_delete_without_privileges(test_ctx): | ||||
|     db.session.add(test_ctx.post_factory(id=1)) | ||||
| def test_trying_to_delete_without_privileges( | ||||
|         user_factory, post_factory, context_factory): | ||||
|     db.session.add(post_factory(id=1)) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             1) | ||||
|         api.post_api.delete_post( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'post_id': 1}) | ||||
|     assert db.session.query(db.Post).count() == 1 | ||||
|  | ||||
| @ -1,132 +1,129 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, posts | ||||
| from szurubooru.func import posts | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, user_factory, post_factory): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'data_url': 'http://example.com', | ||||
|         'privileges': { | ||||
|             'posts:favorite': db.User.RANK_REGULAR, | ||||
|             'users:edit:any:email': db.User.RANK_MODERATOR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     db.session.flush() | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.post_factory = post_factory | ||||
|     ret.api = api.PostFavoriteApi() | ||||
|     return ret | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'posts:favorite': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| def test_adding_to_favorites(test_ctx, fake_datetime): | ||||
|     post = test_ctx.post_factory() | ||||
| def test_adding_to_favorites( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     assert post.score == 0 | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory(user=test_ctx.user_factory()), | ||||
|             post.post_id) | ||||
|     assert 'id' in result | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostFavorite).count() == 1 | ||||
|     assert post is not None | ||||
|     assert post.favorite_count == 1 | ||||
|     assert post.score == 1 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'), \ | ||||
|             fake_datetime('1997-12-01'): | ||||
|         posts.serialize_post.return_value = 'serialized post' | ||||
|         result = api.post_api.add_post_to_favorites( | ||||
|             context_factory(user=user_factory()), | ||||
|             {'post_id': post.post_id}) | ||||
|         assert result == 'serialized post' | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostFavorite).count() == 1 | ||||
|         assert post is not None | ||||
|         assert post.favorite_count == 1 | ||||
|         assert post.score == 1 | ||||
| 
 | ||||
| def test_removing_from_favorites(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory() | ||||
|     post = test_ctx.post_factory() | ||||
| def test_removing_from_favorites( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     user = user_factory() | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     assert post.score == 0 | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory(user=user), | ||||
|             post.post_id) | ||||
|     assert post.score == 1 | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.delete( | ||||
|             test_ctx.context_factory(user=user), | ||||
|             post.post_id) | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert post.score == 1 | ||||
|     assert db.session.query(db.PostFavorite).count() == 0 | ||||
|     assert post.favorite_count == 0 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             api.post_api.add_post_to_favorites( | ||||
|                 context_factory(user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         assert post.score == 1 | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             api.post_api.delete_post_from_favorites( | ||||
|                 context_factory(user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert post.score == 1 | ||||
|         assert db.session.query(db.PostFavorite).count() == 0 | ||||
|         assert post.favorite_count == 0 | ||||
| 
 | ||||
| def test_favoriting_twice(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory() | ||||
|     post = test_ctx.post_factory() | ||||
| def test_favoriting_twice( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     user = user_factory() | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory(user=user), | ||||
|             post.post_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory(user=user), | ||||
|             post.post_id) | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostFavorite).count() == 1 | ||||
|     assert post.favorite_count == 1 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             api.post_api.add_post_to_favorites( | ||||
|                 context_factory(user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             api.post_api.add_post_to_favorites( | ||||
|                 context_factory(user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostFavorite).count() == 1 | ||||
|         assert post.favorite_count == 1 | ||||
| 
 | ||||
| def test_removing_twice(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory() | ||||
|     post = test_ctx.post_factory() | ||||
| def test_removing_twice( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     user = user_factory() | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory(user=user), | ||||
|             post.post_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.delete( | ||||
|             test_ctx.context_factory(user=user), | ||||
|             post.post_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.delete( | ||||
|             test_ctx.context_factory(user=user), | ||||
|             post.post_id) | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostFavorite).count() == 0 | ||||
|     assert post.favorite_count == 0 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             api.post_api.add_post_to_favorites( | ||||
|                 context_factory(user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             api.post_api.delete_post_from_favorites( | ||||
|                 context_factory(user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             api.post_api.delete_post_from_favorites( | ||||
|                 context_factory(user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostFavorite).count() == 0 | ||||
|         assert post.favorite_count == 0 | ||||
| 
 | ||||
| def test_favorites_from_multiple_users(test_ctx, fake_datetime): | ||||
|     user1 = test_ctx.user_factory() | ||||
|     user2 = test_ctx.user_factory() | ||||
|     post = test_ctx.post_factory() | ||||
| def test_favorites_from_multiple_users( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     user1 = user_factory() | ||||
|     user2 = user_factory() | ||||
|     post = post_factory() | ||||
|     db.session.add_all([user1, user2, post]) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory(user=user1), | ||||
|             post.post_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory(user=user2), | ||||
|             post.post_id) | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostFavorite).count() == 2 | ||||
|     assert post.favorite_count == 2 | ||||
|     assert post.last_favorite_time == datetime.datetime(1997, 12, 2) | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             api.post_api.add_post_to_favorites( | ||||
|                 context_factory(user=user1), | ||||
|                 {'post_id': post.post_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             api.post_api.add_post_to_favorites( | ||||
|                 context_factory(user=user2), | ||||
|                 {'post_id': post.post_id}) | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostFavorite).count() == 2 | ||||
|         assert post.favorite_count == 2 | ||||
|         assert post.last_favorite_time == datetime(1997, 12, 2) | ||||
| 
 | ||||
| def test_trying_to_update_non_existing(test_ctx): | ||||
| def test_trying_to_update_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(posts.PostNotFoundError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory(user=test_ctx.user_factory()), 5) | ||||
|         api.post_api.add_post_to_favorites( | ||||
|             context_factory(user=user_factory()), | ||||
|             {'post_id': 5}) | ||||
| 
 | ||||
| def test_trying_to_rate_without_privileges(test_ctx): | ||||
|     post = test_ctx.post_factory() | ||||
| def test_trying_to_rate_without_privileges( | ||||
|         user_factory, post_factory, context_factory): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             post.post_id) | ||||
|         api.post_api.add_post_to_favorites( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'post_id': post.post_id}) | ||||
|  | ||||
| @ -1,107 +1,100 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, posts | ||||
| from szurubooru.func import posts | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, context_factory, config_injector, user_factory, post_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'data_url': 'http://example.com', | ||||
|         'privileges': { | ||||
|             'posts:feature': db.User.RANK_REGULAR, | ||||
|             'posts:view': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.post_factory = post_factory | ||||
|     ret.api = api.PostFeatureApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_no_featured_post(test_ctx): | ||||
| def test_no_featured_post(user_factory, post_factory, context_factory): | ||||
|     assert posts.try_get_featured_post() is None | ||||
|     result = test_ctx.api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result is None | ||||
| 
 | ||||
| def test_featuring(test_ctx): | ||||
|     db.session.add(test_ctx.post_factory(id=1)) | ||||
| def test_featuring(user_factory, post_factory, context_factory): | ||||
|     db.session.add(post_factory(id=1)) | ||||
|     db.session.commit() | ||||
|     assert not posts.get_post_by_id(1).is_featured | ||||
|     result = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={'id': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert posts.try_get_featured_post() is not None | ||||
|     assert posts.try_get_featured_post().post_id == 1 | ||||
|     assert posts.get_post_by_id(1).is_featured | ||||
|     assert 'id' in result | ||||
|     assert 'snapshots' in result | ||||
|     assert 'comments' in result | ||||
|     result = test_ctx.api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert 'id' in result | ||||
|     assert 'snapshots' in result | ||||
|     assert 'comments' in result | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         posts.serialize_post.return_value = 'serialized post' | ||||
|         result = api.post_api.set_featured_post( | ||||
|             context_factory( | ||||
|                 params={'id': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == 'serialized post' | ||||
|         assert posts.try_get_featured_post() is not None | ||||
|         assert posts.try_get_featured_post().post_id == 1 | ||||
|         assert posts.get_post_by_id(1).is_featured | ||||
|         result = api.post_api.get_featured_post( | ||||
|             context_factory( | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == 'serialized post' | ||||
| 
 | ||||
| def test_trying_to_feature_the_same_post_twice(test_ctx): | ||||
|     db.session.add(test_ctx.post_factory(id=1)) | ||||
| def test_trying_to_omit_required_parameter( | ||||
|         user_factory, post_factory, context_factory): | ||||
|     with pytest.raises(errors.MissingRequiredParameterError): | ||||
|         api.post_api.set_featured_post( | ||||
|             context_factory( | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_trying_to_feature_the_same_post_twice( | ||||
|         user_factory, post_factory, context_factory): | ||||
|     db.session.add(post_factory(id=1)) | ||||
|     db.session.commit() | ||||
|     test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={'id': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     with pytest.raises(posts.PostAlreadyFeaturedError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'id': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         api.post_api.set_featured_post( | ||||
|             context_factory( | ||||
|                 params={'id': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         with pytest.raises(posts.PostAlreadyFeaturedError): | ||||
|             api.post_api.set_featured_post( | ||||
|                 context_factory( | ||||
|                     params={'id': 1}, | ||||
|                     user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_featuring_one_post_after_another(test_ctx, fake_datetime): | ||||
|     db.session.add(test_ctx.post_factory(id=1)) | ||||
|     db.session.add(test_ctx.post_factory(id=2)) | ||||
| def test_featuring_one_post_after_another( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     db.session.add(post_factory(id=1)) | ||||
|     db.session.add(post_factory(id=2)) | ||||
|     db.session.commit() | ||||
|     assert posts.try_get_featured_post() is None | ||||
|     assert not posts.get_post_by_id(1).is_featured | ||||
|     assert not posts.get_post_by_id(2).is_featured | ||||
|     with fake_datetime('1997'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'id': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     with fake_datetime('1998'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'id': 2}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert posts.try_get_featured_post() is not None | ||||
|     assert posts.try_get_featured_post().post_id == 2 | ||||
|     assert not posts.get_post_by_id(1).is_featured | ||||
|     assert posts.get_post_by_id(2).is_featured | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997'): | ||||
|             result = api.post_api.set_featured_post( | ||||
|                 context_factory( | ||||
|                     params={'id': 1}, | ||||
|                     user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         with fake_datetime('1998'): | ||||
|             result = api.post_api.set_featured_post( | ||||
|                 context_factory( | ||||
|                     params={'id': 2}, | ||||
|                     user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert posts.try_get_featured_post() is not None | ||||
|         assert posts.try_get_featured_post().post_id == 2 | ||||
|         assert not posts.get_post_by_id(1).is_featured | ||||
|         assert posts.get_post_by_id(2).is_featured | ||||
| 
 | ||||
| def test_trying_to_feature_non_existing(test_ctx): | ||||
| def test_trying_to_feature_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(posts.PostNotFoundError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'id': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         api.post_api.set_featured_post( | ||||
|             context_factory( | ||||
|                 params={'id': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_trying_to_feature_without_privileges(test_ctx): | ||||
| def test_trying_to_feature_without_privileges(user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'id': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.post_api.set_featured_post( | ||||
|             context_factory( | ||||
|                 params={'id': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
| 
 | ||||
| def test_getting_featured_post_without_privileges_to_view(test_ctx): | ||||
|     try: | ||||
|         test_ctx.api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|     except: | ||||
|         pytest.fail() | ||||
| def test_getting_featured_post_without_privileges_to_view( | ||||
|         user_factory, context_factory): | ||||
|     api.post_api.get_featured_post( | ||||
|         context_factory(user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|  | ||||
| @ -1,147 +1,132 @@ | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, posts, scores | ||||
| from szurubooru.func import posts, scores | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, user_factory, post_factory): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'data_url': 'http://example.com', | ||||
|         'privileges': {'posts:score': db.User.RANK_REGULAR}, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     db.session.flush() | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.post_factory = post_factory | ||||
|     ret.api = api.PostScoreApi() | ||||
|     return ret | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'posts:score': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| def test_simple_rating(test_ctx, fake_datetime): | ||||
|     post = test_ctx.post_factory() | ||||
| def test_simple_rating( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'score': 1}, user=test_ctx.user_factory()), | ||||
|             post.post_id) | ||||
|     assert 'id' in result | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostScore).count() == 1 | ||||
|     assert post is not None | ||||
|     assert post.score == 1 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         posts.serialize_post.return_value = 'serialized post' | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.post_api.set_post_score( | ||||
|                 context_factory( | ||||
|                     params={'score': 1}, user=user_factory()), | ||||
|                 {'post_id': post.post_id}) | ||||
|         assert result == 'serialized post' | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostScore).count() == 1 | ||||
|         assert post is not None | ||||
|         assert post.score == 1 | ||||
| 
 | ||||
| def test_updating_rating(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory() | ||||
|     post = test_ctx.post_factory() | ||||
| def test_updating_rating( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     user = user_factory() | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user), | ||||
|             post.post_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': -1}, user=user), | ||||
|             post.post_id) | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostScore).count() == 1 | ||||
|     assert post.score == -1 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.post_api.set_post_score( | ||||
|                 context_factory(params={'score': 1}, user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             result = api.post_api.set_post_score( | ||||
|                 context_factory(params={'score': -1}, user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostScore).count() == 1 | ||||
|         assert post.score == -1 | ||||
| 
 | ||||
| def test_updating_rating_to_zero(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory() | ||||
|     post = test_ctx.post_factory() | ||||
| def test_updating_rating_to_zero( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     user = user_factory() | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user), | ||||
|             post.post_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 0}, user=user), | ||||
|             post.post_id) | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostScore).count() == 0 | ||||
|     assert post.score == 0 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.post_api.set_post_score( | ||||
|                 context_factory(params={'score': 1}, user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             result = api.post_api.set_post_score( | ||||
|                 context_factory(params={'score': 0}, user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostScore).count() == 0 | ||||
|         assert post.score == 0 | ||||
| 
 | ||||
| def test_deleting_rating(test_ctx, fake_datetime): | ||||
|     user = test_ctx.user_factory() | ||||
|     post = test_ctx.post_factory() | ||||
| def test_deleting_rating( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     user = user_factory() | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user), | ||||
|             post.post_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.delete( | ||||
|             test_ctx.context_factory(user=user), post.post_id) | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostScore).count() == 0 | ||||
|     assert post.score == 0 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.post_api.set_post_score( | ||||
|                 context_factory(params={'score': 1}, user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             result = api.post_api.delete_post_score( | ||||
|                 context_factory(user=user), | ||||
|                 {'post_id': post.post_id}) | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostScore).count() == 0 | ||||
|         assert post.score == 0 | ||||
| 
 | ||||
| def test_ratings_from_multiple_users(test_ctx, fake_datetime): | ||||
|     user1 = test_ctx.user_factory() | ||||
|     user2 = test_ctx.user_factory() | ||||
|     post = test_ctx.post_factory() | ||||
| def test_ratings_from_multiple_users( | ||||
|         user_factory, post_factory, context_factory, fake_datetime): | ||||
|     user1 = user_factory() | ||||
|     user2 = user_factory() | ||||
|     post = post_factory() | ||||
|     db.session.add_all([user1, user2, post]) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': 1}, user=user1), | ||||
|             post.post_id) | ||||
|     with fake_datetime('1997-12-02'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={'score': -1}, user=user2), | ||||
|             post.post_id) | ||||
|     post = db.session.query(db.Post).one() | ||||
|     assert db.session.query(db.PostScore).count() == 2 | ||||
|     assert post.score == 0 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         with fake_datetime('1997-12-01'): | ||||
|             result = api.post_api.set_post_score( | ||||
|                 context_factory(params={'score': 1}, user=user1), | ||||
|                 {'post_id': post.post_id}) | ||||
|         with fake_datetime('1997-12-02'): | ||||
|             result = api.post_api.set_post_score( | ||||
|                 context_factory(params={'score': -1}, user=user2), | ||||
|                 {'post_id': post.post_id}) | ||||
|         post = db.session.query(db.Post).one() | ||||
|         assert db.session.query(db.PostScore).count() == 2 | ||||
|         assert post.score == 0 | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_exception', [ | ||||
|     ({'score': None}, errors.ValidationError), | ||||
|     ({'score': ''}, errors.ValidationError), | ||||
|     ({'score': -2}, scores.InvalidScoreValueError), | ||||
|     ({'score': 2}, scores.InvalidScoreValueError), | ||||
|     ({'score': [1]}, errors.ValidationError), | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception): | ||||
|     post = test_ctx.post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(expected_exception): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory(input=input, user=test_ctx.user_factory()), | ||||
|             post.post_id) | ||||
| 
 | ||||
| def test_trying_to_omit_mandatory_field(test_ctx): | ||||
|     post = test_ctx.post_factory() | ||||
| def test_trying_to_omit_mandatory_field( | ||||
|         user_factory, post_factory, context_factory): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory(input={}, user=test_ctx.user_factory()), | ||||
|             post.post_id) | ||||
|         api.post_api.set_post_score( | ||||
|             context_factory(params={}, user=user_factory()), | ||||
|             {'post_id': post.post_id}) | ||||
| 
 | ||||
| def test_trying_to_update_non_existing(test_ctx): | ||||
| def test_trying_to_update_non_existing( | ||||
|         user_factory, post_factory, context_factory): | ||||
|     with pytest.raises(posts.PostNotFoundError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'score': 1}, | ||||
|                 user=test_ctx.user_factory()), | ||||
|             5) | ||||
|         api.post_api.set_post_score( | ||||
|             context_factory(params={'score': 1}, user=user_factory()), | ||||
|             {'post_id': 5}) | ||||
| 
 | ||||
| def test_trying_to_rate_without_privileges(test_ctx): | ||||
|     post = test_ctx.post_factory() | ||||
| def test_trying_to_rate_without_privileges( | ||||
|         user_factory, post_factory, context_factory): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'score': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             post.post_id) | ||||
|         api.post_api.set_post_score( | ||||
|             context_factory( | ||||
|                 params={'score': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'post_id': post.post_id}) | ||||
|  | ||||
| @ -1,105 +1,97 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, posts | ||||
| from szurubooru.func import posts | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, context_factory, config_injector, user_factory, post_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(tmpdir, config_injector): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'data_url': 'http://example.com', | ||||
|         'privileges': { | ||||
|             'posts:list': db.User.RANK_REGULAR, | ||||
|             'posts:view': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.post_factory = post_factory | ||||
|     ret.list_api = api.PostListApi() | ||||
|     ret.detail_api = api.PostDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_retrieving_multiple(test_ctx): | ||||
|     post1 = test_ctx.post_factory(id=1) | ||||
|     post2 = test_ctx.post_factory(id=2) | ||||
| def test_retrieving_multiple(user_factory, post_factory, context_factory): | ||||
|     post1 = post_factory(id=1) | ||||
|     post2 = post_factory(id=2) | ||||
|     db.session.add_all([post1, post2]) | ||||
|     result = test_ctx.list_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             input={'query': '', 'page': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result['query'] == '' | ||||
|     assert result['page'] == 1 | ||||
|     assert result['pageSize'] == 100 | ||||
|     assert result['total'] == 2 | ||||
|     assert [t['id'] for t in result['results']] == [2, 1] | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         posts.serialize_post.return_value = 'serialized post' | ||||
|         result = api.post_api.get_posts( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == { | ||||
|             'query': '', | ||||
|             'page': 1, | ||||
|             'pageSize': 100, | ||||
|             'total': 2, | ||||
|             'results': ['serialized post', 'serialized post'], | ||||
|         } | ||||
| 
 | ||||
| def test_using_special_tokens( | ||||
|         test_ctx, config_injector): | ||||
|     auth_user = test_ctx.user_factory(rank=db.User.RANK_REGULAR) | ||||
|     post1 = test_ctx.post_factory(id=1) | ||||
|     post2 = test_ctx.post_factory(id=2) | ||||
| def test_using_special_tokens(user_factory, post_factory, context_factory): | ||||
|     auth_user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     post1 = post_factory(id=1) | ||||
|     post2 = post_factory(id=2) | ||||
|     post1.favorited_by = [db.PostFavorite( | ||||
|         user=auth_user, time=datetime.datetime.utcnow())] | ||||
|         user=auth_user, time=datetime.utcnow())] | ||||
|     db.session.add_all([post1, post2, auth_user]) | ||||
|     db.session.flush() | ||||
|     result = test_ctx.list_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             input={'query': 'special:fav', 'page': 1}, | ||||
|             user=auth_user)) | ||||
|     assert result['query'] == 'special:fav' | ||||
|     assert result['page'] == 1 | ||||
|     assert result['pageSize'] == 100 | ||||
|     assert result['total'] == 1 | ||||
|     assert [t['id'] for t in result['results']] == [1] | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         posts.serialize_post.side_effect = \ | ||||
|             lambda post, *_args, **_kwargs: \ | ||||
|                 'serialized post %d' % post.post_id | ||||
|         result = api.post_api.get_posts( | ||||
|             context_factory( | ||||
|                 params={'query': 'special:fav', 'page': 1}, | ||||
|                 user=auth_user)) | ||||
|         assert result == { | ||||
|             'query': 'special:fav', | ||||
|             'page': 1, | ||||
|             'pageSize': 100, | ||||
|             'total': 1, | ||||
|             'results': ['serialized post 1'], | ||||
|         } | ||||
| 
 | ||||
| def test_trying_to_use_special_tokens_without_logging_in( | ||||
|         test_ctx, config_injector): | ||||
|         user_factory, post_factory, context_factory, config_injector): | ||||
|     config_injector({ | ||||
|         'privileges': {'posts:list': 'anonymous'}, | ||||
|     }) | ||||
|     with pytest.raises(errors.SearchError): | ||||
|         test_ctx.list_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'query': 'special:fav', 'page': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.post_api.get_posts( | ||||
|             context_factory( | ||||
|                 params={'query': 'special:fav', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
| 
 | ||||
| def test_trying_to_retrieve_multiple_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_multiple_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.list_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'query': '', 'page': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.post_api.get_posts( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
| 
 | ||||
| def test_retrieving_single(test_ctx): | ||||
|     db.session.add(test_ctx.post_factory(id=1)) | ||||
|     result = test_ctx.detail_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), 1) | ||||
|     assert 'id' in result | ||||
|     assert 'snapshots' in result | ||||
|     assert 'comments' in result | ||||
| def test_retrieving_single(user_factory, post_factory, context_factory): | ||||
|     db.session.add(post_factory(id=1)) | ||||
|     with unittest.mock.patch('szurubooru.func.posts.serialize_post'): | ||||
|         posts.serialize_post.return_value = 'serialized post' | ||||
|         result = api.post_api.get_post( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'post_id': 1}) | ||||
|         assert result == 'serialized post' | ||||
| 
 | ||||
| def test_trying_to_retrieve_invalid_id(test_ctx): | ||||
|     with pytest.raises(posts.InvalidPostIdError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             '-') | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_non_existing(test_ctx): | ||||
| def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(posts.PostNotFoundError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             '999') | ||||
|         api.post_api.get_post( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'post_id': 999}) | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_single_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             '999') | ||||
|         api.post_api.get_post( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'post_id': 999}) | ||||
|  | ||||
| @ -1,12 +1,11 @@ | ||||
| import datetime | ||||
| import os | ||||
| import unittest.mock | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import posts, tags, snapshots, net | ||||
| 
 | ||||
| def test_post_updating( | ||||
|         config_injector, context_factory, post_factory, user_factory, fake_datetime): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(tmpdir, config_injector): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'posts:edit:tags': db.User.RANK_REGULAR, | ||||
| @ -17,46 +16,49 @@ def test_post_updating( | ||||
|             'posts:edit:notes': db.User.RANK_REGULAR, | ||||
|             'posts:edit:flags': db.User.RANK_REGULAR, | ||||
|             'posts:edit:thumbnail': db.User.RANK_REGULAR, | ||||
|             'tags:create': db.User.RANK_MODERATOR, | ||||
|         }, | ||||
|     }) | ||||
| 
 | ||||
| def test_post_updating( | ||||
|         context_factory, post_factory, user_factory, fake_datetime): | ||||
|     auth_user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.flush() | ||||
| 
 | ||||
|     with unittest.mock.patch('szurubooru.func.posts.create_post'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_tags'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_content'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_thumbnail'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_safety'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_source'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_relations'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_notes'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_flags'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.serialize_post'), \ | ||||
|         unittest.mock.patch('szurubooru.func.tags.export_to_json'), \ | ||||
|         unittest.mock.patch('szurubooru.func.snapshots.save_entity_modification'): | ||||
| 
 | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_tags'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_content'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_thumbnail'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_safety'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_source'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_relations'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_notes'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_flags'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.serialize_post'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'), \ | ||||
|             unittest.mock.patch('szurubooru.func.snapshots.save_entity_modification'), \ | ||||
|             fake_datetime('1997-01-01'): | ||||
|         posts.serialize_post.return_value = 'serialized post' | ||||
| 
 | ||||
|         with fake_datetime('1997-01-01'): | ||||
|             result = api.PostDetailApi().put( | ||||
|                 context_factory( | ||||
|                     input={ | ||||
|                         'version': 1, | ||||
|                         'safety': 'safe', | ||||
|                         'tags': ['tag1', 'tag2'], | ||||
|                         'relations': [1, 2], | ||||
|                         'source': 'source', | ||||
|                         'notes': ['note1', 'note2'], | ||||
|                         'flags': ['flag1', 'flag2'], | ||||
|                     }, | ||||
|                     files={ | ||||
|                         'content': 'post-content', | ||||
|                         'thumbnail': 'post-thumbnail', | ||||
|                     }, | ||||
|                     user=auth_user), | ||||
|                 post.post_id) | ||||
|         result = api.post_api.update_post( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'version': 1, | ||||
|                     'safety': 'safe', | ||||
|                     'tags': ['tag1', 'tag2'], | ||||
|                     'relations': [1, 2], | ||||
|                     'source': 'source', | ||||
|                     'notes': ['note1', 'note2'], | ||||
|                     'flags': ['flag1', 'flag2'], | ||||
|                 }, | ||||
|                 files={ | ||||
|                     'content': 'post-content', | ||||
|                     'thumbnail': 'post-thumbnail', | ||||
|                 }, | ||||
|                 user=auth_user), | ||||
|             {'post_id': post.post_id}) | ||||
| 
 | ||||
|         assert result == 'serialized post' | ||||
|         posts.create_post.assert_not_called() | ||||
| @ -71,71 +73,62 @@ def test_post_updating( | ||||
|         posts.serialize_post.assert_called_once_with(post, auth_user, options=None) | ||||
|         tags.export_to_json.assert_called_once_with() | ||||
|         snapshots.save_entity_modification.assert_called_once_with(post, auth_user) | ||||
|         assert post.last_edit_time == datetime.datetime(1997, 1, 1) | ||||
|         assert post.last_edit_time == datetime(1997, 1, 1) | ||||
| 
 | ||||
| def test_uploading_from_url_saves_source( | ||||
|         config_injector, context_factory, post_factory, user_factory): | ||||
|     config_injector({ | ||||
|         'privileges': {'posts:edit:content': db.User.RANK_REGULAR}, | ||||
|     }) | ||||
|         context_factory, post_factory, user_factory): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.flush() | ||||
|     with unittest.mock.patch('szurubooru.func.net.download'), \ | ||||
|         unittest.mock.patch('szurubooru.func.tags.export_to_json'), \ | ||||
|         unittest.mock.patch('szurubooru.func.snapshots.save_entity_modification'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.serialize_post'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_content'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_source'): | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'), \ | ||||
|             unittest.mock.patch('szurubooru.func.snapshots.save_entity_modification'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.serialize_post'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_content'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_source'): | ||||
|         net.download.return_value = b'content' | ||||
|         api.PostDetailApi().put( | ||||
|         api.post_api.update_post( | ||||
|             context_factory( | ||||
|                 input={'contentUrl': 'example.com', 'version': 1}, | ||||
|                 params={'contentUrl': 'example.com', 'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             post.post_id) | ||||
|             {'post_id': post.post_id}) | ||||
|         net.download.assert_called_once_with('example.com') | ||||
|         posts.update_post_content.assert_called_once_with(post, b'content') | ||||
|         posts.update_post_source.assert_called_once_with(post, 'example.com') | ||||
| 
 | ||||
| def test_uploading_from_url_with_source_specified( | ||||
|         config_injector, context_factory, post_factory, user_factory): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'posts:edit:content': db.User.RANK_REGULAR, | ||||
|             'posts:edit:source': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|     }) | ||||
|         context_factory, post_factory, user_factory): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.flush() | ||||
|     with unittest.mock.patch('szurubooru.func.net.download'), \ | ||||
|         unittest.mock.patch('szurubooru.func.tags.export_to_json'), \ | ||||
|         unittest.mock.patch('szurubooru.func.snapshots.save_entity_modification'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.serialize_post'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_content'), \ | ||||
|         unittest.mock.patch('szurubooru.func.posts.update_post_source'): | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'), \ | ||||
|             unittest.mock.patch('szurubooru.func.snapshots.save_entity_modification'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.serialize_post'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_content'), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_source'): | ||||
|         net.download.return_value = b'content' | ||||
|         api.PostDetailApi().put( | ||||
|         api.post_api.update_post( | ||||
|             context_factory( | ||||
|                 input={ | ||||
|                 params={ | ||||
|                     'contentUrl': 'example.com', | ||||
|                     'source': 'example2.com', | ||||
|                     'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             post.post_id) | ||||
|             {'post_id': post.post_id}) | ||||
|         net.download.assert_called_once_with('example.com') | ||||
|         posts.update_post_content.assert_called_once_with(post, b'content') | ||||
|         posts.update_post_source.assert_called_once_with(post, 'example2.com') | ||||
| 
 | ||||
| def test_trying_to_update_non_existing(context_factory, user_factory): | ||||
|     with pytest.raises(posts.PostNotFoundError): | ||||
|         api.PostDetailApi().put( | ||||
|         api.post_api.update_post( | ||||
|             context_factory( | ||||
|                 input='whatever', | ||||
|                 params='whatever', | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             1) | ||||
|             {'post_id': 1}) | ||||
| 
 | ||||
| @pytest.mark.parametrize('privilege,files,input', [ | ||||
| @pytest.mark.parametrize('privilege,files,params', [ | ||||
|     ('posts:edit:tags', {}, {'tags': '...'}), | ||||
|     ('posts:edit:safety', {}, {'safety': '...'}), | ||||
|     ('posts:edit:source', {}, {'source': '...'}), | ||||
| @ -146,43 +139,28 @@ def test_trying_to_update_non_existing(context_factory, user_factory): | ||||
|     ('posts:edit:thumbnail', {'thumbnail': '...'}, {}), | ||||
| ]) | ||||
| def test_trying_to_update_field_without_privileges( | ||||
|         config_injector, | ||||
|         context_factory, | ||||
|         post_factory, | ||||
|         user_factory, | ||||
|         files, | ||||
|         input, | ||||
|         privilege): | ||||
|     config_injector({ | ||||
|         'privileges': {privilege: db.User.RANK_REGULAR}, | ||||
|     }) | ||||
|         context_factory, post_factory, user_factory, files, params, privilege): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.flush() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         api.PostDetailApi().put( | ||||
|         api.post_api.update_post( | ||||
|             context_factory( | ||||
|                 input={**input, **{'version': 1}}, | ||||
|                 params={**params, **{'version': 1}}, | ||||
|                 files=files, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             post.post_id) | ||||
|             {'post_id': post.post_id}) | ||||
| 
 | ||||
| def test_trying_to_create_tags_without_privileges( | ||||
|         config_injector, context_factory, post_factory, user_factory): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'posts:edit:tags': db.User.RANK_REGULAR, | ||||
|             'tags:create': db.User.RANK_ADMINISTRATOR, | ||||
|         }, | ||||
|     }) | ||||
|         context_factory, post_factory, user_factory): | ||||
|     post = post_factory() | ||||
|     db.session.add(post) | ||||
|     db.session.flush() | ||||
|     with pytest.raises(errors.AuthError), \ | ||||
|             unittest.mock.patch('szurubooru.func.posts.update_post_tags'): | ||||
|         posts.update_post_tags.return_value = ['new-tag'] | ||||
|         api.PostDetailApi().put( | ||||
|         api.post_api.update_post( | ||||
|             context_factory( | ||||
|                 input={'tags': ['tag1', 'tag2'], 'version': 1}, | ||||
|                 params={'tags': ['tag1', 'tag2'], 'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             post.post_id) | ||||
|             {'post_id': post.post_id}) | ||||
|  | ||||
| @ -1,11 +1,10 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, tags | ||||
| 
 | ||||
| def snapshot_factory(): | ||||
|     snapshot = db.Snapshot() | ||||
|     snapshot.creation_time = datetime.datetime(1999, 1, 1) | ||||
|     snapshot.creation_time = datetime(1999, 1, 1) | ||||
|     snapshot.resource_type = 'dummy' | ||||
|     snapshot.resource_id = 1 | ||||
|     snapshot.resource_repr = 'dummy' | ||||
| @ -13,37 +12,30 @@ def snapshot_factory(): | ||||
|     snapshot.data = '{}' | ||||
|     return snapshot | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx(context_factory, config_injector, user_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'snapshots:list': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|         'privileges': {'snapshots:list': db.User.RANK_REGULAR}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.api = api.SnapshotListApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_retrieving_multiple(test_ctx): | ||||
| def test_retrieving_multiple(user_factory, context_factory): | ||||
|     snapshot1 = snapshot_factory() | ||||
|     snapshot2 = snapshot_factory() | ||||
|     db.session.add_all([snapshot1, snapshot2]) | ||||
|     result = test_ctx.api.get( | ||||
|         test_ctx.context_factory( | ||||
|             input={'query': '', 'page': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     result = api.snapshot_api.get_snapshots( | ||||
|         context_factory( | ||||
|             params={'query': '', 'page': 1}, | ||||
|             user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result['query'] == '' | ||||
|     assert result['page'] == 1 | ||||
|     assert result['pageSize'] == 100 | ||||
|     assert result['total'] == 2 | ||||
|     assert len(result['results']) == 2 | ||||
| 
 | ||||
| def test_trying_to_retrieve_multiple_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_multiple_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'query': '', 'page': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.snapshot_api.get_snapshots( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|  | ||||
| @ -1,94 +1,50 @@ | ||||
| import os | ||||
| import pytest | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import util, tag_categories | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import tag_categories, tags | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx(tmpdir, config_injector, context_factory, user_factory): | ||||
| def _update_category_name(category, name): | ||||
|     category.name = name | ||||
| 
 | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'tag_category_name_regex': '^[^!]+$', | ||||
|         'privileges': {'tag_categories:create': db.User.RANK_REGULAR}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.api = api.TagCategoryListApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_creating_category(test_ctx): | ||||
|     result = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={'name': 'meta', 'color': 'black'}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert len(result['snapshots']) == 1 | ||||
|     del result['snapshots'] | ||||
|     assert result == { | ||||
|         'name': 'meta', | ||||
|         'color': 'black', | ||||
|         'usages': 0, | ||||
|         'default': True, | ||||
|         'version': 1, | ||||
|     } | ||||
|     category = db.session.query(db.TagCategory).one() | ||||
|     assert category.name == 'meta' | ||||
|     assert category.color == 'black' | ||||
|     assert category.tag_count == 0 | ||||
|     assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json')) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input', [ | ||||
|     {'name': None}, | ||||
|     {'name': ''}, | ||||
|     {'name': '!bad'}, | ||||
|     {'color': None}, | ||||
|     {'color': ''}, | ||||
|     {'color': 'a' * 100}, | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input): | ||||
|     real_input = { | ||||
|         'name': 'okay', | ||||
|         'color': 'okay', | ||||
|     } | ||||
|     for key, value in input.items(): | ||||
|         real_input[key] = value | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input=real_input, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
| def test_creating_category(user_factory, context_factory): | ||||
|     with unittest.mock.patch('szurubooru.func.tag_categories.serialize_category'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tag_categories.update_category_name'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         tag_categories.update_category_name.side_effect = _update_category_name | ||||
|         tag_categories.serialize_category.return_value = 'serialized category' | ||||
|         result = api.tag_category_api.create_tag_category( | ||||
|             context_factory( | ||||
|                 params={'name': 'meta', 'color': 'black'}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == 'serialized category' | ||||
|         category = db.session.query(db.TagCategory).one() | ||||
|         assert category.name == 'meta' | ||||
|         assert category.color == 'black' | ||||
|         assert category.tag_count == 0 | ||||
|         tags.export_to_json.assert_called_once_with() | ||||
| 
 | ||||
| @pytest.mark.parametrize('field', ['name', 'color']) | ||||
| def test_trying_to_omit_mandatory_field(test_ctx, field): | ||||
|     input = { | ||||
| def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): | ||||
|     params = { | ||||
|         'name': 'meta', | ||||
|         'color': 'black', | ||||
|     } | ||||
|     del input[field] | ||||
|     del params[field] | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input=input, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         api.tag_category_api.create_tag_category( | ||||
|             context_factory( | ||||
|                 params=params, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_trying_to_use_existing_name(test_ctx): | ||||
|     result = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={'name': 'meta', 'color': 'black'}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     with pytest.raises(tag_categories.TagCategoryAlreadyExistsError): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': 'meta', 'color': 'black'}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     with pytest.raises(tag_categories.TagCategoryAlreadyExistsError): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': 'META', 'color': 'black'}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_trying_to_create_without_privileges(test_ctx): | ||||
| def test_trying_to_create_without_privileges(user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': 'meta', 'color': 'black'}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.tag_category_api.create_tag_category( | ||||
|             context_factory( | ||||
|                 params={'name': 'meta', 'color': 'black'}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|  | ||||
| @ -1,84 +1,70 @@ | ||||
| import pytest | ||||
| import os | ||||
| from datetime import datetime | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import util, tags, tag_categories | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import tag_categories, tags | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, | ||||
|         config_injector, | ||||
|         context_factory, | ||||
|         tag_factory, | ||||
|         tag_category_factory, | ||||
|         user_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'privileges': { | ||||
|             'tag_categories:delete': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|         'privileges': {'tag_categories:delete': db.User.RANK_REGULAR}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_factory = tag_factory | ||||
|     ret.tag_category_factory = tag_category_factory | ||||
|     ret.api = api.TagCategoryDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_deleting(test_ctx): | ||||
|     db.session.add(test_ctx.tag_category_factory(name='root')) | ||||
|     db.session.add(test_ctx.tag_category_factory(name='category')) | ||||
| def test_deleting(user_factory, tag_category_factory, context_factory): | ||||
|     db.session.add(tag_category_factory(name='root')) | ||||
|     db.session.add(tag_category_factory(name='category')) | ||||
|     db.session.commit() | ||||
|     result = test_ctx.api.delete( | ||||
|         test_ctx.context_factory( | ||||
|             input={'version': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'category') | ||||
|     assert result == {} | ||||
|     assert db.session.query(db.TagCategory).count() == 1 | ||||
|     assert db.session.query(db.TagCategory).one().name == 'root' | ||||
|     assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json')) | ||||
|     with unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         result = api.tag_category_api.delete_tag_category( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': 'category'}) | ||||
|         assert result == {} | ||||
|         assert db.session.query(db.TagCategory).count() == 1 | ||||
|         assert db.session.query(db.TagCategory).one().name == 'root' | ||||
|         tags.export_to_json.assert_called_once_with() | ||||
| 
 | ||||
| def test_trying_to_delete_used(test_ctx, tag_factory): | ||||
|     category = test_ctx.tag_category_factory(name='category') | ||||
| def test_trying_to_delete_used( | ||||
|         user_factory, tag_category_factory, tag_factory, context_factory): | ||||
|     category = tag_category_factory(name='category') | ||||
|     db.session.add(category) | ||||
|     db.session.flush() | ||||
|     tag = test_ctx.tag_factory(names=['tag'], category=category) | ||||
|     tag = tag_factory(names=['tag'], category=category) | ||||
|     db.session.add(tag) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(tag_categories.TagCategoryIsInUseError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'version': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'category') | ||||
|         api.tag_category_api.delete_tag_category( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': 'category'}) | ||||
|     assert db.session.query(db.TagCategory).count() == 1 | ||||
| 
 | ||||
| def test_trying_to_delete_last(test_ctx, tag_factory): | ||||
|     db.session.add(test_ctx.tag_category_factory(name='root')) | ||||
| def test_trying_to_delete_last( | ||||
|         user_factory, tag_category_factory, context_factory): | ||||
|     db.session.add(tag_category_factory(name='root')) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(tag_categories.TagCategoryIsInUseError): | ||||
|         result = test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'version': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'root') | ||||
|         api.tag_category_api.delete_tag_category( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': 'root'}) | ||||
| 
 | ||||
| def test_trying_to_delete_non_existing(test_ctx): | ||||
| def test_trying_to_delete_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(tag_categories.TagCategoryNotFoundError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'bad') | ||||
|         api.tag_category_api.delete_tag_category( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': 'bad'}) | ||||
| 
 | ||||
| def test_trying_to_delete_without_privileges(test_ctx): | ||||
|     db.session.add(test_ctx.tag_category_factory(name='category')) | ||||
| def test_trying_to_delete_without_privileges( | ||||
|         user_factory, tag_category_factory, context_factory): | ||||
|     db.session.add(tag_category_factory(name='category')) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'version': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             'category') | ||||
|         api.tag_category_api.delete_tag_category( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'category_name': 'category'}) | ||||
|     assert db.session.query(db.TagCategory).count() == 1 | ||||
|  | ||||
| @ -1,42 +1,31 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, tag_categories | ||||
| from szurubooru.func import tag_categories | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         context_factory, config_injector, user_factory, tag_category_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'tag_categories:list': db.User.RANK_REGULAR, | ||||
|             'tag_categories:view': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_category_factory = tag_category_factory | ||||
|     ret.list_api = api.TagCategoryListApi() | ||||
|     ret.detail_api = api.TagCategoryDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_retrieving_multiple(test_ctx): | ||||
| def test_retrieving_multiple( | ||||
|         user_factory, tag_category_factory, context_factory): | ||||
|     db.session.add_all([ | ||||
|         test_ctx.tag_category_factory(name='c1'), | ||||
|         test_ctx.tag_category_factory(name='c2'), | ||||
|         tag_category_factory(name='c1'), | ||||
|         tag_category_factory(name='c2'), | ||||
|     ]) | ||||
|     result = test_ctx.list_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     result = api.tag_category_api.get_tag_categories( | ||||
|         context_factory(user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert [cat['name'] for cat in result['results']] == ['c1', 'c2'] | ||||
| 
 | ||||
| def test_retrieving_single(test_ctx): | ||||
|     db.session.add(test_ctx.tag_category_factory(name='cat')) | ||||
|     result = test_ctx.detail_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'cat') | ||||
| def test_retrieving_single(user_factory, tag_category_factory, context_factory): | ||||
|     db.session.add(tag_category_factory(name='cat')) | ||||
|     result = api.tag_category_api.get_tag_category( | ||||
|         context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         {'category_name': 'cat'}) | ||||
|     assert result == { | ||||
|         'name': 'cat', | ||||
|         'color': 'dummy', | ||||
| @ -46,16 +35,15 @@ def test_retrieving_single(test_ctx): | ||||
|         'version': 1, | ||||
|     } | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_non_existing(test_ctx): | ||||
| def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(tag_categories.TagCategoryNotFoundError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             '-') | ||||
|         api.tag_category_api.get_tag_category( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': '-'}) | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_single_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             '-') | ||||
|         api.tag_category_api.get_tag_category( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'category_name': '-'}) | ||||
|  | ||||
| @ -1,137 +1,104 @@ | ||||
| import os | ||||
| import pytest | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import util, tag_categories | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import tag_categories, tags | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, | ||||
|         config_injector, | ||||
|         context_factory, | ||||
|         user_factory, | ||||
|         tag_category_factory): | ||||
| def _update_category_name(category, name): | ||||
|     category.name = name | ||||
| 
 | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'tag_category_name_regex': '^[^!]*$', | ||||
|         'privileges': { | ||||
|             'tag_categories:edit:name': db.User.RANK_REGULAR, | ||||
|             'tag_categories:edit:color': db.User.RANK_REGULAR, | ||||
|             'tag_categories:set_default': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_category_factory = tag_category_factory | ||||
|     ret.api = api.TagCategoryDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_simple_updating(test_ctx): | ||||
|     category = test_ctx.tag_category_factory(name='name', color='black') | ||||
| def test_simple_updating(user_factory, tag_category_factory, context_factory): | ||||
|     category = tag_category_factory(name='name', color='black') | ||||
|     db.session.add(category) | ||||
|     db.session.commit() | ||||
|     result = test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={ | ||||
|                 'name': 'changed', | ||||
|                 'color': 'white', | ||||
|                 'version': 1, | ||||
|             }, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'name') | ||||
|     assert len(result['snapshots']) == 1 | ||||
|     del result['snapshots'] | ||||
|     assert result == { | ||||
|         'name': 'changed', | ||||
|         'color': 'white', | ||||
|         'usages': 0, | ||||
|         'default': False, | ||||
|         'version': 2, | ||||
|     } | ||||
|     assert tag_categories.try_get_category_by_name('name') is None | ||||
|     category = tag_categories.get_category_by_name('changed') | ||||
|     assert category is not None | ||||
|     assert category.name == 'changed' | ||||
|     assert category.color == 'white' | ||||
|     assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json')) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_exception', [ | ||||
|     ({'name': None}, tag_categories.InvalidTagCategoryNameError), | ||||
|     ({'name': ''}, tag_categories.InvalidTagCategoryNameError), | ||||
|     ({'name': '!bad'}, tag_categories.InvalidTagCategoryNameError), | ||||
|     ({'color': None}, tag_categories.InvalidTagCategoryColorError), | ||||
|     ({'color': ''}, tag_categories.InvalidTagCategoryColorError), | ||||
|     ({'color': '; float:left'}, tag_categories.InvalidTagCategoryColorError), | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception): | ||||
|     db.session.add(test_ctx.tag_category_factory(name='meta', color='black')) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(expected_exception): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={**input, **{'version': 1}}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'meta') | ||||
|     with unittest.mock.patch('szurubooru.func.tag_categories.serialize_category'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tag_categories.update_category_name'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tag_categories.update_category_color'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         tag_categories.update_category_name.side_effect = _update_category_name | ||||
|         tag_categories.serialize_category.return_value = 'serialized category' | ||||
|         result = api.tag_category_api.update_tag_category( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'name': 'changed', | ||||
|                     'color': 'white', | ||||
|                     'version': 1, | ||||
|                 }, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': 'name'}) | ||||
|         assert result == 'serialized category' | ||||
|         tag_categories.update_category_name.assert_called_once_with(category, 'changed') | ||||
|         tag_categories.update_category_color.assert_called_once_with(category, 'white') | ||||
|         tags.export_to_json.assert_called_once_with() | ||||
| 
 | ||||
| @pytest.mark.parametrize('field', ['name', 'color']) | ||||
| def test_omitting_optional_field(test_ctx, field): | ||||
|     db.session.add(test_ctx.tag_category_factory(name='name', color='black')) | ||||
| def test_omitting_optional_field( | ||||
|         user_factory, tag_category_factory, context_factory, field): | ||||
|     db.session.add(tag_category_factory(name='name', color='black')) | ||||
|     db.session.commit() | ||||
|     input = { | ||||
|     params = { | ||||
|         'name': 'changed', | ||||
|         'color': 'white', | ||||
|     } | ||||
|     del input[field] | ||||
|     result = test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={**input, **{'version': 1}}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'name') | ||||
|     assert result is not None | ||||
|     del params[field] | ||||
|     with unittest.mock.patch('szurubooru.func.tag_categories.serialize_category'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tag_categories.update_category_name'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         api.tag_category_api.update_tag_category( | ||||
|             context_factory( | ||||
|                 params={**params, **{'version': 1}}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': 'name'}) | ||||
| 
 | ||||
| def test_trying_to_update_non_existing(test_ctx): | ||||
| def test_trying_to_update_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(tag_categories.TagCategoryNotFoundError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': ['dummy']}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'bad') | ||||
|         api.tag_category_api.update_tag_category( | ||||
|             context_factory( | ||||
|                 params={'name': ['dummy']}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': 'bad'}) | ||||
| 
 | ||||
| @pytest.mark.parametrize('new_name', ['cat', 'CAT']) | ||||
| def test_reusing_own_name(test_ctx, new_name): | ||||
|     db.session.add(test_ctx.tag_category_factory(name='cat', color='black')) | ||||
|     db.session.commit() | ||||
|     result = test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={'name': new_name, 'version': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'cat') | ||||
|     assert result['name'] == new_name | ||||
|     category = tag_categories.get_category_by_name('cat') | ||||
|     assert category.name == new_name | ||||
| 
 | ||||
| @pytest.mark.parametrize('dup_name', ['cat1', 'CAT1']) | ||||
| def test_trying_to_use_existing_name(test_ctx, dup_name): | ||||
|     db.session.add_all([ | ||||
|         test_ctx.tag_category_factory(name='cat1', color='black'), | ||||
|         test_ctx.tag_category_factory(name='cat2', color='black')]) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(tag_categories.TagCategoryAlreadyExistsError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': dup_name, 'version': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'cat2') | ||||
| 
 | ||||
| @pytest.mark.parametrize('input', [ | ||||
| @pytest.mark.parametrize('params', [ | ||||
|     {'name': 'whatever'}, | ||||
|     {'color': 'whatever'}, | ||||
| ]) | ||||
| def test_trying_to_update_without_privileges(test_ctx, input): | ||||
|     db.session.add(test_ctx.tag_category_factory(name='dummy')) | ||||
| def test_trying_to_update_without_privileges( | ||||
|         user_factory, tag_category_factory, context_factory, params): | ||||
|     db.session.add(tag_category_factory(name='dummy')) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={**input, **{'version': 1}}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             'dummy') | ||||
|         api.tag_category_api.update_tag_category( | ||||
|             context_factory( | ||||
|                 params={**params, **{'version': 1}}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'category_name': 'dummy'}) | ||||
| 
 | ||||
| def test_set_as_default(user_factory, tag_category_factory, context_factory): | ||||
|     category = tag_category_factory(name='name', color='black') | ||||
|     db.session.add(category) | ||||
|     db.session.commit() | ||||
|     with unittest.mock.patch('szurubooru.func.tag_categories.serialize_category'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tag_categories.set_default_category'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         tag_categories.update_category_name.side_effect = _update_category_name | ||||
|         tag_categories.serialize_category.return_value = 'serialized category' | ||||
|         result = api.tag_category_api.set_tag_category_as_default( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'name': 'changed', | ||||
|                     'color': 'white', | ||||
|                     'version': 1, | ||||
|                 }, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'category_name': 'name'}) | ||||
|         assert result == 'serialized category' | ||||
|         tag_categories.set_default_category.assert_called_once_with(category) | ||||
|  | ||||
| @ -1,187 +1,77 @@ | ||||
| import datetime | ||||
| import os | ||||
| import pytest | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import util, tags, tag_categories, cache | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import tags, tag_categories | ||||
| 
 | ||||
| def assert_relations(relations, expected_tag_names): | ||||
|     actual_names = sorted([rel.names[0].name for rel in relations]) | ||||
|     assert actual_names == sorted(expected_tag_names) | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'tags:create': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, user_factory, tag_factory): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'tag_name_regex': '^[^!]*$', | ||||
|         'tag_category_name_regex': '^[^!]*$', | ||||
|         'privileges': {'tags:create': db.User.RANK_REGULAR}, | ||||
|     }) | ||||
|     db.session.add_all([ | ||||
|         db.TagCategory(name) for name in ['meta', 'character', 'copyright']]) | ||||
|     db.session.flush() | ||||
|     cache.purge() | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_factory = tag_factory | ||||
|     ret.api = api.TagListApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_creating_simple_tags(test_ctx, fake_datetime): | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
| def test_creating_simple_tags(tag_factory, user_factory, context_factory): | ||||
|     with unittest.mock.patch('szurubooru.func.tags.create_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.get_or_create_tags_by_names'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.serialize_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         tags.get_or_create_tags_by_names.return_value = ([], []) | ||||
|         tags.create_tag.return_value = tag_factory() | ||||
|         tags.serialize_tag.return_value = 'serialized tag' | ||||
|         result = api.tag_api.create_tag( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'names': ['tag1', 'tag2'], | ||||
|                     'category': 'meta', | ||||
|                     'description': 'desc', | ||||
|                     'suggestions': [], | ||||
|                     'implications': [], | ||||
|                     'suggestions': ['sug1', 'sug2'], | ||||
|                     'implications': ['imp1', 'imp2'], | ||||
|                 }, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert len(result['snapshots']) == 1 | ||||
|     del result['snapshots'] | ||||
|     assert result == { | ||||
|         'names': ['tag1', 'tag2'], | ||||
|         'category': 'meta', | ||||
|         'description': 'desc', | ||||
|         'suggestions': [], | ||||
|         'implications': [], | ||||
|         'creationTime': datetime.datetime(1997, 12, 1), | ||||
|         'lastEditTime': None, | ||||
|         'usages': 0, | ||||
|         'version': 1, | ||||
|     } | ||||
|     tag = tags.get_tag_by_name('tag1') | ||||
|     assert [tag_name.name for tag_name in tag.names] == ['tag1', 'tag2'] | ||||
|     assert tag.category.name == 'meta' | ||||
|     assert tag.last_edit_time is None | ||||
|     assert tag.post_count == 0 | ||||
|     assert_relations(tag.suggestions, []) | ||||
|     assert_relations(tag.implications, []) | ||||
|     assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json')) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_exception', [ | ||||
|     ({'names': None}, tags.InvalidTagNameError), | ||||
|     ({'names': []}, tags.InvalidTagNameError), | ||||
|     ({'names': [None]}, tags.InvalidTagNameError), | ||||
|     ({'names': ['']}, tags.InvalidTagNameError), | ||||
|     ({'names': ['!bad']}, tags.InvalidTagNameError), | ||||
|     ({'names': ['x' * 65]}, tags.InvalidTagNameError), | ||||
|     ({'category': None}, tag_categories.TagCategoryNotFoundError), | ||||
|     ({'category': ''}, tag_categories.TagCategoryNotFoundError), | ||||
|     ({'category': '!bad'}, tag_categories.TagCategoryNotFoundError), | ||||
|     ({'suggestions': ['good', '!bad']}, tags.InvalidTagNameError), | ||||
|     ({'implications': ['good', '!bad']}, tags.InvalidTagNameError), | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception): | ||||
|     real_input={ | ||||
|         'names': ['tag1', 'tag2'], | ||||
|         'category': 'meta', | ||||
|         'suggestions': [], | ||||
|         'implications': [], | ||||
|     } | ||||
|     for key, value in input.items(): | ||||
|         real_input[key] = value | ||||
|     with pytest.raises(expected_exception): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input=real_input, | ||||
|                 user=test_ctx.user_factory())) | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == 'serialized tag' | ||||
|         tags.create_tag.assert_called_once_with( | ||||
|             ['tag1', 'tag2'], 'meta', ['sug1', 'sug2'], ['imp1', 'imp2']) | ||||
|         tags.export_to_json.assert_called_once_with() | ||||
| 
 | ||||
| @pytest.mark.parametrize('field', ['names', 'category']) | ||||
| def test_trying_to_omit_mandatory_field(test_ctx, field): | ||||
|     input = { | ||||
| def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): | ||||
|     params = { | ||||
|         'names': ['tag1', 'tag2'], | ||||
|         'category': 'meta', | ||||
|         'suggestions': [], | ||||
|         'implications': [], | ||||
|     } | ||||
|     del input[field] | ||||
|     del params[field] | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input=input, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         api.tag_api.create_tag( | ||||
|             context_factory( | ||||
|                 params=params, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| @pytest.mark.parametrize('field', ['implications', 'suggestions']) | ||||
| def test_omitting_optional_field(test_ctx, field): | ||||
|     input = { | ||||
| def test_omitting_optional_field( | ||||
|         tag_factory, user_factory, context_factory, field): | ||||
|     params = { | ||||
|         'names': ['tag1', 'tag2'], | ||||
|         'category': 'meta', | ||||
|         'suggestions': [], | ||||
|         'implications': [], | ||||
|     } | ||||
|     del input[field] | ||||
|     result = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input=input, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result is not None | ||||
|     del params[field] | ||||
|     with unittest.mock.patch('szurubooru.func.tags.create_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.serialize_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         tags.create_tag.return_value = tag_factory() | ||||
|         api.tag_api.create_tag( | ||||
|             context_factory( | ||||
|                 params=params, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_creating_new_category(test_ctx): | ||||
|     with pytest.raises(tag_categories.TagCategoryNotFoundError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
|                     'names': ['main'], | ||||
|                     'category': 'new', | ||||
|                     'suggestions': [], | ||||
|                     'implications': [], | ||||
|                 }, user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_suggestions,expected_implications', [ | ||||
|     # new relations | ||||
|     ({ | ||||
|         'names': ['main'], | ||||
|         'category': 'meta', | ||||
|         'suggestions': ['sug1', 'sug2'], | ||||
|         'implications': ['imp1', 'imp2'], | ||||
|     }, ['sug1', 'sug2'], ['imp1', 'imp2']), | ||||
|     # overlapping relations | ||||
|     ({ | ||||
|         'names': ['main'], | ||||
|         'category': 'meta', | ||||
|         'suggestions': ['sug', 'shared'], | ||||
|         'implications': ['shared', 'imp'], | ||||
|     }, ['shared', 'sug'], ['imp', 'shared']), | ||||
|     # duplicate relations | ||||
|     ({ | ||||
|         'names': ['main'], | ||||
|         'category': 'meta', | ||||
|         'suggestions': ['sug', 'SUG'], | ||||
|         'implications': ['imp', 'IMP'], | ||||
|     }, ['sug'], ['imp']), | ||||
|     # overlapping duplicate relations | ||||
|     ({ | ||||
|         'names': ['main'], | ||||
|         'category': 'meta', | ||||
|         'suggestions': ['shared1', 'shared2'], | ||||
|         'implications': ['SHARED1', 'SHARED2'], | ||||
|     }, ['shared1', 'shared2'], ['shared1', 'shared2']), | ||||
| ]) | ||||
| def test_creating_new_suggestions_and_implications( | ||||
|         test_ctx, input, expected_suggestions, expected_implications): | ||||
|     result = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input=input, user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result['suggestions'] == expected_suggestions | ||||
|     assert result['implications'] == expected_implications | ||||
|     tag = tags.get_tag_by_name('main') | ||||
|     assert_relations(tag.suggestions, expected_suggestions) | ||||
|     assert_relations(tag.implications, expected_implications) | ||||
|     for name in ['main'] + expected_suggestions + expected_implications: | ||||
|         assert tags.try_get_tag_by_name(name) is not None | ||||
| 
 | ||||
| def test_trying_to_create_tag_without_privileges(test_ctx): | ||||
| def test_trying_to_create_tag_without_privileges(user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
|         api.tag_api.create_tag( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'names': ['tag'], | ||||
|                     'category': 'meta', | ||||
|                     'suggestions': ['tag'], | ||||
|                     'implications': [], | ||||
|                 }, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|  | ||||
| @ -1,50 +1,55 @@ | ||||
| import pytest | ||||
| import os | ||||
| from datetime import datetime | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import util, tags | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import tags | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, tag_factory, user_factory): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'privileges': { | ||||
|             'tags:delete': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_factory = tag_factory | ||||
|     ret.api = api.TagDetailApi() | ||||
|     return ret | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'tags:delete': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| def test_deleting(test_ctx): | ||||
|     db.session.add(test_ctx.tag_factory(names=['tag'])) | ||||
| def test_deleting(user_factory, tag_factory, context_factory): | ||||
|     db.session.add(tag_factory(names=['tag'])) | ||||
|     db.session.commit() | ||||
|     result = test_ctx.api.delete( | ||||
|         test_ctx.context_factory( | ||||
|             input={'version': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'tag') | ||||
|     assert result == {} | ||||
|     assert db.session.query(db.Tag).count() == 0 | ||||
|     assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json')) | ||||
|     with unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         result = api.tag_api.delete_tag( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': 'tag'}) | ||||
|         assert result == {} | ||||
|         assert db.session.query(db.Tag).count() == 0 | ||||
|         tags.export_to_json.assert_called_once_with() | ||||
| 
 | ||||
| def test_trying_to_delete_non_existing(test_ctx): | ||||
| def test_deleting_used(user_factory, tag_factory, context_factory, post_factory): | ||||
|     tag = tag_factory(names=['tag']) | ||||
|     post = post_factory() | ||||
|     post.tags.append(tag) | ||||
|     db.session.add_all([tag, post]) | ||||
|     db.session.commit() | ||||
|     with unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         api.tag_api.delete_tag( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': 'tag'}) | ||||
|         db.session.refresh(post) | ||||
|         assert db.session.query(db.Tag).count() == 0 | ||||
|         assert post.tags == [] | ||||
| 
 | ||||
| def test_trying_to_delete_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(tags.TagNotFoundError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), 'bad') | ||||
|         api.tag_api.delete_tag( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': 'bad'}) | ||||
| 
 | ||||
| def test_trying_to_delete_without_privileges(test_ctx): | ||||
|     db.session.add(test_ctx.tag_factory(names=['tag'])) | ||||
| def test_trying_to_delete_without_privileges( | ||||
|         user_factory, tag_factory, context_factory): | ||||
|     db.session.add(tag_factory(names=['tag'])) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'version': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             'tag') | ||||
|         api.tag_api.delete_tag( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'tag_name': 'tag'}) | ||||
|     assert db.session.query(db.Tag).count() == 1 | ||||
|  | ||||
| @ -1,34 +1,15 @@ | ||||
| import datetime | ||||
| import os | ||||
| import pytest | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import util, tags | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import tags | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, | ||||
|         config_injector, | ||||
|         context_factory, | ||||
|         user_factory, | ||||
|         tag_factory, | ||||
|         tag_category_factory): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'privileges': { | ||||
|             'tags:merge': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_factory = tag_factory | ||||
|     ret.tag_category_factory = tag_category_factory | ||||
|     ret.api = api.TagMergeApi() | ||||
|     return ret | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'tags:merge': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| def test_merging_with_usages(test_ctx, fake_datetime, post_factory): | ||||
|     source_tag = test_ctx.tag_factory(names=['source']) | ||||
|     target_tag = test_ctx.tag_factory(names=['target']) | ||||
| def test_merging(user_factory, tag_factory, context_factory, post_factory): | ||||
|     source_tag = tag_factory(names=['source']) | ||||
|     target_tag = tag_factory(names=['target']) | ||||
|     db.session.add_all([source_tag, target_tag]) | ||||
|     db.session.flush() | ||||
|     assert source_tag.post_count == 0 | ||||
| @ -39,73 +20,78 @@ def test_merging_with_usages(test_ctx, fake_datetime, post_factory): | ||||
|     db.session.commit() | ||||
|     assert source_tag.post_count == 1 | ||||
|     assert target_tag.post_count == 0 | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
|     with unittest.mock.patch('szurubooru.func.tags.serialize_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.merge_tags'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         result = api.tag_api.merge_tags( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'removeVersion': 1, | ||||
|                     'mergeToVersion': 1, | ||||
|                     'remove': 'source', | ||||
|                     'mergeTo': 'target', | ||||
|                 }, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert tags.try_get_tag_by_name('source') is None | ||||
|     assert tags.get_tag_by_name('target').post_count == 1 | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         tags.merge_tags.called_once_with(source_tag, target_tag) | ||||
|         tags.export_to_json.assert_called_once_with() | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     'field', ['remove', 'mergeTo', 'removeVersion', 'mergeToVersion']) | ||||
| def test_trying_to_omit_mandatory_field(test_ctx, field): | ||||
| def test_trying_to_omit_mandatory_field( | ||||
|         user_factory, tag_factory, context_factory, field): | ||||
|     db.session.add_all([ | ||||
|         test_ctx.tag_factory(names=['source']), | ||||
|         test_ctx.tag_factory(names=['target']), | ||||
|         tag_factory(names=['source']), | ||||
|         tag_factory(names=['target']), | ||||
|     ]) | ||||
|     db.session.commit() | ||||
|     input = { | ||||
|     params = { | ||||
|         'removeVersion': 1, | ||||
|         'mergeToVersion': 1, | ||||
|         'remove': 'source', | ||||
|         'mergeTo': 'target', | ||||
|     } | ||||
|     del input[field] | ||||
|     del params[field] | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input=input, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         api.tag_api.merge_tags( | ||||
|             context_factory( | ||||
|                 params=params, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| def test_trying_to_merge_non_existing(test_ctx): | ||||
|     db.session.add(test_ctx.tag_factory(names=['good'])) | ||||
| def test_trying_to_merge_non_existing( | ||||
|         user_factory, tag_factory, context_factory): | ||||
|     db.session.add(tag_factory(names=['good'])) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(tags.TagNotFoundError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'remove': 'good', 'mergeTo': 'bad'}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         api.tag_api.merge_tags( | ||||
|             context_factory( | ||||
|                 params={'remove': 'good', 'mergeTo': 'bad'}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     with pytest.raises(tags.TagNotFoundError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'remove': 'bad', 'mergeTo': 'good'}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         api.tag_api.merge_tags( | ||||
|             context_factory( | ||||
|                 params={'remove': 'bad', 'mergeTo': 'good'}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input', [ | ||||
| @pytest.mark.parametrize('params', [ | ||||
|     {'names': 'whatever'}, | ||||
|     {'category': 'whatever'}, | ||||
|     {'suggestions': ['whatever']}, | ||||
|     {'implications': ['whatever']}, | ||||
| ]) | ||||
| def test_trying_to_merge_without_privileges(test_ctx, input): | ||||
| def test_trying_to_merge_without_privileges( | ||||
|         user_factory, tag_factory, context_factory, params): | ||||
|     db.session.add_all([ | ||||
|         test_ctx.tag_factory(names=['source']), | ||||
|         test_ctx.tag_factory(names=['target']), | ||||
|         tag_factory(names=['source']), | ||||
|         tag_factory(names=['target']), | ||||
|     ]) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
|         api.tag_api.merge_tags( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'removeVersion': 1, | ||||
|                     'mergeToVersion': 1, | ||||
|                     'remove': 'source', | ||||
|                     'mergeTo': 'target', | ||||
|                 }, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|  | ||||
| @ -1,82 +1,64 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, tags | ||||
| from szurubooru.func import tags | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         context_factory, | ||||
|         config_injector, | ||||
|         user_factory, | ||||
|         tag_factory, | ||||
|         tag_category_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'tags:list': db.User.RANK_REGULAR, | ||||
|             'tags:view': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_factory = tag_factory | ||||
|     ret.tag_category_factory = tag_category_factory | ||||
|     ret.list_api = api.TagListApi() | ||||
|     ret.detail_api = api.TagDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_retrieving_multiple(test_ctx): | ||||
|     tag1 = test_ctx.tag_factory(names=['t1']) | ||||
|     tag2 = test_ctx.tag_factory(names=['t2']) | ||||
| def test_retrieving_multiple(user_factory, tag_factory, context_factory): | ||||
|     tag1 = tag_factory(names=['t1']) | ||||
|     tag2 = tag_factory(names=['t2']) | ||||
|     db.session.add_all([tag1, tag2]) | ||||
|     result = test_ctx.list_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             input={'query': '', 'page': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result['query'] == '' | ||||
|     assert result['page'] == 1 | ||||
|     assert result['pageSize'] == 100 | ||||
|     assert result['total'] == 2 | ||||
|     assert [t['names'] for t in result['results']] == [['t1'], ['t2']] | ||||
|     with unittest.mock.patch('szurubooru.func.tags.serialize_tag'): | ||||
|         tags.serialize_tag.return_value = 'serialized tag' | ||||
|         result = api.tag_api.get_tags( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == { | ||||
|             'query': '', | ||||
|             'page': 1, | ||||
|             'pageSize': 100, | ||||
|             'total': 2, | ||||
|             'results': ['serialized tag', 'serialized tag'], | ||||
|         } | ||||
| 
 | ||||
| def test_trying_to_retrieve_multiple_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_multiple_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.list_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'query': '', 'page': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.tag_api.get_tags( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
| 
 | ||||
| def test_retrieving_single(test_ctx): | ||||
|     category = test_ctx.tag_category_factory(name='meta') | ||||
|     db.session.add(test_ctx.tag_factory(names=['tag'], category=category)) | ||||
|     result = test_ctx.detail_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'tag') | ||||
|     assert result == { | ||||
|         'names': ['tag'], | ||||
|         'category': 'meta', | ||||
|         'description': None, | ||||
|         'creationTime': datetime.datetime(1996, 1, 1), | ||||
|         'lastEditTime': None, | ||||
|         'suggestions': [], | ||||
|         'implications': [], | ||||
|         'usages': 0, | ||||
|         'snapshots': [], | ||||
|         'version': 1, | ||||
|     } | ||||
| def test_retrieving_single(user_factory, tag_factory, context_factory): | ||||
|     db.session.add(tag_factory(names=['tag'])) | ||||
|     with unittest.mock.patch('szurubooru.func.tags.serialize_tag'): | ||||
|         tags.serialize_tag.return_value = 'serialized tag' | ||||
|         result = api.tag_api.get_tag( | ||||
|             context_factory( | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': 'tag'}) | ||||
|         assert result == 'serialized tag' | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_non_existing(test_ctx): | ||||
| def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(tags.TagNotFoundError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             '-') | ||||
|         api.tag_api.get_tag( | ||||
|             context_factory( | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': '-'}) | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_single_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             '-') | ||||
|         api.tag_api.get_tag( | ||||
|             context_factory( | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'tag_name': '-'}) | ||||
|  | ||||
| @ -1,56 +1,47 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, tags | ||||
| from szurubooru.func import tags | ||||
| 
 | ||||
| def assert_results(result, expected_tag_names_and_occurrences): | ||||
|     actual_tag_names_and_occurences = [] | ||||
|     for item in result['results']: | ||||
|         tag_name = item['tag']['names'][0] | ||||
|         occurrences = item['occurrences'] | ||||
|         actual_tag_names_and_occurences.append((tag_name, occurrences)) | ||||
|     assert actual_tag_names_and_occurences == expected_tag_names_and_occurrences | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'tags:view': db.User.RANK_REGULAR}}) | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         context_factory, config_injector, user_factory, tag_factory, post_factory): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'tags:view': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_factory = tag_factory | ||||
|     ret.post_factory = post_factory | ||||
|     ret.api = api.TagSiblingsApi() | ||||
|     return ret | ||||
| def test_get_tag_siblings(user_factory, tag_factory, post_factory, context_factory): | ||||
|     db.session.add(tag_factory(names=['tag'])) | ||||
|     with unittest.mock.patch('szurubooru.func.tags.serialize_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.get_tag_siblings'): | ||||
|         tags.serialize_tag.side_effect = \ | ||||
|             lambda tag, *args, **kwargs: \ | ||||
|                 'serialized tag %s' % tag.names[0].name | ||||
|         tags.get_tag_siblings.return_value = [ | ||||
|             (tag_factory(names=['sib1']), 1), | ||||
|             (tag_factory(names=['sib2']), 3), | ||||
|         ] | ||||
|         result = api.tag_api.get_tag_siblings( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': 'tag'}) | ||||
|         assert result == { | ||||
|             'results': [ | ||||
|                 { | ||||
|                     'tag': 'serialized tag sib1', | ||||
|                     'occurrences': 1, | ||||
|                 }, | ||||
|                 { | ||||
|                     'tag': 'serialized tag sib2', | ||||
|                     'occurrences': 3, | ||||
|                 }, | ||||
|             ], | ||||
|         } | ||||
| 
 | ||||
| def test_used_with_others(test_ctx): | ||||
|     tag1 = test_ctx.tag_factory(names=['tag1']) | ||||
|     tag2 = test_ctx.tag_factory(names=['tag2']) | ||||
|     post = test_ctx.post_factory() | ||||
|     post.tags = [tag1, tag2] | ||||
|     db.session.add_all([post, tag1, tag2]) | ||||
|     result = test_ctx.api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), 'tag1') | ||||
|     assert_results(result, [('tag2', 1)]) | ||||
|     result = test_ctx.api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), 'tag2') | ||||
|     assert_results(result, [('tag1', 1)]) | ||||
| 
 | ||||
| def test_trying_to_retrieve_non_existing(test_ctx): | ||||
| def test_trying_to_retrieve_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(tags.TagNotFoundError): | ||||
|         test_ctx.api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), '-') | ||||
|         api.tag_api.get_tag_siblings( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': '-'}) | ||||
| 
 | ||||
| def test_trying_to_retrieve_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_without_privileges(user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), '-') | ||||
|         api.tag_api.get_tag_siblings( | ||||
|             context_factory(user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'tag_name': '-'}) | ||||
|  | ||||
| @ -1,20 +1,11 @@ | ||||
| import datetime | ||||
| import os | ||||
| import pytest | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import util, tags, tag_categories, cache | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import tags | ||||
| 
 | ||||
| def assert_relations(relations, expected_tag_names): | ||||
|     actual_names = sorted([rel.names[0].name for rel in relations]) | ||||
|     assert actual_names == sorted(expected_tag_names) | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx( | ||||
|         tmpdir, config_injector, context_factory, user_factory, tag_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'data_dir': str(tmpdir), | ||||
|         'tag_name_regex': '^[^!]*$', | ||||
|         'tag_category_name_regex': '^[^!]*$', | ||||
|         'privileges': { | ||||
|             'tags:create': db.User.RANK_REGULAR, | ||||
|             'tags:edit:names': db.User.RANK_REGULAR, | ||||
| @ -24,118 +15,115 @@ def test_ctx( | ||||
|             'tags:edit:implications': db.User.RANK_REGULAR, | ||||
|         }, | ||||
|     }) | ||||
|     db.session.add_all([ | ||||
|         db.TagCategory(name) for name in ['meta', 'character', 'copyright']]) | ||||
|     db.session.commit() | ||||
|     cache.purge() | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.tag_factory = tag_factory | ||||
|     ret.api = api.TagDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_simple_updating(test_ctx, fake_datetime): | ||||
|     tag = test_ctx.tag_factory(names=['tag1', 'tag2']) | ||||
| def test_simple_updating(user_factory, tag_factory, context_factory, fake_datetime): | ||||
|     auth_user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     tag = tag_factory(names=['tag1', 'tag2']) | ||||
|     db.session.add(tag) | ||||
|     db.session.commit() | ||||
|     with fake_datetime('1997-12-01'): | ||||
|         result = test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
|     with unittest.mock.patch('szurubooru.func.tags.create_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.get_or_create_tags_by_names'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.update_tag_names'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.update_tag_category_name'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.update_tag_description'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.update_tag_suggestions'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.update_tag_implications'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.serialize_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         tags.get_or_create_tags_by_names.return_value = ([], []) | ||||
|         tags.serialize_tag.return_value = 'serialized tag' | ||||
|         result = api.tag_api.update_tag( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'version': 1, | ||||
|                     'names': ['tag3'], | ||||
|                     'category': 'character', | ||||
|                     'description': 'desc', | ||||
|                     'suggestions': ['sug1', 'sug2'], | ||||
|                     'implications': ['imp1', 'imp2'], | ||||
|                 }, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'tag1') | ||||
|     assert len(result['snapshots']) == 1 | ||||
|     del result['snapshots'] | ||||
|     assert result == { | ||||
|         'names': ['tag3'], | ||||
|         'category': 'character', | ||||
|         'description': 'desc', | ||||
|         'suggestions': [], | ||||
|         'implications': [], | ||||
|         'creationTime': datetime.datetime(1996, 1, 1), | ||||
|         'lastEditTime': datetime.datetime(1997, 12, 1), | ||||
|         'usages': 0, | ||||
|         'version': 2, | ||||
|     } | ||||
|     assert tags.try_get_tag_by_name('tag1') is None | ||||
|     assert tags.try_get_tag_by_name('tag2') is None | ||||
|     tag = tags.get_tag_by_name('tag3') | ||||
|     assert tag is not None | ||||
|     assert [tag_name.name for tag_name in tag.names] == ['tag3'] | ||||
|     assert tag.category.name == 'character' | ||||
|     assert tag.suggestions == [] | ||||
|     assert tag.implications == [] | ||||
|     assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json')) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_exception', [ | ||||
|     ({'names': None}, tags.InvalidTagNameError), | ||||
|     ({'names': []}, tags.InvalidTagNameError), | ||||
|     ({'names': [None]}, tags.InvalidTagNameError), | ||||
|     ({'names': ['']}, tags.InvalidTagNameError), | ||||
|     ({'names': ['!bad']}, tags.InvalidTagNameError), | ||||
|     ({'names': ['x' * 65]}, tags.InvalidTagNameError), | ||||
|     ({'category': None}, tag_categories.TagCategoryNotFoundError), | ||||
|     ({'category': ''}, tag_categories.TagCategoryNotFoundError), | ||||
|     ({'category': '!bad'}, tag_categories.TagCategoryNotFoundError), | ||||
|     ({'suggestions': ['good', '!bad']}, tags.InvalidTagNameError), | ||||
|     ({'implications': ['good', '!bad']}, tags.InvalidTagNameError), | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception): | ||||
|     db.session.add(test_ctx.tag_factory(names=['tag1'])) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(expected_exception): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={**input, **{'version': 1}}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'tag1') | ||||
|                 user=auth_user), | ||||
|             {'tag_name': 'tag1'}) | ||||
|         assert result == 'serialized tag' | ||||
|         tags.create_tag.assert_not_called() | ||||
|         tags.update_tag_names.assert_called_once_with(tag, ['tag3']) | ||||
|         tags.update_tag_category_name.assert_called_once_with(tag, 'character') | ||||
|         tags.update_tag_description.assert_called_once_with(tag, 'desc') | ||||
|         tags.update_tag_suggestions.assert_called_once_with(tag, ['sug1', 'sug2']) | ||||
|         tags.update_tag_implications.assert_called_once_with(tag, ['imp1', 'imp2']) | ||||
|         tags.serialize_tag.assert_called_once_with(tag, options=None) | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     'field', ['names', 'category', 'description', 'implications', 'suggestions']) | ||||
| def test_omitting_optional_field(test_ctx, field): | ||||
|     db.session.add(test_ctx.tag_factory(names=['tag'])) | ||||
| def test_omitting_optional_field( | ||||
|         user_factory, tag_factory, context_factory, field): | ||||
|     db.session.add(tag_factory(names=['tag'])) | ||||
|     db.session.commit() | ||||
|     input = { | ||||
|     params = { | ||||
|         'names': ['tag1', 'tag2'], | ||||
|         'category': 'meta', | ||||
|         'description': 'desc', | ||||
|         'suggestions': [], | ||||
|         'implications': [], | ||||
|     } | ||||
|     del input[field] | ||||
|     result = test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={**input, **{'version': 1}}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'tag') | ||||
|     assert result is not None | ||||
|     del params[field] | ||||
|     with unittest.mock.patch('szurubooru.func.tags.create_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.update_tag_names'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.update_tag_category_name'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.serialize_tag'), \ | ||||
|             unittest.mock.patch('szurubooru.func.tags.export_to_json'): | ||||
|         api.tag_api.update_tag( | ||||
|             context_factory( | ||||
|                 params={**params, **{'version': 1}}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': 'tag'}) | ||||
| 
 | ||||
| def test_trying_to_update_non_existing(test_ctx): | ||||
| def test_trying_to_update_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(tags.TagNotFoundError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'names': ['dummy']}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'tag1') | ||||
|         api.tag_api.update_tag( | ||||
|             context_factory( | ||||
|                 params={'names': ['dummy']}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'tag_name': 'tag1'}) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input', [ | ||||
| @pytest.mark.parametrize('params', [ | ||||
|     {'names': 'whatever'}, | ||||
|     {'category': 'whatever'}, | ||||
|     {'suggestions': ['whatever']}, | ||||
|     {'implications': ['whatever']}, | ||||
| ]) | ||||
| def test_trying_to_update_without_privileges(test_ctx, input): | ||||
|     db.session.add(test_ctx.tag_factory(names=['tag'])) | ||||
| def test_trying_to_update_without_privileges( | ||||
|         user_factory, tag_factory, context_factory, params): | ||||
|     db.session.add(tag_factory(names=['tag'])) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={**input, **{'version': 1}}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             'tag') | ||||
|         api.tag_api.update_tag( | ||||
|             context_factory( | ||||
|                 params={**params, **{'version': 1}}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             {'tag_name': 'tag'}) | ||||
| 
 | ||||
| def test_trying_to_create_tags_without_privileges( | ||||
|         config_injector, context_factory, tag_factory, user_factory): | ||||
|     tag = tag_factory(names=['tag']) | ||||
|     db.session.add(tag) | ||||
|     db.session.commit() | ||||
|     config_injector({'privileges': { | ||||
|         'tags:create': db.User.RANK_ADMINISTRATOR, | ||||
|         'tags:edit:suggestions': db.User.RANK_REGULAR, | ||||
|         'tags:edit:implications': db.User.RANK_REGULAR, | ||||
|     }}) | ||||
|     with unittest.mock.patch('szurubooru.func.tags.get_or_create_tags_by_names'): | ||||
|         tags.get_or_create_tags_by_names.return_value = ([], ['new-tag']) | ||||
|         with pytest.raises(errors.AuthError): | ||||
|             api.tag_api.update_tag( | ||||
|                 context_factory( | ||||
|                     params={'suggestions': ['tag1', 'tag2'], 'version': 1}, | ||||
|                     user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|                 {'tag_name': 'tag'}) | ||||
|         with pytest.raises(errors.AuthError): | ||||
|             api.tag_api.update_tag( | ||||
|                 context_factory( | ||||
|                     params={'implications': ['tag1', 'tag2'], 'version': 1}, | ||||
|                     user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|                 {'tag_name': 'tag'}) | ||||
|  | ||||
| @ -1,230 +1,79 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import auth, util, users | ||||
| import unittest.mock | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import users | ||||
| 
 | ||||
| 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' | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({'privileges': {'users:create': 'regular'}}) | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx(tmpdir, config_injector, context_factory, user_factory): | ||||
|     config_injector({ | ||||
|         'secret': '', | ||||
|         'user_name_regex': '[^!]{3,}', | ||||
|         'password_regex': '[^!]{3,}', | ||||
|         'default_rank': db.User.RANK_REGULAR, | ||||
|         'thumbnails': {'avatar_width': 200, 'avatar_height': 200}, | ||||
|         'privileges': {'users:create': 'anonymous'}, | ||||
|         'data_dir': str(tmpdir.mkdir('data')), | ||||
|         'data_url': 'http://example.com/data/', | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.api = api.UserListApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_creating_user(test_ctx, fake_datetime): | ||||
|     with fake_datetime('1969-02-12'): | ||||
|         result = test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
| def test_creating_user(user_factory, context_factory, fake_datetime): | ||||
|     user = user_factory() | ||||
|     with unittest.mock.patch('szurubooru.func.users.create_user'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_name'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_password'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_email'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_rank'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_avatar'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.serialize_user'), \ | ||||
|             fake_datetime('1969-02-12'): | ||||
|         users.serialize_user.return_value = 'serialized user' | ||||
|         users.create_user.return_value = user | ||||
|         result = api.user_api.create_user( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'name': 'chewie1', | ||||
|                     'email': 'asd@asd.asd', | ||||
|                     'password': 'oks', | ||||
|                     'rank': 'moderator', | ||||
|                     'avatarStyle': 'manual', | ||||
|                 }, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result == { | ||||
|         'avatarStyle': 'gravatar', | ||||
|         'avatarUrl': 'https://gravatar.com/avatar/' + | ||||
|             '6f370c8c7109534c3d5c394123a477d7?d=retro&s=200', | ||||
|         'creationTime': datetime.datetime(1969, 2, 12), | ||||
|         'lastLoginTime': None, | ||||
|         'name': 'chewie1', | ||||
|         'rank': 'administrator', | ||||
|         'email': 'asd@asd.asd', | ||||
|         'commentCount': 0, | ||||
|         'likedPostCount': 0, | ||||
|         'dislikedPostCount': 0, | ||||
|         'favoritePostCount': 0, | ||||
|         'uploadedPostCount': 0, | ||||
|         'version': 1, | ||||
|     } | ||||
|     user = users.get_user_by_name('chewie1') | ||||
|     assert user.name == 'chewie1' | ||||
|     assert user.email == 'asd@asd.asd' | ||||
|     assert user.rank == db.User.RANK_ADMINISTRATOR | ||||
|     assert auth.is_valid_password(user, 'oks') is True | ||||
|     assert auth.is_valid_password(user, 'invalid') is False | ||||
| 
 | ||||
| def test_first_user_becomes_admin_others_not(test_ctx): | ||||
|     result1 = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={ | ||||
|                 'name': 'chewie1', | ||||
|                 'email': 'asd@asd.asd', | ||||
|                 'password': 'oks', | ||||
|             }, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|     result2 = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={ | ||||
|                 'name': 'chewie2', | ||||
|                 'email': 'asd@asd.asd', | ||||
|                 'password': 'sok', | ||||
|             }, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|     assert result1['rank'] == 'administrator' | ||||
|     assert result2['rank'] == 'regular' | ||||
|     first_user = users.get_user_by_name('chewie1') | ||||
|     other_user = users.get_user_by_name('chewie2') | ||||
|     assert first_user.rank == db.User.RANK_ADMINISTRATOR | ||||
|     assert other_user.rank == db.User.RANK_REGULAR | ||||
| 
 | ||||
| def test_first_user_does_not_become_admin_if_they_dont_wish_so(test_ctx): | ||||
|     result = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={ | ||||
|                 'name': 'chewie1', | ||||
|                 'email': 'asd@asd.asd', | ||||
|                 'password': 'oks', | ||||
|                 'rank': 'regular', | ||||
|             }, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|     assert result['rank'] == 'regular' | ||||
| 
 | ||||
| def test_trying_to_become_someone_else(test_ctx): | ||||
|     test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={ | ||||
|                 'name': 'chewie', | ||||
|                 'email': 'asd@asd.asd', | ||||
|                 'password': 'oks', | ||||
|             }, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     with pytest.raises(users.UserAlreadyExistsError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
|                     'name': 'chewie', | ||||
|                     'email': 'asd@asd.asd', | ||||
|                     'password': 'oks', | ||||
|                 }, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     with pytest.raises(users.UserAlreadyExistsError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input={ | ||||
|                     'name': 'CHEWIE', | ||||
|                     'email': 'asd@asd.asd', | ||||
|                     'password': 'oks', | ||||
|                 }, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_exception', [ | ||||
|     ({'name': None}, users.InvalidUserNameError), | ||||
|     ({'name': ''}, users.InvalidUserNameError), | ||||
|     ({'name': '!bad'}, users.InvalidUserNameError), | ||||
|     ({'name': 'x' * 51}, users.InvalidUserNameError), | ||||
|     ({'password': None}, users.InvalidPasswordError), | ||||
|     ({'password': ''}, users.InvalidPasswordError), | ||||
|     ({'password': '!bad'}, users.InvalidPasswordError), | ||||
|     ({'rank': None}, users.InvalidRankError), | ||||
|     ({'rank': ''}, users.InvalidRankError), | ||||
|     ({'rank': 'bad'}, users.InvalidRankError), | ||||
|     ({'rank': 'anonymous'}, users.InvalidRankError), | ||||
|     ({'rank': 'nobody'}, users.InvalidRankError), | ||||
|     ({'email': 'bad'}, users.InvalidEmailError), | ||||
|     ({'email': 'x@' * 65 + '.com'}, users.InvalidEmailError), | ||||
|     ({'avatarStyle': None}, users.InvalidAvatarError), | ||||
|     ({'avatarStyle': ''}, users.InvalidAvatarError), | ||||
|     ({'avatarStyle': 'invalid'}, users.InvalidAvatarError), | ||||
|     ({'avatarStyle': 'manual'}, users.InvalidAvatarError), # missing file | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception): | ||||
|     real_input={ | ||||
|         'name': 'chewie', | ||||
|         'email': 'asd@asd.asd', | ||||
|         'password': 'oks', | ||||
|     } | ||||
|     for key, value in input.items(): | ||||
|         real_input[key] = value | ||||
|     with pytest.raises(expected_exception): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input=real_input, | ||||
|                 user=test_ctx.user_factory( | ||||
|                     name='u1', rank=db.User.RANK_ADMINISTRATOR))) | ||||
|                 files={'avatar': b'...'}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == 'serialized user' | ||||
|         users.create_user.assert_called_once_with('chewie1', 'oks', 'asd@asd.asd') | ||||
|         assert not users.update_user_name.called | ||||
|         assert not users.update_user_password.called | ||||
|         assert not users.update_user_email.called | ||||
|         users.update_user_rank.called_once_with(user, 'moderator') | ||||
|         users.update_user_avatar.called_once_with(user, 'manual', b'...') | ||||
| 
 | ||||
| @pytest.mark.parametrize('field', ['name', 'password']) | ||||
| def test_trying_to_omit_mandatory_field(test_ctx, field): | ||||
|     input = { | ||||
| def test_trying_to_omit_mandatory_field(user_factory, context_factory, field): | ||||
|     params = { | ||||
|         'name': 'chewie', | ||||
|         'email': 'asd@asd.asd', | ||||
|         'password': 'oks', | ||||
|     } | ||||
|     del input[field] | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test_ctx.api.post( | ||||
|             test_ctx.context_factory( | ||||
|                 input=input, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     user = user_factory() | ||||
|     auth_user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     del params[field] | ||||
|     with unittest.mock.patch('szurubooru.func.users.create_user'), \ | ||||
|             pytest.raises(errors.MissingRequiredParameterError): | ||||
|         users.create_user.return_value = user | ||||
|         api.user_api.create_user(context_factory(params=params, user=auth_user)) | ||||
| 
 | ||||
| @pytest.mark.parametrize('field', ['rank', 'email', 'avatarStyle']) | ||||
| def test_omitting_optional_field(test_ctx, field): | ||||
|     input = { | ||||
| def test_omitting_optional_field(user_factory, context_factory, field): | ||||
|     params = { | ||||
|         'name': 'chewie', | ||||
|         'email': 'asd@asd.asd', | ||||
|         'password': 'oks', | ||||
|         'rank': 'moderator', | ||||
|         'avatarStyle': 'manual', | ||||
|         'avatarStyle': 'gravatar', | ||||
|     } | ||||
|     del input[field] | ||||
|     result = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input=input, | ||||
|             files={'avatar': EMPTY_PIXEL}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_MODERATOR))) | ||||
|     assert result is not None | ||||
|     del params[field] | ||||
|     user = user_factory() | ||||
|     auth_user = user_factory(rank=db.User.RANK_MODERATOR) | ||||
|     with unittest.mock.patch('szurubooru.func.users.create_user'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_avatar'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.serialize_user'): | ||||
|         users.create_user.return_value = user | ||||
|         api.user_api.create_user( | ||||
|             context_factory(params=params, user=auth_user)) | ||||
| 
 | ||||
| def test_mods_trying_to_become_admin(test_ctx): | ||||
|     user1 = test_ctx.user_factory(name='u1', rank=db.User.RANK_MODERATOR) | ||||
|     user2 = test_ctx.user_factory(name='u2', rank=db.User.RANK_MODERATOR) | ||||
|     db.session.add_all([user1, user2]) | ||||
|     context = test_ctx.context_factory(input={ | ||||
|             'name': 'chewie', | ||||
|             'email': 'asd@asd.asd', | ||||
|             'password': 'oks', | ||||
|             'rank': 'administrator', | ||||
|         }, user=user1) | ||||
| def test_trying_to_create_user_without_privileges(context_factory, user_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.post(context) | ||||
| 
 | ||||
| def test_admin_creating_mod_account(test_ctx): | ||||
|     user = test_ctx.user_factory(rank=db.User.RANK_ADMINISTRATOR) | ||||
|     db.session.add(user) | ||||
|     context = test_ctx.context_factory(input={ | ||||
|             'name': 'chewie', | ||||
|             'email': 'asd@asd.asd', | ||||
|             'password': 'oks', | ||||
|             'rank': 'moderator', | ||||
|         }, user=user) | ||||
|     result = test_ctx.api.post(context) | ||||
|     assert result['rank'] == 'moderator' | ||||
| 
 | ||||
| def test_uploading_avatar(test_ctx): | ||||
|     response = test_ctx.api.post( | ||||
|         test_ctx.context_factory( | ||||
|             input={ | ||||
|                 'name': 'chewie', | ||||
|                 'email': 'asd@asd.asd', | ||||
|                 'password': 'oks', | ||||
|                 'avatarStyle': 'manual', | ||||
|             }, | ||||
|             files={'avatar': EMPTY_PIXEL}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_MODERATOR))) | ||||
|     user = users.get_user_by_name('chewie') | ||||
|     assert user.avatar_style == user.AVATAR_MANUAL | ||||
|     assert response['avatarUrl'] == 'http://example.com/data/avatars/chewie.png' | ||||
|         api.user_api.create_user(context_factory( | ||||
|             params='whatever', | ||||
|             user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|  | ||||
| @ -1,54 +1,52 @@ | ||||
| import pytest | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, users | ||||
| from szurubooru.func import users | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx(config_injector, context_factory, user_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'users:delete:self': db.User.RANK_REGULAR, | ||||
|             'users:delete:any': db.User.RANK_MODERATOR, | ||||
|         }, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.api = api.UserDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_deleting_oneself(test_ctx): | ||||
|     user = test_ctx.user_factory(name='u', rank=db.User.RANK_REGULAR) | ||||
| def test_deleting_oneself(user_factory, context_factory): | ||||
|     user = user_factory(name='u', rank=db.User.RANK_REGULAR) | ||||
|     db.session.add(user) | ||||
|     db.session.commit() | ||||
|     result = test_ctx.api.delete( | ||||
|         test_ctx.context_factory(input={'version': 1}, user=user), 'u') | ||||
|     result = api.user_api.delete_user( | ||||
|         context_factory( | ||||
|             params={'version': 1}, user=user), {'user_name': 'u'}) | ||||
|     assert result == {} | ||||
|     assert db.session.query(db.User).count() == 0 | ||||
| 
 | ||||
| def test_deleting_someone_else(test_ctx): | ||||
|     user1 = test_ctx.user_factory(name='u1', rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(name='u2', rank=db.User.RANK_MODERATOR) | ||||
| def test_deleting_someone_else(user_factory, context_factory): | ||||
|     user1 = user_factory(name='u1', rank=db.User.RANK_REGULAR) | ||||
|     user2 = user_factory(name='u2', rank=db.User.RANK_MODERATOR) | ||||
|     db.session.add_all([user1, user2]) | ||||
|     db.session.commit() | ||||
|     test_ctx.api.delete( | ||||
|         test_ctx.context_factory(input={'version': 1}, user=user2), 'u1') | ||||
|     api.user_api.delete_user( | ||||
|         context_factory( | ||||
|             params={'version': 1}, user=user2), {'user_name': 'u1'}) | ||||
|     assert db.session.query(db.User).count() == 1 | ||||
| 
 | ||||
| def test_trying_to_delete_someone_else_without_privileges(test_ctx): | ||||
|     user1 = test_ctx.user_factory(name='u1', rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(name='u2', rank=db.User.RANK_REGULAR) | ||||
| def test_trying_to_delete_someone_else_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     user1 = user_factory(name='u1', rank=db.User.RANK_REGULAR) | ||||
|     user2 = user_factory(name='u2', rank=db.User.RANK_REGULAR) | ||||
|     db.session.add_all([user1, user2]) | ||||
|     db.session.commit() | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory(input={'version': 1}, user=user2), 'u1') | ||||
|         api.user_api.delete_user( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, user=user2), {'user_name': 'u1'}) | ||||
|     assert db.session.query(db.User).count() == 2 | ||||
| 
 | ||||
| def test_trying_to_delete_non_existing(test_ctx): | ||||
| def test_trying_to_delete_non_existing(user_factory, context_factory): | ||||
|     with pytest.raises(users.UserNotFoundError): | ||||
|         test_ctx.api.delete( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'version': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             'bad') | ||||
|         api.user_api.delete_user( | ||||
|             context_factory( | ||||
|                 params={'version': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             {'user_name': 'bad'}) | ||||
|  | ||||
| @ -1,83 +1,64 @@ | ||||
| import datetime | ||||
| import unittest.mock | ||||
| import pytest | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import util, users | ||||
| from szurubooru.func import users | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx(context_factory, config_injector, user_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'privileges': { | ||||
|             'users:list': db.User.RANK_REGULAR, | ||||
|             'users:view': db.User.RANK_REGULAR, | ||||
|             'users:edit:any:email': db.User.RANK_MODERATOR, | ||||
|         }, | ||||
|         'thumbnails': {'avatar_width': 200}, | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.list_api = api.UserListApi() | ||||
|     ret.detail_api = api.UserDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_retrieving_multiple(test_ctx): | ||||
|     user1 = test_ctx.user_factory(name='u1', rank=db.User.RANK_MODERATOR) | ||||
|     user2 = test_ctx.user_factory(name='u2', rank=db.User.RANK_MODERATOR) | ||||
| def test_retrieving_multiple(user_factory, context_factory): | ||||
|     user1 = user_factory(name='u1', rank=db.User.RANK_MODERATOR) | ||||
|     user2 = user_factory(name='u2', rank=db.User.RANK_MODERATOR) | ||||
|     db.session.add_all([user1, user2]) | ||||
|     result = test_ctx.list_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             input={'query': '', 'page': 1}, | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR))) | ||||
|     assert result['query'] == '' | ||||
|     assert result['page'] == 1 | ||||
|     assert result['pageSize'] == 100 | ||||
|     assert result['total'] == 2 | ||||
|     assert [u['name'] for u in result['results']] == ['u1', 'u2'] | ||||
|     with unittest.mock.patch('szurubooru.func.users.serialize_user'): | ||||
|         users.serialize_user.return_value = 'serialized user' | ||||
|         result = api.user_api.get_users( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_REGULAR))) | ||||
|         assert result == { | ||||
|             'query': '', | ||||
|             'page': 1, | ||||
|             'pageSize': 100, | ||||
|             'total': 2, | ||||
|             'results': ['serialized user', 'serialized user'], | ||||
|         } | ||||
| 
 | ||||
| def test_trying_to_retrieve_multiple_without_privileges(test_ctx): | ||||
| def test_trying_to_retrieve_multiple_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.list_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'query': '', 'page': 1}, | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
|         api.user_api.get_users( | ||||
|             context_factory( | ||||
|                 params={'query': '', 'page': 1}, | ||||
|                 user=user_factory(rank=db.User.RANK_ANONYMOUS))) | ||||
| 
 | ||||
| def test_retrieving_single(test_ctx): | ||||
|     db.session.add(test_ctx.user_factory(name='u1', rank=db.User.RANK_REGULAR)) | ||||
|     result = test_ctx.detail_api.get( | ||||
|         test_ctx.context_factory( | ||||
|             user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|         'u1') | ||||
|     assert result == { | ||||
|         'name': 'u1', | ||||
|         'rank': db.User.RANK_REGULAR, | ||||
|         'creationTime': datetime.datetime(1997, 1, 1), | ||||
|         'lastLoginTime': None, | ||||
|         'avatarStyle': 'gravatar', | ||||
|         'avatarUrl': 'https://gravatar.com/avatar/' + | ||||
|             '275876e34cf609db118f3d84b799a790?d=retro&s=200', | ||||
|         'email': False, | ||||
|         'commentCount': 0, | ||||
|         'likedPostCount': False, | ||||
|         'dislikedPostCount': False, | ||||
|         'favoritePostCount': 0, | ||||
|         'uploadedPostCount': 0, | ||||
|         'version': 1, | ||||
|     } | ||||
|     assert result['email'] is False | ||||
|     assert result['likedPostCount'] is False | ||||
|     assert result['dislikedPostCount'] is False | ||||
| def test_retrieving_single(user_factory, context_factory): | ||||
|     user = user_factory(name='u1', rank=db.User.RANK_REGULAR) | ||||
|     auth_user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     db.session.add(user) | ||||
|     with unittest.mock.patch('szurubooru.func.users.serialize_user'): | ||||
|         users.serialize_user.return_value = 'serialized user' | ||||
|         result = api.user_api.get_user( | ||||
|             context_factory(user=auth_user), {'user_name': 'u1'}) | ||||
|         assert result == 'serialized user' | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_non_existing(test_ctx): | ||||
| def test_trying_to_retrieve_single_non_existing(user_factory, context_factory): | ||||
|     auth_user = user_factory(rank=db.User.RANK_REGULAR) | ||||
|     with pytest.raises(users.UserNotFoundError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), | ||||
|             '-') | ||||
|         api.user_api.get_user( | ||||
|             context_factory(user=auth_user), {'user_name': '-'}) | ||||
| 
 | ||||
| def test_trying_to_retrieve_single_without_privileges(test_ctx): | ||||
|     db.session.add(test_ctx.user_factory(name='u1', rank=db.User.RANK_REGULAR)) | ||||
| def test_trying_to_retrieve_single_without_privileges( | ||||
|         user_factory, context_factory): | ||||
|     auth_user = user_factory(rank=db.User.RANK_ANONYMOUS) | ||||
|     db.session.add(user_factory(name='u1', rank=db.User.RANK_REGULAR)) | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.detail_api.get( | ||||
|             test_ctx.context_factory( | ||||
|                 user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)), | ||||
|             'u1') | ||||
|         api.user_api.get_user( | ||||
|             context_factory(user=auth_user), {'user_name': 'u1'}) | ||||
|  | ||||
| @ -1,20 +1,12 @@ | ||||
| import datetime | ||||
| import pytest | ||||
| from szurubooru import api, config, db, errors | ||||
| from szurubooru.func import auth, util, users | ||||
| import unittest.mock | ||||
| from datetime import datetime | ||||
| from szurubooru import api, db, errors | ||||
| from szurubooru.func import users | ||||
| 
 | ||||
| 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' | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def test_ctx(tmpdir, config_injector, context_factory, user_factory): | ||||
| @pytest.fixture(autouse=True) | ||||
| def inject_config(config_injector): | ||||
|     config_injector({ | ||||
|         'secret': '', | ||||
|         'user_name_regex': '^[^!]{3,}$', | ||||
|         'password_regex': '^[^!]{3,}$', | ||||
|         'thumbnails': {'avatar_width': 200, 'avatar_height': 200}, | ||||
|         'privileges': { | ||||
|             'users:edit:self:name': db.User.RANK_REGULAR, | ||||
|             'users:edit:self:pass': db.User.RANK_REGULAR, | ||||
| @ -27,203 +19,97 @@ def test_ctx(tmpdir, config_injector, context_factory, user_factory): | ||||
|             'users:edit:any:rank': db.User.RANK_ADMINISTRATOR, | ||||
|             'users:edit:any:avatar': db.User.RANK_ADMINISTRATOR, | ||||
|         }, | ||||
|         'data_dir': str(tmpdir.mkdir('data')), | ||||
|         'data_url': 'http://example.com/data/', | ||||
|     }) | ||||
|     ret = util.dotdict() | ||||
|     ret.context_factory = context_factory | ||||
|     ret.user_factory = user_factory | ||||
|     ret.api = api.UserDetailApi() | ||||
|     return ret | ||||
| 
 | ||||
| def test_updating_user(test_ctx): | ||||
|     user = test_ctx.user_factory(name='u1', rank=db.User.RANK_ADMINISTRATOR) | ||||
| def test_updating_user(context_factory, user_factory): | ||||
|     user = user_factory(name='u1', rank=db.User.RANK_ADMINISTRATOR) | ||||
|     auth_user = user_factory(rank=db.User.RANK_ADMINISTRATOR) | ||||
|     db.session.add(user) | ||||
|     result = test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={ | ||||
|                 'version': 1, | ||||
|                 'name': 'chewie', | ||||
|                 'email': 'asd@asd.asd', | ||||
|                 'password': 'oks', | ||||
|                 'rank': 'moderator', | ||||
|                 'avatarStyle': 'gravatar', | ||||
|             }, | ||||
|             user=user), | ||||
|         'u1') | ||||
|     assert result == { | ||||
|         'avatarStyle': 'gravatar', | ||||
|         'avatarUrl': 'https://gravatar.com/avatar/' + | ||||
|             '6f370c8c7109534c3d5c394123a477d7?d=retro&s=200', | ||||
|         'creationTime': datetime.datetime(1997, 1, 1), | ||||
|         'lastLoginTime': None, | ||||
|         'email': 'asd@asd.asd', | ||||
|         'name': 'chewie', | ||||
|         'rank': 'moderator', | ||||
|         'commentCount': 0, | ||||
|         'likedPostCount': 0, | ||||
|         'dislikedPostCount': 0, | ||||
|         'favoritePostCount': 0, | ||||
|         'uploadedPostCount': 0, | ||||
|         'version': 2, | ||||
|     } | ||||
|     user = users.get_user_by_name('chewie') | ||||
|     assert user.name == 'chewie' | ||||
|     assert user.email == 'asd@asd.asd' | ||||
|     assert user.rank == db.User.RANK_MODERATOR | ||||
|     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 | ||||
|     db.session.flush() | ||||
| 
 | ||||
| @pytest.mark.parametrize('input,expected_exception', [ | ||||
|     ({'name': None}, users.InvalidUserNameError), | ||||
|     ({'name': ''}, users.InvalidUserNameError), | ||||
|     ({'name': '!bad'}, users.InvalidUserNameError), | ||||
|     ({'name': 'x' * 51}, users.InvalidUserNameError), | ||||
|     ({'password': None}, users.InvalidPasswordError), | ||||
|     ({'password': ''}, users.InvalidPasswordError), | ||||
|     ({'password': '!bad'}, users.InvalidPasswordError), | ||||
|     ({'rank': None}, users.InvalidRankError), | ||||
|     ({'rank': ''}, users.InvalidRankError), | ||||
|     ({'rank': 'bad'}, users.InvalidRankError), | ||||
|     ({'rank': 'anonymous'}, users.InvalidRankError), | ||||
|     ({'rank': 'nobody'}, users.InvalidRankError), | ||||
|     ({'email': 'bad'}, users.InvalidEmailError), | ||||
|     ({'email': 'x@' * 65 + '.com'}, users.InvalidEmailError), | ||||
|     ({'avatarStyle': None}, users.InvalidAvatarError), | ||||
|     ({'avatarStyle': ''}, users.InvalidAvatarError), | ||||
|     ({'avatarStyle': 'invalid'}, users.InvalidAvatarError), | ||||
|     ({'avatarStyle': 'manual'}, users.InvalidAvatarError), # missing file | ||||
| ]) | ||||
| def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception): | ||||
|     user = test_ctx.user_factory(name='u1', rank=db.User.RANK_ADMINISTRATOR) | ||||
|     db.session.add(user) | ||||
|     with pytest.raises(expected_exception): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={**input, **{'version': 1}}, | ||||
|                 user=user), | ||||
|             'u1') | ||||
|     with unittest.mock.patch('szurubooru.func.users.create_user'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_name'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_password'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_email'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_rank'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_avatar'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.serialize_user'): | ||||
|         users.serialize_user.return_value = 'serialized user' | ||||
| 
 | ||||
|         result = api.user_api.update_user( | ||||
|             context_factory( | ||||
|                 params={ | ||||
|                     'version': 1, | ||||
|                     'name': 'chewie', | ||||
|                     'email': 'asd@asd.asd', | ||||
|                     'password': 'oks', | ||||
|                     'rank': 'moderator', | ||||
|                     'avatarStyle': 'manual', | ||||
|                 }, | ||||
|                 files={ | ||||
|                     'avatar': b'...', | ||||
|                 }, | ||||
|                 user=auth_user), | ||||
|             {'user_name': 'u1'}) | ||||
| 
 | ||||
|         assert result == 'serialized user' | ||||
|         users.create_user.assert_not_called() | ||||
|         users.update_user_name.assert_called_once_with(user, 'chewie') | ||||
|         users.update_user_password.assert_called_once_with(user, 'oks') | ||||
|         users.update_user_email.assert_called_once_with(user, 'asd@asd.asd') | ||||
|         users.update_user_rank.assert_called_once_with(user, 'moderator', auth_user) | ||||
|         users.update_user_avatar.assert_called_once_with(user, 'manual', b'...') | ||||
|         users.serialize_user.assert_called_once_with(user, auth_user, options=None) | ||||
| 
 | ||||
| @pytest.mark.parametrize( | ||||
|     'field', ['name', 'email', 'password', 'rank', 'avatarStyle']) | ||||
| def test_omitting_optional_field(test_ctx, field): | ||||
|     user = test_ctx.user_factory(name='u1', rank=db.User.RANK_ADMINISTRATOR) | ||||
| def test_omitting_optional_field(user_factory, context_factory, field): | ||||
|     user = user_factory(name='u1', rank=db.User.RANK_ADMINISTRATOR) | ||||
|     db.session.add(user) | ||||
|     input = { | ||||
|     params = { | ||||
|         'name': 'chewie', | ||||
|         'email': 'asd@asd.asd', | ||||
|         'password': 'oks', | ||||
|         'rank': 'moderator', | ||||
|         'avatarStyle': 'gravatar', | ||||
|     } | ||||
|     del input[field] | ||||
|     result = test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={**input, **{'version': 1}}, | ||||
|             files={'avatar': EMPTY_PIXEL}, | ||||
|             user=user), | ||||
|         'u1') | ||||
|     assert result is not None | ||||
|     del params[field] | ||||
|     with unittest.mock.patch('szurubooru.func.users.create_user'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_name'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_password'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_email'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_rank'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.update_user_avatar'), \ | ||||
|             unittest.mock.patch('szurubooru.func.users.serialize_user'): | ||||
|         api.user_api.update_user( | ||||
|             context_factory( | ||||
|                 params={**params, **{'version': 1}}, | ||||
|                 files={'avatar': b'...'}, | ||||
|                 user=user), | ||||
|             {'user_name': 'u1'}) | ||||
| 
 | ||||
| def test_trying_to_update_non_existing(test_ctx): | ||||
|     user = test_ctx.user_factory(name='u1', rank=db.User.RANK_ADMINISTRATOR) | ||||
| def test_trying_to_update_non_existing(user_factory, context_factory): | ||||
|     user = user_factory(name='u1', rank=db.User.RANK_ADMINISTRATOR) | ||||
|     db.session.add(user) | ||||
|     with pytest.raises(users.UserNotFoundError): | ||||
|         test_ctx.api.put(test_ctx.context_factory(user=user), 'u2') | ||||
|         api.user_api.update_user( | ||||
|             context_factory(user=user), {'user_name': 'u2'}) | ||||
| 
 | ||||
| def test_removing_email(test_ctx): | ||||
|     user = test_ctx.user_factory(name='u1', rank=db.User.RANK_ADMINISTRATOR) | ||||
|     db.session.add(user) | ||||
|     test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={'email': '', 'version': 1}, user=user), 'u1') | ||||
|     assert users.get_user_by_name('u1').email is None | ||||
| 
 | ||||
| @pytest.mark.parametrize('input', [ | ||||
| @pytest.mark.parametrize('params', [ | ||||
|     {'name': 'whatever'}, | ||||
|     {'email': 'whatever'}, | ||||
|     {'rank': 'whatever'}, | ||||
|     {'password': 'whatever'}, | ||||
|     {'avatarStyle': 'whatever'}, | ||||
| ]) | ||||
| def test_trying_to_update_someone_else(test_ctx, input): | ||||
|     user1 = test_ctx.user_factory(name='u1', rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(name='u2', rank=db.User.RANK_REGULAR) | ||||
| def test_trying_to_update_field_without_privileges( | ||||
|         user_factory, context_factory, params): | ||||
|     user1 = user_factory(name='u1', rank=db.User.RANK_REGULAR) | ||||
|     user2 = user_factory(name='u2', rank=db.User.RANK_REGULAR) | ||||
|     db.session.add_all([user1, user2]) | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={**input, **{'version': 1}}, | ||||
|         api.user_api.update_user( | ||||
|             context_factory( | ||||
|                 params={**params, **{'version': 1}}, | ||||
|                 user=user1), | ||||
|             user2.name) | ||||
| 
 | ||||
| def test_trying_to_become_someone_else(test_ctx): | ||||
|     user1 = test_ctx.user_factory(name='me', rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(name='her', rank=db.User.RANK_REGULAR) | ||||
|     db.session.add_all([user1, user2]) | ||||
|     with pytest.raises(users.UserAlreadyExistsError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': 'her', 'version': 1}, user=user1), | ||||
|             'me') | ||||
|     with pytest.raises(users.UserAlreadyExistsError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': 'HER', 'version': 1}, user=user1), | ||||
|             'me') | ||||
| 
 | ||||
| def test_trying_to_make_someone_into_someone_else(test_ctx): | ||||
|     user1 = test_ctx.user_factory(name='him', rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(name='her', rank=db.User.RANK_REGULAR) | ||||
|     user3 = test_ctx.user_factory(name='me', rank=db.User.RANK_MODERATOR) | ||||
|     db.session.add_all([user1, user2, user3]) | ||||
|     with pytest.raises(users.UserAlreadyExistsError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': 'her', 'version': 1}, user=user3), | ||||
|             'him') | ||||
|     with pytest.raises(users.UserAlreadyExistsError): | ||||
|         test_ctx.api.put( | ||||
|             test_ctx.context_factory( | ||||
|                 input={'name': 'HER', 'version': 1}, user=user3), | ||||
|             'him') | ||||
| 
 | ||||
| def test_renaming_someone_else(test_ctx): | ||||
|     user1 = test_ctx.user_factory(name='him', rank=db.User.RANK_REGULAR) | ||||
|     user2 = test_ctx.user_factory(name='me', rank=db.User.RANK_MODERATOR) | ||||
|     db.session.add_all([user1, user2]) | ||||
|     test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={'name': 'himself', 'version': 1}, user=user2), | ||||
|         'him') | ||||
|     test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={'name': 'HIMSELF', 'version': 2}, user=user2), | ||||
|         'himself') | ||||
| 
 | ||||
| def test_mods_trying_to_become_admin(test_ctx): | ||||
|     user1 = test_ctx.user_factory(name='u1', rank=db.User.RANK_MODERATOR) | ||||
|     user2 = test_ctx.user_factory(name='u2', rank=db.User.RANK_MODERATOR) | ||||
|     db.session.add_all([user1, user2]) | ||||
|     context = test_ctx.context_factory( | ||||
|         input={'rank': 'administrator', 'version': 1}, | ||||
|         user=user1) | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.put(context, user1.name) | ||||
|     with pytest.raises(errors.AuthError): | ||||
|         test_ctx.api.put(context, user2.name) | ||||
| 
 | ||||
| def test_uploading_avatar(test_ctx): | ||||
|     user = test_ctx.user_factory(name='u1', rank=db.User.RANK_MODERATOR) | ||||
|     db.session.add(user) | ||||
|     response = test_ctx.api.put( | ||||
|         test_ctx.context_factory( | ||||
|             input={'avatarStyle': 'manual', 'version': 1}, | ||||
|             files={'avatar': EMPTY_PIXEL}, | ||||
|             user=user), | ||||
|         'u1') | ||||
|     user = users.get_user_by_name('u1') | ||||
|     assert user.avatar_style == user.AVATAR_MANUAL | ||||
|     assert response['avatarUrl'] == \ | ||||
|         'http://example.com/data/avatars/u1.png' | ||||
|             {'user_name': user2.name}) | ||||
|  | ||||
| @ -5,7 +5,7 @@ import uuid | ||||
| import pytest | ||||
| import freezegun | ||||
| import sqlalchemy | ||||
| from szurubooru import api, config, db | ||||
| from szurubooru import api, config, db, rest | ||||
| from szurubooru.func import util | ||||
| 
 | ||||
| class QueryCounter(object): | ||||
| @ -74,12 +74,14 @@ def session(query_logger): | ||||
| 
 | ||||
| @pytest.fixture | ||||
| def context_factory(session): | ||||
|     def factory(request=None, input=None, files=None, user=None): | ||||
|         ctx = api.Context() | ||||
|         ctx.input = input or {} | ||||
|     def factory(params=None, files=None, user=None): | ||||
|         ctx = rest.Context( | ||||
|             method=None, | ||||
|             url=None, | ||||
|             headers={}, | ||||
|             params=params or {}, | ||||
|             files=files or {}) | ||||
|         ctx.session = session | ||||
|         ctx.request = request or {} | ||||
|         ctx.files = files or {} | ||||
|         ctx.user = user or db.User() | ||||
|         return ctx | ||||
|     return factory | ||||
|  | ||||
| @ -1,32 +1,30 @@ | ||||
| import unittest.mock | ||||
| import pytest | ||||
| from szurubooru import api, errors | ||||
| from szurubooru import rest, errors | ||||
| from szurubooru.func import net | ||||
| 
 | ||||
| def test_has_param(): | ||||
|     ctx = api.Context() | ||||
|     ctx.input = {'key': 'value'} | ||||
|     ctx = rest.Context(method=None, url=None, params={'key': 'value'}) | ||||
|     assert ctx.has_param('key') | ||||
|     assert not ctx.has_param('key2') | ||||
| 
 | ||||
| def test_get_file(): | ||||
|     ctx = api.Context() | ||||
|     ctx.files = {'key': b'content'} | ||||
|     ctx = rest.Context(method=None, url=None, files={'key': b'content'}) | ||||
|     assert ctx.get_file('key') == b'content' | ||||
|     assert ctx.get_file('key2') is None | ||||
| 
 | ||||
| def test_get_file_from_url(): | ||||
|     with unittest.mock.patch('szurubooru.func.net.download'): | ||||
|         net.download.return_value = b'content' | ||||
|         ctx = api.Context() | ||||
|         ctx.input = {'keyUrl': 'example.com'} | ||||
|         ctx = rest.Context( | ||||
|             method=None, url=None, params={'keyUrl': 'example.com'}) | ||||
|         assert ctx.get_file('key') == b'content' | ||||
|         assert ctx.get_file('key2') is None | ||||
|         net.download.assert_called_once_with('example.com') | ||||
| 
 | ||||
| def test_getting_list_parameter(): | ||||
|     ctx = api.Context() | ||||
|     ctx.input = {'key': 'value', 'list': ['1', '2', '3']} | ||||
|     ctx = rest.Context( | ||||
|         method=None, url=None, params={'key': 'value', 'list': ['1', '2', '3']}) | ||||
|     assert ctx.get_param_as_list('key') == ['value'] | ||||
|     assert ctx.get_param_as_list('key2') is None | ||||
|     assert ctx.get_param_as_list('key2', default=['def']) == ['def'] | ||||
| @ -35,8 +33,8 @@ def test_getting_list_parameter(): | ||||
|         ctx.get_param_as_list('key2', required=True) | ||||
| 
 | ||||
| def test_getting_string_parameter(): | ||||
|     ctx = api.Context() | ||||
|     ctx.input = {'key': 'value', 'list': ['1', '2', '3']} | ||||
|     ctx = rest.Context( | ||||
|         method=None, url=None, params={'key': 'value', 'list': ['1', '2', '3']}) | ||||
|     assert ctx.get_param_as_string('key') == 'value' | ||||
|     assert ctx.get_param_as_string('key2') is None | ||||
|     assert ctx.get_param_as_string('key2', default='def') == 'def' | ||||
| @ -45,8 +43,10 @@ def test_getting_string_parameter(): | ||||
|         ctx.get_param_as_string('key2', required=True) | ||||
| 
 | ||||
| def test_getting_int_parameter(): | ||||
|     ctx = api.Context() | ||||
|     ctx.input = {'key': '50', 'err': 'invalid', 'list': [1, 2, 3]} | ||||
|     ctx = rest.Context( | ||||
|         method=None, | ||||
|         url=None, | ||||
|         params={'key': '50', 'err': 'invalid', 'list': [1, 2, 3]}) | ||||
|     assert ctx.get_param_as_int('key') == 50 | ||||
|     assert ctx.get_param_as_int('key2') is None | ||||
|     assert ctx.get_param_as_int('key2', default=5) == 5 | ||||
| @ -65,8 +65,7 @@ def test_getting_int_parameter(): | ||||
| 
 | ||||
| def test_getting_bool_parameter(): | ||||
|     def test(value): | ||||
|         ctx = api.Context() | ||||
|         ctx.input = {'key': value} | ||||
|         ctx = rest.Context(method=None, url=None, params={'key': value}) | ||||
|         return ctx.get_param_as_bool('key') | ||||
| 
 | ||||
|     assert test('1') is True | ||||
| @ -94,7 +93,7 @@ def test_getting_bool_parameter(): | ||||
|     with pytest.raises(errors.ValidationError): | ||||
|         test(['1', '2']) | ||||
| 
 | ||||
|     ctx = api.Context() | ||||
|     ctx = rest.Context(method=None, url=None) | ||||
|     assert ctx.get_param_as_bool('non-existing') is None | ||||
|     assert ctx.get_param_as_bool('non-existing', default=True) is True | ||||
|     with pytest.raises(errors.ValidationError): | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 rr-
						rr-