server/general: add entity versions

This commit is contained in:
rr-
2016-08-06 21:16:39 +02:00
parent f4ea0d84ad
commit 8d04df38fd
37 changed files with 313 additions and 67 deletions

View File

@ -39,16 +39,19 @@ class CommentDetailApi(BaseApi):
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)
comment.last_edit_time = datetime.datetime.utcnow()
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 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)

View File

@ -1,6 +1,6 @@
from szurubooru import config, errors
from szurubooru.api.base_api import BaseApi
from szurubooru.func import auth, mailer, users
from szurubooru.func import auth, mailer, users, util
MAIL_SUBJECT = 'Password reset for {name}'
MAIL_BODY = \
@ -34,5 +34,6 @@ class PasswordResetApi(BaseApi):
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}

View File

@ -62,6 +62,7 @@ class PostDetailApi(BaseApi):
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'))
@ -90,6 +91,7 @@ class PostDetailApi(BaseApi):
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)
@ -100,6 +102,7 @@ class PostDetailApi(BaseApi):
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()

View File

@ -60,6 +60,7 @@ class TagDetailApi(BaseApi):
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'))
@ -81,6 +82,7 @@ class TagDetailApi(BaseApi):
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)
@ -90,6 +92,7 @@ class TagDetailApi(BaseApi):
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)
@ -103,11 +106,14 @@ class TagMergeApi(BaseApi):
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)

View File

@ -33,6 +33,7 @@ class TagCategoryDetailApi(BaseApi):
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(
@ -41,6 +42,7 @@ class TagCategoryDetailApi(BaseApi):
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()
@ -49,6 +51,7 @@ class TagCategoryDetailApi(BaseApi):
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')
if len(tag_categories.get_all_category_names()) == 1:
raise tag_categories.TagCategoryIsInUseError(

View File

@ -47,6 +47,7 @@ class UserDetailApi(BaseApi):
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)
@ -68,11 +69,13 @@ class UserDetailApi(BaseApi):
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)

View File

@ -26,6 +26,7 @@ class Comment(Base):
'post_id', Integer, ForeignKey('post.id'), index=True, nullable=False)
user_id = Column(
'user_id', Integer, ForeignKey('user.id'), index=True)
version = Column('version', Integer, default=1, nullable=False)
creation_time = Column('creation_time', DateTime, nullable=False)
last_edit_time = Column('last_edit_time', DateTime)
text = Column('text', UnicodeText, default=None)

View File

@ -101,6 +101,7 @@ class Post(Base):
# basic meta
post_id = Column('id', Integer, primary_key=True)
user_id = Column('user_id', Integer, ForeignKey('user.id'), index=True)
version = Column('version', Integer, default=1, nullable=False)
creation_time = Column('creation_time', DateTime, nullable=False)
last_edit_time = Column('last_edit_time', DateTime)
safety = Column('safety', Unicode(32), nullable=False)

View File

@ -44,6 +44,7 @@ class Tag(Base):
tag_id = Column('id', Integer, primary_key=True)
category_id = Column(
'category_id', Integer, ForeignKey('tag_category.id'), nullable=False, index=True)
version = Column('version', Integer, default=1, nullable=False)
creation_time = Column('creation_time', DateTime, nullable=False)
last_edit_time = Column('last_edit_time', DateTime)
description = Column('description', UnicodeText, default=None)

View File

@ -8,6 +8,7 @@ class TagCategory(Base):
__tablename__ = 'tag_category'
tag_category_id = Column('id', Integer, primary_key=True)
version = Column('version', Integer, default=1, nullable=False)
name = Column('name', Unicode(32), nullable=False)
color = Column('color', Unicode(32), nullable=False, default='#000000')
default = Column('default', Boolean, nullable=False, default=False)

View File

@ -20,13 +20,14 @@ class User(Base):
RANK_NOBODY = 'nobody' # used for privileges: "nobody can be higher than admin"
user_id = Column('id', Integer, primary_key=True)
creation_time = Column('creation_time', DateTime, nullable=False)
last_login_time = Column('last_login_time', DateTime)
version = Column('version', Integer, default=1, nullable=False)
name = Column('name', Unicode(50), nullable=False, unique=True)
password_hash = Column('password_hash', Unicode(64), nullable=False)
password_salt = Column('password_salt', Unicode(32))
email = Column('email', Unicode(64), nullable=True)
rank = Column('rank', Unicode(32), nullable=False)
creation_time = Column('creation_time', DateTime, nullable=False)
last_login_time = Column('last_login_time', DateTime)
avatar_style = Column(
'avatar_style', Unicode(32), nullable=False, default=AVATAR_GRAVATAR)

View File

@ -12,6 +12,7 @@ def serialize_comment(comment, authenticated_user, options=None):
'id': lambda: comment.comment_id,
'user': lambda: users.serialize_micro_user(comment.user),
'postId': lambda: comment.post.post_id,
'version': lambda: comment.version,
'text': lambda: comment.text,
'creationTime': lambda: comment.creation_time,
'lastEditTime': lambda: comment.last_edit_time,

View File

@ -67,6 +67,7 @@ def serialize_post(post, authenticated_user, options=None):
post,
{
'id': lambda: post.post_id,
'version': lambda: post.version,
'creationTime': lambda: post.creation_time,
'lastEditTime': lambda: post.last_edit_time,
'safety': lambda: SAFETY_MAP[post.safety],

View File

@ -20,6 +20,7 @@ def serialize_category(category, options=None):
category,
{
'name': lambda: category.name,
'version': lambda: category.version,
'color': lambda: category.color,
'usages': lambda: category.tag_count,
'default': lambda: category.default,

View File

@ -53,6 +53,7 @@ def serialize_tag(tag, options=None):
{
'names': lambda: [tag_name.name for tag_name in tag.names],
'category': lambda: tag.category.name,
'version': lambda: tag.version,
'description': lambda: tag.description,
'creationTime': lambda: tag.creation_time,
'lastEditTime': lambda: tag.last_edit_time,

View File

@ -46,9 +46,10 @@ def serialize_user(user, authenticated_user, options=None, force_show_email=Fals
user,
{
'name': lambda: user.name,
'rank': lambda: user.rank,
'creationTime': lambda: user.creation_time,
'lastLoginTime': lambda: user.last_login_time,
'version': lambda: user.version,
'rank': lambda: user.rank,
'avatarStyle': lambda: user.avatar_style,
'avatarUrl': lambda: _get_avatar_url(user),
'commentCount': lambda: user.comment_count,

View File

@ -139,3 +139,14 @@ def value_exceeds_column_size(value, column):
if max_length is None:
return False
return len(value) > max_length
def verify_version(entity, context, field_name='version'):
actual_version = context.get_param_as_int(field_name, required=True)
expected_version = entity.version
if actual_version != expected_version:
raise errors.InvalidParameterError(
'Someone else modified this in the meantime. ' +
'Please try again.')
def bump_version(entity):
entity.version += 1

View File

@ -0,0 +1,26 @@
'''
Add entity versions
Revision ID: 7f6baf38c27c
Created at: 2016-08-06 22:26:58.111763
'''
import sqlalchemy as sa
from alembic import op
revision = '7f6baf38c27c'
down_revision = '4c526f869323'
branch_labels = None
depends_on = None
tables = ['tag_category', 'tag', 'user', 'post', 'comment']
def upgrade():
for table in tables:
op.add_column(table, sa.Column('version', sa.Integer(), nullable=True))
op.execute(sa.table(table, sa.column('version')).update().values(version=1))
op.alter_column(table, 'version', nullable=False)
def downgrade():
for table in tables:
op.drop_column(table, 'version')

View File

@ -24,7 +24,8 @@ def test_deleting_own_comment(test_ctx):
db.session.add(comment)
db.session.commit()
result = test_ctx.api.delete(
test_ctx.context_factory(user=user), comment.comment_id)
test_ctx.context_factory(input={'version': 1}, user=user),
comment.comment_id)
assert result == {}
assert db.session.query(db.Comment).count() == 0
@ -35,7 +36,8 @@ def test_deleting_someones_else_comment(test_ctx):
db.session.add(comment)
db.session.commit()
result = test_ctx.api.delete(
test_ctx.context_factory(user=user2), comment.comment_id)
test_ctx.context_factory(input={'version': 1}, user=user2),
comment.comment_id)
assert db.session.query(db.Comment).count() == 0
def test_trying_to_delete_someones_else_comment_without_privileges(test_ctx):
@ -46,11 +48,14 @@ def test_trying_to_delete_someones_else_comment_without_privileges(test_ctx):
db.session.commit()
with pytest.raises(errors.AuthError):
test_ctx.api.delete(
test_ctx.context_factory(user=user2), comment.comment_id)
test_ctx.context_factory(input={'version': 1}, user=user2),
comment.comment_id)
assert db.session.query(db.Comment).count() == 1
def test_trying_to_delete_non_existing(test_ctx):
with pytest.raises(comments.CommentNotFoundError):
test_ctx.api.delete(
test_ctx.context_factory(
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), 1)
input={'version': 1},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
1)

View File

@ -31,7 +31,8 @@ def test_simple_updating(test_ctx, fake_datetime):
db.session.commit()
with fake_datetime('1997-12-01'):
result = test_ctx.api.put(
test_ctx.context_factory(input={'text': 'new text'}, user=user),
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()
@ -53,7 +54,8 @@ def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception):
db.session.commit()
with pytest.raises(expected_exception):
test_ctx.api.put(
test_ctx.context_factory(input=input, user=user),
test_ctx.context_factory(
input={**input, **{'version': 1}}, user=user),
comment.comment_id)
def test_trying_to_omit_mandatory_field(test_ctx):
@ -63,7 +65,7 @@ def test_trying_to_omit_mandatory_field(test_ctx):
db.session.commit()
with pytest.raises(errors.ValidationError):
test_ctx.api.put(
test_ctx.context_factory(input={}, user=user),
test_ctx.context_factory(input={'version': 1}, user=user),
comment.comment_id)
def test_trying_to_update_non_existing(test_ctx):
@ -82,7 +84,8 @@ def test_trying_to_update_someones_comment_without_privileges(test_ctx):
db.session.commit()
with pytest.raises(errors.AuthError):
test_ctx.api.put(
test_ctx.context_factory(input={'text': 'new text'}, user=user2),
test_ctx.context_factory(
input={'text': 'new text', 'version': 1}, user=user2),
comment.comment_id)
def test_updating_someones_comment_with_privileges(test_ctx):
@ -93,7 +96,8 @@ def test_updating_someones_comment_with_privileges(test_ctx):
db.session.commit()
try:
test_ctx.api.put(
test_ctx.context_factory(input={'text': 'new text'}, user=user2),
test_ctx.context_factory(
input={'text': 'new text', 'version': 1}, user=user2),
comment.comment_id)
except:
pytest.fail()

View File

@ -25,6 +25,7 @@ def test_deleting(test_ctx):
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 == {}

View File

@ -43,6 +43,7 @@ def test_post_updating(
result = api.PostDetailApi().put(
context_factory(
input={
'version': 1,
'safety': 'safe',
'tags': ['tag1', 'tag2'],
'relations': [1, 2],
@ -89,7 +90,7 @@ def test_uploading_from_url_saves_source(
net.download.return_value = b'content'
api.PostDetailApi().put(
context_factory(
input={'contentUrl': 'example.com'},
input={'contentUrl': 'example.com', 'version': 1},
user=user_factory(rank=db.User.RANK_REGULAR)),
post.post_id)
net.download.assert_called_once_with('example.com')
@ -116,7 +117,10 @@ def test_uploading_from_url_with_source_specified(
net.download.return_value = b'content'
api.PostDetailApi().put(
context_factory(
input={'contentUrl': 'example.com', 'source': 'example2.com'},
input={
'contentUrl': 'example.com',
'source': 'example2.com',
'version': 1},
user=user_factory(rank=db.User.RANK_REGULAR)),
post.post_id)
net.download.assert_called_once_with('example.com')
@ -158,7 +162,7 @@ def test_trying_to_update_field_without_privileges(
with pytest.raises(errors.AuthError):
api.PostDetailApi().put(
context_factory(
input=input,
input={**input, **{'version': 1}},
files=files,
user=user_factory(rank=db.User.RANK_ANONYMOUS)),
post.post_id)
@ -179,6 +183,6 @@ def test_trying_to_create_tags_without_privileges(
posts.update_post_tags.return_value = ['new-tag']
api.PostDetailApi().put(
context_factory(
input={'tags': ['tag1', 'tag2']},
input={'tags': ['tag1', 'tag2'], 'version': 1},
user=user_factory(rank=db.User.RANK_REGULAR)),
post.post_id)

View File

@ -28,6 +28,7 @@ def test_creating_category(test_ctx):
'color': 'black',
'usages': 0,
'default': True,
'version': 1,
}
category = db.session.query(db.TagCategory).one()
assert category.name == 'meta'

View File

@ -32,6 +32,7 @@ def test_deleting(test_ctx):
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 == {}
@ -49,6 +50,7 @@ def test_trying_to_delete_used(test_ctx, tag_factory):
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')
assert db.session.query(db.TagCategory).count() == 1
@ -59,6 +61,7 @@ def test_trying_to_delete_last(test_ctx, tag_factory):
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')
@ -66,7 +69,8 @@ def test_trying_to_delete_non_existing(test_ctx):
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')
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'bad')
def test_trying_to_delete_without_privileges(test_ctx):
db.session.add(test_ctx.tag_category_factory(name='category'))
@ -74,6 +78,7 @@ def test_trying_to_delete_without_privileges(test_ctx):
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')
assert db.session.query(db.TagCategory).count() == 1

View File

@ -43,6 +43,7 @@ def test_retrieving_single(test_ctx):
'usages': 0,
'default': False,
'snapshots': [],
'version': 1,
}
def test_trying_to_retrieve_single_non_existing(test_ctx):

View File

@ -34,6 +34,7 @@ def test_simple_updating(test_ctx):
input={
'name': 'changed',
'color': 'white',
'version': 1,
},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'name')
@ -44,6 +45,7 @@ def test_simple_updating(test_ctx):
'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')
@ -66,7 +68,7 @@ def test_trying_to_pass_invalid_input(test_ctx, input):
with pytest.raises(tag_categories.InvalidTagCategoryNameError):
test_ctx.api.put(
test_ctx.context_factory(
input=input,
input={**input, **{'version': 1}},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'meta')
@ -81,7 +83,7 @@ def test_omitting_optional_field(test_ctx, field):
del input[field]
result = test_ctx.api.put(
test_ctx.context_factory(
input=input,
input={**input, **{'version': 1}},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'name')
assert result is not None
@ -100,7 +102,7 @@ def test_reusing_own_name(test_ctx, new_name):
db.session.commit()
result = test_ctx.api.put(
test_ctx.context_factory(
input={'name': new_name},
input={'name': new_name, 'version': 1},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'cat')
assert result['name'] == new_name
@ -116,7 +118,7 @@ def test_trying_to_use_existing_name(test_ctx, dup_name):
with pytest.raises(tag_categories.TagCategoryAlreadyExistsError):
test_ctx.api.put(
test_ctx.context_factory(
input={'name': dup_name},
input={'name': dup_name, 'version': 1},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'cat2')
@ -130,6 +132,6 @@ def test_trying_to_update_without_privileges(test_ctx, input):
with pytest.raises(errors.AuthError):
test_ctx.api.put(
test_ctx.context_factory(
input=input,
input={**input, **{'version': 1}},
user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)),
'dummy')

View File

@ -50,6 +50,7 @@ def test_creating_simple_tags(test_ctx, fake_datetime):
'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']

View File

@ -25,6 +25,7 @@ def test_deleting(test_ctx):
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 == {}
@ -39,6 +40,7 @@ def test_deleting_used(test_ctx, post_factory):
db.session.commit()
test_ctx.api.delete(
test_ctx.context_factory(
input={'version': 1},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'tag')
db.session.refresh(post)
@ -57,6 +59,7 @@ def test_trying_to_delete_without_privileges(test_ctx):
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')
assert db.session.query(db.Tag).count() == 1

View File

@ -29,6 +29,8 @@ def test_merging_without_usages(test_ctx, fake_datetime):
result = test_ctx.api.post(
test_ctx.context_factory(
input={
'removeVersion': 1,
'mergeToVersion': 1,
'remove': 'source',
'mergeTo': 'target',
},
@ -44,6 +46,7 @@ def test_merging_without_usages(test_ctx, fake_datetime):
'creationTime': datetime.datetime(1996, 1, 1),
'lastEditTime': None,
'usages': 0,
'version': 2,
}
assert tags.try_get_tag_by_name('source') is None
tag = tags.get_tag_by_name('target')
@ -67,6 +70,8 @@ def test_merging_with_usages(test_ctx, fake_datetime, post_factory):
result = test_ctx.api.post(
test_ctx.context_factory(
input={
'removeVersion': 1,
'mergeToVersion': 1,
'remove': 'source',
'mergeTo': 'target',
},
@ -90,6 +95,8 @@ def test_merging_when_related(test_ctx, fake_datetime):
result = test_ctx.api.post(
test_ctx.context_factory(
input={
'removeVersion': 1,
'mergeToVersion': 1,
'remove': 'source',
'mergeTo': 'target',
},
@ -113,6 +120,8 @@ def test_merging_when_target_exists(test_ctx, fake_datetime, post_factory):
result = test_ctx.api.post(
test_ctx.context_factory(
input={
'removeVersion': 1,
'mergeToVersion': 1,
'remove': 'source',
'mergeTo': 'target',
},
@ -134,6 +143,8 @@ def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception):
db.session.add_all([source_tag, target_tag])
db.session.commit()
real_input = {
'removeVersion': 1,
'mergeToVersion': 1,
'remove': 'source',
'mergeTo': 'target',
}
@ -146,7 +157,7 @@ def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception):
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)))
@pytest.mark.parametrize(
'field', ['remove', 'mergeTo'])
'field', ['remove', 'mergeTo', 'removeVersion', 'mergeToVersion'])
def test_trying_to_omit_mandatory_field(test_ctx, field):
db.session.add_all([
test_ctx.tag_factory(names=['source'], category_name='meta'),
@ -154,6 +165,8 @@ def test_trying_to_omit_mandatory_field(test_ctx, field):
])
db.session.commit()
input = {
'removeVersion': 1,
'mergeToVersion': 1,
'remove': 'source',
'mergeTo': 'target',
}
@ -184,7 +197,11 @@ def test_trying_to_merge_to_itself(test_ctx):
with pytest.raises(tags.InvalidTagRelationError):
test_ctx.api.post(
test_ctx.context_factory(
input={'remove': 'good', 'mergeTo': 'good'},
input={
'removeVersion': 1,
'mergeToVersion': 1,
'remove': 'good',
'mergeTo': 'good'},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)))
@pytest.mark.parametrize('input', [
@ -203,6 +220,8 @@ def test_trying_to_merge_without_privileges(test_ctx, input):
test_ctx.api.post(
test_ctx.context_factory(
input={
'removeVersion': 1,
'mergeToVersion': 1,
'remove': 'source',
'mergeTo': 'target',
},

View File

@ -57,6 +57,7 @@ def test_retrieving_single(test_ctx):
'implications': [],
'usages': 0,
'snapshots': [],
'version': 1,
}
def test_trying_to_retrieve_single_non_existing(test_ctx):

View File

@ -42,6 +42,7 @@ def test_simple_updating(test_ctx, fake_datetime):
result = test_ctx.api.put(
test_ctx.context_factory(
input={
'version': 1,
'names': ['tag3'],
'category': 'character',
'description': 'desc',
@ -59,6 +60,7 @@ def test_simple_updating(test_ctx, fake_datetime):
'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
@ -89,7 +91,7 @@ def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception):
with pytest.raises(expected_exception):
test_ctx.api.put(
test_ctx.context_factory(
input=input,
input={**input, **{'version': 1}},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'tag1')
@ -108,7 +110,7 @@ def test_omitting_optional_field(test_ctx, field):
del input[field]
result = test_ctx.api.put(
test_ctx.context_factory(
input=input,
input={**input, **{'version': 1}},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'tag')
assert result is not None
@ -128,7 +130,7 @@ def test_reusing_own_name(test_ctx, dup_name):
db.session.commit()
result = test_ctx.api.put(
test_ctx.context_factory(
input={'names': [dup_name, 'tag3']},
input={'names': [dup_name, 'tag3'], 'version': 1},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'tag1')
assert result['names'] == ['tag1', 'tag3']
@ -143,7 +145,7 @@ def test_duplicating_names(test_ctx):
test_ctx.tag_factory(names=['tag1', 'tag2'], category_name='meta'))
result = test_ctx.api.put(
test_ctx.context_factory(
input={'names': ['tag3', 'TAG3']},
input={'names': ['tag3', 'TAG3'], 'version': 1},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'tag1')
assert result['names'] == ['tag3']
@ -162,7 +164,7 @@ def test_trying_to_use_existing_name(test_ctx, dup_name):
with pytest.raises(tags.TagAlreadyExistsError):
test_ctx.api.put(
test_ctx.context_factory(
input={'names': [dup_name]},
input={'names': [dup_name], 'version': 1},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'tag3')
@ -195,7 +197,8 @@ def test_updating_new_suggestions_and_implications(
db.session.commit()
result = test_ctx.api.put(
test_ctx.context_factory(
input=input, user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
input={**input, **{'version': 1}},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'main')
assert result['suggestions'] == expected_suggestions
assert result['implications'] == expected_implications
@ -219,6 +222,7 @@ def test_reusing_suggestions_and_implications(test_ctx):
'category': 'meta',
'suggestions': ['TAG2'],
'implications': ['tag1'],
'version': 1,
},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'tag4')
@ -249,7 +253,8 @@ def test_trying_to_relate_tag_to_itself(test_ctx, input):
with pytest.raises(tags.InvalidTagRelationError):
test_ctx.api.put(
test_ctx.context_factory(
input=input, user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
input={**input, **{'version': 1}},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'tag1')
@pytest.mark.parametrize('input', [
@ -264,6 +269,6 @@ def test_trying_to_update_without_privileges(test_ctx, input):
with pytest.raises(errors.AuthError):
test_ctx.api.put(
test_ctx.context_factory(
input=input,
input={**input, **{'version': 1}},
user=test_ctx.user_factory(rank=db.User.RANK_ANONYMOUS)),
'tag')

View File

@ -50,6 +50,7 @@ def test_creating_user(test_ctx, fake_datetime):
'dislikedPostCount': 0,
'favoritePostCount': 0,
'uploadedPostCount': 0,
'version': 1,
}
user = users.get_user_by_name('chewie1')
assert user.name == 'chewie1'

View File

@ -21,7 +21,8 @@ def test_deleting_oneself(test_ctx):
user = test_ctx.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(user=user), 'u')
result = test_ctx.api.delete(
test_ctx.context_factory(input={'version': 1}, user=user), 'u')
assert result == {}
assert db.session.query(db.User).count() == 0
@ -30,7 +31,8 @@ def test_deleting_someone_else(test_ctx):
user2 = test_ctx.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(user=user2), 'u1')
test_ctx.api.delete(
test_ctx.context_factory(input={'version': 1}, user=user2), 'u1')
assert db.session.query(db.User).count() == 1
def test_trying_to_delete_someone_else_without_privileges(test_ctx):
@ -39,11 +41,14 @@ def test_trying_to_delete_someone_else_without_privileges(test_ctx):
db.session.add_all([user1, user2])
db.session.commit()
with pytest.raises(errors.AuthError):
test_ctx.api.delete(test_ctx.context_factory(user=user2), 'u1')
test_ctx.api.delete(
test_ctx.context_factory(input={'version': 1}, user=user2), 'u1')
assert db.session.query(db.User).count() == 2
def test_trying_to_delete_non_existing(test_ctx):
with pytest.raises(users.UserNotFoundError):
test_ctx.api.delete(
test_ctx.context_factory(
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)), 'bad')
input={'version': 1},
user=test_ctx.user_factory(rank=db.User.RANK_REGULAR)),
'bad')

View File

@ -61,6 +61,7 @@ def test_retrieving_single(test_ctx):
'dislikedPostCount': False,
'favoritePostCount': 0,
'uploadedPostCount': 0,
'version': 1,
}
assert result['email'] is False
assert result['likedPostCount'] is False

View File

@ -42,6 +42,7 @@ def test_updating_user(test_ctx):
result = test_ctx.api.put(
test_ctx.context_factory(
input={
'version': 1,
'name': 'chewie',
'email': 'asd@asd.asd',
'password': 'oks',
@ -64,6 +65,7 @@ def test_updating_user(test_ctx):
'dislikedPostCount': 0,
'favoritePostCount': 0,
'uploadedPostCount': 0,
'version': 2,
}
user = users.get_user_by_name('chewie')
assert user.name == 'chewie'
@ -98,7 +100,10 @@ def test_trying_to_pass_invalid_input(test_ctx, input, expected_exception):
db.session.add(user)
with pytest.raises(expected_exception):
test_ctx.api.put(
test_ctx.context_factory(input=input, user=user), 'u1')
test_ctx.context_factory(
input={**input, **{'version': 1}},
user=user),
'u1')
@pytest.mark.parametrize(
'field', ['name', 'email', 'password', 'rank', 'avatarStyle'])
@ -115,7 +120,7 @@ def test_omitting_optional_field(test_ctx, field):
del input[field]
result = test_ctx.api.put(
test_ctx.context_factory(
input=input,
input={**input, **{'version': 1}},
files={'avatar': EMPTY_PIXEL},
user=user),
'u1')
@ -131,7 +136,8 @@ 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': ''}, user=user), 'u1')
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', [
@ -147,7 +153,10 @@ def test_trying_to_update_someone_else(test_ctx, input):
db.session.add_all([user1, user2])
with pytest.raises(errors.AuthError):
test_ctx.api.put(
test_ctx.context_factory(input=input, user=user1), user2.name)
test_ctx.context_factory(
input={**input, **{'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)
@ -155,10 +164,14 @@ def test_trying_to_become_someone_else(test_ctx):
db.session.add_all([user1, user2])
with pytest.raises(users.UserAlreadyExistsError):
test_ctx.api.put(
test_ctx.context_factory(input={'name': 'her'}, user=user1), 'me')
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'}, user=user1), 'me')
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)
@ -167,25 +180,35 @@ def test_trying_to_make_someone_into_someone_else(test_ctx):
db.session.add_all([user1, user2, user3])
with pytest.raises(users.UserAlreadyExistsError):
test_ctx.api.put(
test_ctx.context_factory(input={'name': 'her'}, user=user3), 'him')
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'}, user=user3), 'him')
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'}, user=user2), 'him')
test_ctx.context_factory(
input={'name': 'himself', 'version': 1}, user=user2),
'him')
test_ctx.api.put(
test_ctx.context_factory(input={'name': 'HIMSELF'}, user=user2), 'himself')
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'}, user=user1)
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):
@ -196,7 +219,7 @@ def test_uploading_avatar(test_ctx):
db.session.add(user)
response = test_ctx.api.put(
test_ctx.context_factory(
input={'avatarStyle': 'manual'},
input={'avatarStyle': 'manual', 'version': 1},
files={'avatar': EMPTY_PIXEL},
user=user),
'u1')

View File

@ -130,6 +130,7 @@ def test_serialize_post(
assert result == {
'id': 1,
'version': 1,
'creationTime': datetime.datetime(1997, 1, 1),
'lastEditTime': datetime.datetime(1998, 1, 1),
'safety': 'safe',