server/api: improve input sanitization, fix docs
This commit is contained in:
parent
7263849fac
commit
83784c5e76
11
API.md
11
API.md
|
@ -14,7 +14,7 @@
|
||||||
2. [API reference](#api-reference)
|
2. [API reference](#api-reference)
|
||||||
|
|
||||||
- Tag categories
|
- Tag categories
|
||||||
- [Listing tag categories](#listing-tags-category)
|
- [Listing tag categories](#listing-tags-categories)
|
||||||
- [Creating tag category](#creating-tag-category)
|
- [Creating tag category](#creating-tag-category)
|
||||||
- [Updating tag category](#updating-tag-category)
|
- [Updating tag category](#updating-tag-category)
|
||||||
- [Getting tag category](#getting-tag-category)
|
- [Getting tag category](#getting-tag-category)
|
||||||
|
@ -245,7 +245,8 @@ Not implemented yet.
|
||||||
are optional. If specified implied tags or suggested tags do not exist yet,
|
are optional. If specified implied tags or suggested tags do not exist yet,
|
||||||
they will be automatically created. Tags created automatically have no
|
they will be automatically created. Tags created automatically have no
|
||||||
implications, no suggestions, one name and their category is set to the
|
implications, no suggestions, one name and their category is set to the
|
||||||
first item of `tag_categories` from server's configuration.
|
first tag category found. If there are no tag categories established yet,
|
||||||
|
an error will be thrown.
|
||||||
|
|
||||||
|
|
||||||
## Updating tag
|
## Updating tag
|
||||||
|
@ -275,10 +276,10 @@ Not implemented yet.
|
||||||
|
|
||||||
- **Errors**
|
- **Errors**
|
||||||
|
|
||||||
|
- the tag does not exist
|
||||||
- any name is used by an existing tag (names are case insensitive)
|
- any name is used by an existing tag (names are case insensitive)
|
||||||
- any name, implication or suggestion has invalid name
|
- any name, implication or suggestion name is invalid
|
||||||
- category is invalid
|
- category is invalid
|
||||||
- no name was specified
|
|
||||||
- implications or suggestions contain any item from names (e.g. there's a
|
- implications or suggestions contain any item from names (e.g. there's a
|
||||||
shallow cyclic dependency)
|
shallow cyclic dependency)
|
||||||
- privileges are too low
|
- privileges are too low
|
||||||
|
@ -338,7 +339,7 @@ Not implemented yet.
|
||||||
|
|
||||||
- **Description**
|
- **Description**
|
||||||
|
|
||||||
Deletes existing tag.
|
Deletes existing tag. The tag to be deleted must have no usages.
|
||||||
|
|
||||||
|
|
||||||
## Listing users
|
## Listing users
|
||||||
|
|
|
@ -70,6 +70,34 @@ def test_creating_simple_tags(test_ctx, fake_datetime):
|
||||||
assert_relations(tag.implications, [])
|
assert_relations(tag.implications, [])
|
||||||
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
|
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}, tags.InvalidTagCategoryError),
|
||||||
|
({'category': ''}, tags.InvalidTagCategoryError),
|
||||||
|
({'category': 'invalid'}, tags.InvalidTagCategoryError),
|
||||||
|
({'suggestions': ['good', '!bad']}, tags.InvalidTagNameError),
|
||||||
|
({'implications': ['good', '!bad']}, tags.InvalidTagNameError),
|
||||||
|
])
|
||||||
|
def test_invalid_inputs(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()))
|
||||||
|
|
||||||
@pytest.mark.parametrize('field', ['names', 'category'])
|
@pytest.mark.parametrize('field', ['names', 'category'])
|
||||||
def test_missing_mandatory_field(test_ctx, field):
|
def test_missing_mandatory_field(test_ctx, field):
|
||||||
input = {
|
input = {
|
||||||
|
@ -94,10 +122,11 @@ def test_missing_optional_field(test_ctx, tmpdir, field):
|
||||||
'implications': [],
|
'implications': [],
|
||||||
}
|
}
|
||||||
del input[field]
|
del input[field]
|
||||||
test_ctx.api.post(
|
result = test_ctx.api.post(
|
||||||
test_ctx.context_factory(
|
test_ctx.context_factory(
|
||||||
input=input,
|
input=input,
|
||||||
user=test_ctx.user_factory(rank='regular_user')))
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
def test_duplicating_names(test_ctx):
|
def test_duplicating_names(test_ctx):
|
||||||
result = test_ctx.api.post(
|
result = test_ctx.api.post(
|
||||||
|
@ -114,33 +143,6 @@ def test_duplicating_names(test_ctx):
|
||||||
tag = get_tag(test_ctx.session, 'tag1')
|
tag = get_tag(test_ctx.session, 'tag1')
|
||||||
assert [tag_name.name for tag_name in tag.names] == ['tag1']
|
assert [tag_name.name for tag_name in tag.names] == ['tag1']
|
||||||
|
|
||||||
def test_trying_to_create_tag_without_names(test_ctx):
|
|
||||||
with pytest.raises(tags.InvalidTagNameError):
|
|
||||||
test_ctx.api.post(
|
|
||||||
test_ctx.context_factory(
|
|
||||||
input={
|
|
||||||
'names': [],
|
|
||||||
'category': 'meta',
|
|
||||||
'suggestions': [],
|
|
||||||
'implications': [],
|
|
||||||
},
|
|
||||||
user=test_ctx.user_factory(rank='regular_user')))
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('names', [['!'], ['x' * 65]])
|
|
||||||
def test_trying_to_create_tag_with_invalid_name(test_ctx, names):
|
|
||||||
with pytest.raises(tags.InvalidTagNameError):
|
|
||||||
test_ctx.api.post(
|
|
||||||
test_ctx.context_factory(
|
|
||||||
input={
|
|
||||||
'names': names,
|
|
||||||
'category': 'meta',
|
|
||||||
'suggestions': [],
|
|
||||||
'implications': [],
|
|
||||||
},
|
|
||||||
user=test_ctx.user_factory(rank='regular_user')))
|
|
||||||
assert get_tag(test_ctx.session, 'ok') is None
|
|
||||||
assert get_tag(test_ctx.session, '!') is None
|
|
||||||
|
|
||||||
def test_trying_to_use_existing_name(test_ctx):
|
def test_trying_to_use_existing_name(test_ctx):
|
||||||
test_ctx.session.add_all([
|
test_ctx.session.add_all([
|
||||||
test_ctx.tag_factory(names=['used1'], category_name='meta'),
|
test_ctx.tag_factory(names=['used1'], category_name='meta'),
|
||||||
|
@ -169,19 +171,6 @@ def test_trying_to_use_existing_name(test_ctx):
|
||||||
user=test_ctx.user_factory(rank='regular_user')))
|
user=test_ctx.user_factory(rank='regular_user')))
|
||||||
assert get_tag(test_ctx.session, 'unused') is None
|
assert get_tag(test_ctx.session, 'unused') is None
|
||||||
|
|
||||||
def test_trying_to_create_tag_with_invalid_category(test_ctx):
|
|
||||||
with pytest.raises(tags.InvalidTagCategoryError):
|
|
||||||
test_ctx.api.post(
|
|
||||||
test_ctx.context_factory(
|
|
||||||
input={
|
|
||||||
'names': ['ok'],
|
|
||||||
'category': 'invalid',
|
|
||||||
'suggestions': [],
|
|
||||||
'implications': [],
|
|
||||||
},
|
|
||||||
user=test_ctx.user_factory(rank='regular_user')))
|
|
||||||
assert get_tag(test_ctx.session, 'ok') is None
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('input,expected_suggestions,expected_implications', [
|
@pytest.mark.parametrize('input,expected_suggestions,expected_implications', [
|
||||||
# new relations
|
# new relations
|
||||||
({
|
({
|
||||||
|
@ -285,7 +274,7 @@ def test_trying_to_create_tag_with_invalid_relation(test_ctx, input):
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
def test_tag_trying_to_relate_to_itself(test_ctx, input):
|
def test_tag_trying_to_relate_to_itself(test_ctx, input):
|
||||||
with pytest.raises(tags.RelationError):
|
with pytest.raises(tags.InvalidTagRelationError):
|
||||||
test_ctx.api.post(
|
test_ctx.api.post(
|
||||||
test_ctx.context_factory(
|
test_ctx.context_factory(
|
||||||
input=input,
|
input=input,
|
||||||
|
|
|
@ -27,7 +27,7 @@ def test_ctx(
|
||||||
ret.api = api.TagDetailApi()
|
ret.api = api.TagDetailApi()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def test_deleting_tags(test_ctx):
|
def test_deleting(test_ctx):
|
||||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag']))
|
test_ctx.session.add(test_ctx.tag_factory(names=['tag']))
|
||||||
test_ctx.session.commit()
|
test_ctx.session.commit()
|
||||||
result = test_ctx.api.delete(
|
result = test_ctx.api.delete(
|
||||||
|
@ -38,7 +38,7 @@ def test_deleting_tags(test_ctx):
|
||||||
assert test_ctx.session.query(db.Tag).count() == 0
|
assert test_ctx.session.query(db.Tag).count() == 0
|
||||||
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
|
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
|
||||||
|
|
||||||
def test_deleting_tags_without_privileges(test_ctx):
|
def test_deleting_without_privileges(test_ctx):
|
||||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag']))
|
test_ctx.session.add(test_ctx.tag_factory(names=['tag']))
|
||||||
test_ctx.session.commit()
|
test_ctx.session.commit()
|
||||||
with pytest.raises(errors.AuthError):
|
with pytest.raises(errors.AuthError):
|
||||||
|
@ -48,7 +48,7 @@ def test_deleting_tags_without_privileges(test_ctx):
|
||||||
'tag')
|
'tag')
|
||||||
assert test_ctx.session.query(db.Tag).count() == 1
|
assert test_ctx.session.query(db.Tag).count() == 1
|
||||||
|
|
||||||
def test_deleting_tags_with_usages(test_ctx, post_factory):
|
def test_deleting_with_usages(test_ctx, post_factory):
|
||||||
tag = test_ctx.tag_factory(names=['tag'])
|
tag = test_ctx.tag_factory(names=['tag'])
|
||||||
post = post_factory()
|
post = post_factory()
|
||||||
post.tags.append(tag)
|
post.tags.append(tag)
|
||||||
|
|
|
@ -20,9 +20,31 @@ def test_ctx(
|
||||||
ret.context_factory = context_factory
|
ret.context_factory = context_factory
|
||||||
ret.user_factory = user_factory
|
ret.user_factory = user_factory
|
||||||
ret.tag_factory = tag_factory
|
ret.tag_factory = tag_factory
|
||||||
|
ret.list_api = api.TagListApi()
|
||||||
ret.detail_api = api.TagDetailApi()
|
ret.detail_api = api.TagDetailApi()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
def test_retrieving_multiple(test_ctx):
|
||||||
|
tag1 = test_ctx.tag_factory(names=['t1'])
|
||||||
|
tag2 = test_ctx.tag_factory(names=['t2'])
|
||||||
|
test_ctx.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='regular_user')))
|
||||||
|
assert result['query'] == ''
|
||||||
|
assert result['page'] == 1
|
||||||
|
assert result['pageSize'] == 100
|
||||||
|
assert result['total'] == 2
|
||||||
|
assert [t['names'] for t in result['tags']] == [['t1'], ['t2']]
|
||||||
|
|
||||||
|
def test_retrieving_multiple_without_privileges(test_ctx):
|
||||||
|
with pytest.raises(errors.AuthError):
|
||||||
|
test_ctx.list_api.get(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input={'query': '', 'page': 1},
|
||||||
|
user=test_ctx.user_factory(rank='anonymous')))
|
||||||
|
|
||||||
def test_retrieving_single(test_ctx):
|
def test_retrieving_single(test_ctx):
|
||||||
test_ctx.session.add(test_ctx.tag_factory(names=['tag']))
|
test_ctx.session.add(test_ctx.tag_factory(names=['tag']))
|
||||||
result = test_ctx.detail_api.get(
|
result = test_ctx.detail_api.get(
|
||||||
|
|
|
@ -78,6 +78,50 @@ def test_simple_updating(test_ctx, fake_datetime):
|
||||||
assert tag.implications == []
|
assert tag.implications == []
|
||||||
assert os.path.exists(os.path.join(config.config['data_dir'], 'tags.json'))
|
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}, tags.InvalidTagCategoryError),
|
||||||
|
({'category': ''}, tags.InvalidTagCategoryError),
|
||||||
|
({'category': 'invalid'}, tags.InvalidTagCategoryError),
|
||||||
|
({'suggestions': ['good', '!bad']}, tags.InvalidTagNameError),
|
||||||
|
({'implications': ['good', '!bad']}, tags.InvalidTagNameError),
|
||||||
|
])
|
||||||
|
def test_invalid_inputs(test_ctx, input, expected_exception):
|
||||||
|
test_ctx.session.add(
|
||||||
|
test_ctx.tag_factory(names=['tag1'], category_name='meta'))
|
||||||
|
test_ctx.session.commit()
|
||||||
|
with pytest.raises(expected_exception):
|
||||||
|
test_ctx.api.put(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input=input,
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')),
|
||||||
|
'tag1')
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'field', ['names', 'category', 'implications', 'suggestions'])
|
||||||
|
def test_missing_optional_field(test_ctx, tmpdir, field):
|
||||||
|
test_ctx.session.add(
|
||||||
|
test_ctx.tag_factory(names=['tag'], category_name='meta'))
|
||||||
|
test_ctx.session.commit()
|
||||||
|
input = {
|
||||||
|
'names': ['tag1', 'tag2'],
|
||||||
|
'category': 'meta',
|
||||||
|
'suggestions': [],
|
||||||
|
'implications': [],
|
||||||
|
}
|
||||||
|
del input[field]
|
||||||
|
result = test_ctx.api.put(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input=input,
|
||||||
|
user=test_ctx.user_factory(rank='regular_user')),
|
||||||
|
'tag')
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
def test_trying_to_update_non_existing_tag(test_ctx):
|
def test_trying_to_update_non_existing_tag(test_ctx):
|
||||||
with pytest.raises(tags.TagNotFoundError):
|
with pytest.raises(tags.TagNotFoundError):
|
||||||
test_ctx.api.put(
|
test_ctx.api.put(
|
||||||
|
@ -118,22 +162,6 @@ def test_duplicating_names(test_ctx):
|
||||||
assert tag is not None
|
assert tag is not None
|
||||||
assert [tag_name.name for tag_name in tag.names] == ['tag3']
|
assert [tag_name.name for tag_name in tag.names] == ['tag3']
|
||||||
|
|
||||||
@pytest.mark.parametrize('input', [
|
|
||||||
{'names': []},
|
|
||||||
{'names': ['!']},
|
|
||||||
{'names': ['x' * 65]},
|
|
||||||
])
|
|
||||||
def test_trying_to_set_invalid_name(test_ctx, input):
|
|
||||||
test_ctx.session.add(
|
|
||||||
test_ctx.tag_factory(names=['tag1'], category_name='meta'))
|
|
||||||
test_ctx.session.commit()
|
|
||||||
with pytest.raises(tags.InvalidTagNameError):
|
|
||||||
test_ctx.api.put(
|
|
||||||
test_ctx.context_factory(
|
|
||||||
input=input,
|
|
||||||
user=test_ctx.user_factory(rank='regular_user')),
|
|
||||||
'tag1')
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('dup_name', ['tag1', 'TAG1', 'tag2', 'TAG2'])
|
@pytest.mark.parametrize('dup_name', ['tag1', 'TAG1', 'tag2', 'TAG2'])
|
||||||
def test_trying_to_use_existing_name(test_ctx, dup_name):
|
def test_trying_to_use_existing_name(test_ctx, dup_name):
|
||||||
test_ctx.session.add_all([
|
test_ctx.session.add_all([
|
||||||
|
@ -147,22 +175,6 @@ def test_trying_to_use_existing_name(test_ctx, dup_name):
|
||||||
user=test_ctx.user_factory(rank='regular_user')),
|
user=test_ctx.user_factory(rank='regular_user')),
|
||||||
'tag3')
|
'tag3')
|
||||||
|
|
||||||
def test_trying_to_update_tag_with_invalid_category(test_ctx):
|
|
||||||
test_ctx.session.add(
|
|
||||||
test_ctx.tag_factory(names=['tag1'], category_name='meta'))
|
|
||||||
test_ctx.session.commit()
|
|
||||||
with pytest.raises(tags.InvalidTagCategoryError):
|
|
||||||
test_ctx.api.put(
|
|
||||||
test_ctx.context_factory(
|
|
||||||
input={
|
|
||||||
'names': ['ok'],
|
|
||||||
'category': 'invalid',
|
|
||||||
'suggestions': [],
|
|
||||||
'implications': [],
|
|
||||||
},
|
|
||||||
user=test_ctx.user_factory(rank='regular_user')),
|
|
||||||
'tag1')
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('input,expected_suggestions,expected_implications', [
|
@pytest.mark.parametrize('input,expected_suggestions,expected_implications', [
|
||||||
# new relations
|
# new relations
|
||||||
({
|
({
|
||||||
|
@ -226,25 +238,6 @@ def test_reusing_suggestions_and_implications(test_ctx):
|
||||||
assert_relations(tag.suggestions, ['tag1'])
|
assert_relations(tag.suggestions, ['tag1'])
|
||||||
assert_relations(tag.implications, ['tag1'])
|
assert_relations(tag.implications, ['tag1'])
|
||||||
|
|
||||||
@pytest.mark.parametrize('input', [
|
|
||||||
{'names': ['ok'], 'suggestions': ['ok2', '!']},
|
|
||||||
{'names': ['ok'], 'implications': ['ok2', '!']},
|
|
||||||
])
|
|
||||||
def test_trying_to_update_tag_with_invalid_relation(test_ctx, input):
|
|
||||||
test_ctx.session.add(
|
|
||||||
test_ctx.tag_factory(names=['tag'], category_name='meta'))
|
|
||||||
test_ctx.session.commit()
|
|
||||||
with pytest.raises(tags.InvalidTagNameError):
|
|
||||||
test_ctx.api.put(
|
|
||||||
test_ctx.context_factory(
|
|
||||||
input=input, user=test_ctx.user_factory(rank='regular_user')),
|
|
||||||
'tag')
|
|
||||||
test_ctx.session.rollback()
|
|
||||||
assert get_tag(test_ctx.session, 'tag') is not None
|
|
||||||
assert get_tag(test_ctx.session, '!') is None
|
|
||||||
assert get_tag(test_ctx.session, 'ok') is None
|
|
||||||
assert get_tag(test_ctx.session, 'ok2') is None
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('input', [
|
@pytest.mark.parametrize('input', [
|
||||||
{
|
{
|
||||||
'names': ['tag1'],
|
'names': ['tag1'],
|
||||||
|
@ -263,7 +256,7 @@ def test_tag_trying_to_relate_to_itself(test_ctx, input):
|
||||||
test_ctx.session.add(
|
test_ctx.session.add(
|
||||||
test_ctx.tag_factory(names=['tag1'], category_name='meta'))
|
test_ctx.tag_factory(names=['tag1'], category_name='meta'))
|
||||||
test_ctx.session.commit()
|
test_ctx.session.commit()
|
||||||
with pytest.raises(tags.RelationError):
|
with pytest.raises(tags.InvalidTagRelationError):
|
||||||
test_ctx.api.put(
|
test_ctx.api.put(
|
||||||
test_ctx.context_factory(
|
test_ctx.context_factory(
|
||||||
input=input, user=test_ctx.user_factory(rank='regular_user')),
|
input=input, user=test_ctx.user_factory(rank='regular_user')),
|
||||||
|
|
|
@ -16,8 +16,8 @@ def test_ctx(
|
||||||
session, config_injector, context_factory, user_factory):
|
session, config_injector, context_factory, user_factory):
|
||||||
config_injector({
|
config_injector({
|
||||||
'secret': '',
|
'secret': '',
|
||||||
'user_name_regex': '.{3,}',
|
'user_name_regex': '[^!]{3,}',
|
||||||
'password_regex': '.{3,}',
|
'password_regex': '[^!]{3,}',
|
||||||
'default_rank': 'regular_user',
|
'default_rank': 'regular_user',
|
||||||
'thumbnails': {'avatar_width': 200, 'avatar_height': 200},
|
'thumbnails': {'avatar_width': 200, 'avatar_height': 200},
|
||||||
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
|
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
|
||||||
|
@ -150,34 +150,44 @@ def test_missing_optional_field(test_ctx, tmpdir, field):
|
||||||
'avatarStyle': 'manual',
|
'avatarStyle': 'manual',
|
||||||
}
|
}
|
||||||
del input[field]
|
del input[field]
|
||||||
test_ctx.api.post(
|
result = test_ctx.api.post(
|
||||||
test_ctx.context_factory(
|
test_ctx.context_factory(
|
||||||
input=input,
|
input=input,
|
||||||
files={'avatar': EMPTY_PIXEL},
|
files={'avatar': EMPTY_PIXEL},
|
||||||
user=test_ctx.user_factory(rank='mod')))
|
user=test_ctx.user_factory(rank='mod')))
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
@pytest.mark.parametrize('input', [
|
@pytest.mark.parametrize('input,expected_exception', [
|
||||||
{'name': '.'},
|
({'name': None}, users.InvalidUserNameError),
|
||||||
{'name': 'x' * 51},
|
({'name': ''}, users.InvalidUserNameError),
|
||||||
{'password': '.'},
|
({'name': '!bad'}, users.InvalidUserNameError),
|
||||||
{'rank': '.'},
|
({'name': 'x' * 51}, users.InvalidUserNameError),
|
||||||
{'email': '.'},
|
({'password': None}, users.InvalidPasswordError),
|
||||||
{'email': 'x' * 65},
|
({'password': ''}, users.InvalidPasswordError),
|
||||||
{'avatarStyle': 'manual'},
|
({'password': '!bad'}, users.InvalidPasswordError),
|
||||||
|
({'rank': None}, users.InvalidRankError),
|
||||||
|
({'rank': ''}, users.InvalidRankError),
|
||||||
|
({'rank': 'bad'}, 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_invalid_inputs(test_ctx, input):
|
def test_invalid_inputs(test_ctx, input, expected_exception):
|
||||||
user = test_ctx.user_factory(name='u1', rank='admin')
|
real_input={
|
||||||
test_ctx.session.add(user)
|
'name': 'chewie',
|
||||||
with pytest.raises(errors.ValidationError):
|
'email': 'asd@asd.asd',
|
||||||
real_input={
|
'password': 'oks',
|
||||||
'name': 'chewie',
|
}
|
||||||
'email': 'asd@asd.asd',
|
for key, value in input.items():
|
||||||
'password': 'oks',
|
real_input[key] = value
|
||||||
}
|
with pytest.raises(expected_exception):
|
||||||
for key, value in input.items():
|
|
||||||
real_input[key] = value
|
|
||||||
test_ctx.api.post(
|
test_ctx.api.post(
|
||||||
test_ctx.context_factory(input=real_input, user=user))
|
test_ctx.context_factory(
|
||||||
|
input=real_input,
|
||||||
|
user=test_ctx.user_factory(name='u1', rank='admin')))
|
||||||
|
|
||||||
def test_mods_trying_to_become_admin(test_ctx):
|
def test_mods_trying_to_become_admin(test_ctx):
|
||||||
user1 = test_ctx.user_factory(name='u1', rank='mod')
|
user1 = test_ctx.user_factory(name='u1', rank='mod')
|
||||||
|
|
|
@ -3,6 +3,11 @@ import pytest
|
||||||
from szurubooru import api, config, db, errors
|
from szurubooru import api, config, db, errors
|
||||||
from szurubooru.util import auth, misc, users
|
from szurubooru.util import auth, misc, 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'
|
||||||
|
|
||||||
def get_user(session, name):
|
def get_user(session, name):
|
||||||
return session.query(db.User).filter_by(name=name).first()
|
return session.query(db.User).filter_by(name=name).first()
|
||||||
|
|
||||||
|
@ -11,8 +16,8 @@ def test_ctx(
|
||||||
session, config_injector, context_factory, user_factory):
|
session, config_injector, context_factory, user_factory):
|
||||||
config_injector({
|
config_injector({
|
||||||
'secret': '',
|
'secret': '',
|
||||||
'user_name_regex': '.{3,}',
|
'user_name_regex': '^[^!]{3,}$',
|
||||||
'password_regex': '.{3,}',
|
'password_regex': '^[^!]{3,}$',
|
||||||
'thumbnails': {'avatar_width': 200, 'avatar_height': 200},
|
'thumbnails': {'avatar_width': 200, 'avatar_height': 200},
|
||||||
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
|
'ranks': ['anonymous', 'regular_user', 'mod', 'admin'],
|
||||||
'rank_names': {},
|
'rank_names': {},
|
||||||
|
@ -71,6 +76,29 @@ def test_updating_user(test_ctx):
|
||||||
assert auth.is_valid_password(user, 'oks') is True
|
assert auth.is_valid_password(user, 'oks') is True
|
||||||
assert auth.is_valid_password(user, 'invalid') is False
|
assert auth.is_valid_password(user, 'invalid') is False
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'field', ['name', 'email', 'password', 'rank', 'avatarStyle'])
|
||||||
|
def test_missing_optional_field(test_ctx, tmpdir, field):
|
||||||
|
config.config['data_dir'] = str(tmpdir.mkdir('data'))
|
||||||
|
config.config['data_url'] = 'http://example.com/data/'
|
||||||
|
user = test_ctx.user_factory(name='u1', rank='admin')
|
||||||
|
test_ctx.session.add(user)
|
||||||
|
input = {
|
||||||
|
'name': 'chewie',
|
||||||
|
'email': 'asd@asd.asd',
|
||||||
|
'password': 'oks',
|
||||||
|
'rank': 'mod',
|
||||||
|
'avatarStyle': 'gravatar',
|
||||||
|
}
|
||||||
|
del input[field]
|
||||||
|
result = test_ctx.api.put(
|
||||||
|
test_ctx.context_factory(
|
||||||
|
input=input,
|
||||||
|
files={'avatar': EMPTY_PIXEL},
|
||||||
|
user=user),
|
||||||
|
'u1')
|
||||||
|
assert result is not None
|
||||||
|
|
||||||
def test_update_changing_nothing(test_ctx):
|
def test_update_changing_nothing(test_ctx):
|
||||||
user = test_ctx.user_factory(name='u1', rank='admin')
|
user = test_ctx.user_factory(name='u1', rank='admin')
|
||||||
test_ctx.session.add(user)
|
test_ctx.session.add(user)
|
||||||
|
@ -93,19 +121,28 @@ def test_removing_email(test_ctx):
|
||||||
test_ctx.context_factory(input={'email': ''}, user=user), 'u1')
|
test_ctx.context_factory(input={'email': ''}, user=user), 'u1')
|
||||||
assert get_user(test_ctx.session, 'u1').email is None
|
assert get_user(test_ctx.session, 'u1').email is None
|
||||||
|
|
||||||
@pytest.mark.parametrize('input', [
|
@pytest.mark.parametrize('input,expected_exception', [
|
||||||
{'name': '.'},
|
({'name': None}, users.InvalidUserNameError),
|
||||||
{'name': 'x' * 51},
|
({'name': ''}, users.InvalidUserNameError),
|
||||||
{'password': '.'},
|
({'name': '!bad'}, users.InvalidUserNameError),
|
||||||
{'rank': '.'},
|
({'name': 'x' * 51}, users.InvalidUserNameError),
|
||||||
{'email': '.'},
|
({'password': None}, users.InvalidPasswordError),
|
||||||
{'email': 'x' * 65},
|
({'password': ''}, users.InvalidPasswordError),
|
||||||
{'avatarStyle': 'manual'},
|
({'password': '!bad'}, users.InvalidPasswordError),
|
||||||
|
({'rank': None}, users.InvalidRankError),
|
||||||
|
({'rank': ''}, users.InvalidRankError),
|
||||||
|
({'rank': 'bad'}, 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_invalid_inputs(test_ctx, input):
|
def test_invalid_inputs(test_ctx, input, expected_exception):
|
||||||
user = test_ctx.user_factory(name='u1', rank='admin')
|
user = test_ctx.user_factory(name='u1', rank='admin')
|
||||||
test_ctx.session.add(user)
|
test_ctx.session.add(user)
|
||||||
with pytest.raises(errors.ValidationError):
|
with pytest.raises(expected_exception):
|
||||||
test_ctx.api.put(
|
test_ctx.api.put(
|
||||||
test_ctx.context_factory(input=input, user=user), 'u1')
|
test_ctx.context_factory(input=input, user=user), 'u1')
|
||||||
|
|
||||||
|
@ -151,14 +188,10 @@ def test_uploading_avatar(test_ctx, tmpdir):
|
||||||
config.config['data_url'] = 'http://example.com/data/'
|
config.config['data_url'] = 'http://example.com/data/'
|
||||||
user = test_ctx.user_factory(name='u1', rank='mod')
|
user = test_ctx.user_factory(name='u1', rank='mod')
|
||||||
test_ctx.session.add(user)
|
test_ctx.session.add(user)
|
||||||
empty_pixel = \
|
|
||||||
b'\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x01\x00\x00\x00\x00' \
|
|
||||||
b'\xff\xff\xff\x21\xf9\x04\x01\x00\x00\x01\x00\x2c\x00\x00\x00\x00' \
|
|
||||||
b'\x01\x00\x01\x00\x00\x02\x02\x4c\x01\x00\x3b'
|
|
||||||
response = test_ctx.api.put(
|
response = test_ctx.api.put(
|
||||||
test_ctx.context_factory(
|
test_ctx.context_factory(
|
||||||
input={'avatarStyle': 'manual'},
|
input={'avatarStyle': 'manual'},
|
||||||
files={'avatar': empty_pixel},
|
files={'avatar': EMPTY_PIXEL},
|
||||||
user=user),
|
user=user),
|
||||||
'u1')
|
'u1')
|
||||||
user = get_user(test_ctx.session, 'u1')
|
user = get_user(test_ctx.session, 'u1')
|
||||||
|
|
|
@ -11,7 +11,7 @@ class TagAlreadyExistsError(errors.ValidationError): pass
|
||||||
class TagIsInUseError(errors.ValidationError): pass
|
class TagIsInUseError(errors.ValidationError): pass
|
||||||
class InvalidTagNameError(errors.ValidationError): pass
|
class InvalidTagNameError(errors.ValidationError): pass
|
||||||
class InvalidTagCategoryError(errors.ValidationError): pass
|
class InvalidTagCategoryError(errors.ValidationError): pass
|
||||||
class RelationError(errors.ValidationError): pass
|
class InvalidTagRelationError(errors.ValidationError): pass
|
||||||
|
|
||||||
def _verify_name_validity(name):
|
def _verify_name_validity(name):
|
||||||
name_regex = config.config['tag_name_regex']
|
name_regex = config.config['tag_name_regex']
|
||||||
|
@ -111,7 +111,7 @@ def update_category_name(tag, category_name):
|
||||||
tag.category = category
|
tag.category = category
|
||||||
|
|
||||||
def update_names(tag, names):
|
def update_names(tag, names):
|
||||||
names = misc.icase_unique(names)
|
names = misc.icase_unique([name for name in names if name])
|
||||||
if not len(names):
|
if not len(names):
|
||||||
raise InvalidTagNameError('At least one name must be specified.')
|
raise InvalidTagNameError('At least one name must be specified.')
|
||||||
for name in names:
|
for name in names:
|
||||||
|
@ -139,14 +139,14 @@ def update_names(tag, names):
|
||||||
|
|
||||||
def update_implications(tag, relations):
|
def update_implications(tag, relations):
|
||||||
if _check_name_intersection(_get_plain_names(tag), relations):
|
if _check_name_intersection(_get_plain_names(tag), relations):
|
||||||
raise RelationError('Tag cannot imply itself.')
|
raise InvalidTagRelationError('Tag cannot imply itself.')
|
||||||
related_tags, new_tags = get_or_create_tags_by_names(relations)
|
related_tags, new_tags = get_or_create_tags_by_names(relations)
|
||||||
db.session().flush()
|
db.session().flush()
|
||||||
tag.implications = related_tags + new_tags
|
tag.implications = related_tags + new_tags
|
||||||
|
|
||||||
def update_suggestions(tag, relations):
|
def update_suggestions(tag, relations):
|
||||||
if _check_name_intersection(_get_plain_names(tag), relations):
|
if _check_name_intersection(_get_plain_names(tag), relations):
|
||||||
raise RelationError('Tag cannot suggest itself.')
|
raise InvalidTagRelationError('Tag cannot suggest itself.')
|
||||||
related_tags, new_tags = get_or_create_tags_by_names(relations)
|
related_tags, new_tags = get_or_create_tags_by_names(relations)
|
||||||
db.session().flush()
|
db.session().flush()
|
||||||
tag.suggestions = related_tags + new_tags
|
tag.suggestions = related_tags + new_tags
|
||||||
|
|
|
@ -6,7 +6,7 @@ from szurubooru.util import auth, misc, files, images
|
||||||
|
|
||||||
class UserNotFoundError(errors.NotFoundError): pass
|
class UserNotFoundError(errors.NotFoundError): pass
|
||||||
class UserAlreadyExistsError(errors.ValidationError): pass
|
class UserAlreadyExistsError(errors.ValidationError): pass
|
||||||
class InvalidNameError(errors.ValidationError): pass
|
class InvalidUserNameError(errors.ValidationError): pass
|
||||||
class InvalidEmailError(errors.ValidationError): pass
|
class InvalidEmailError(errors.ValidationError): pass
|
||||||
class InvalidPasswordError(errors.ValidationError): pass
|
class InvalidPasswordError(errors.ValidationError): pass
|
||||||
class InvalidRankError(errors.ValidationError): pass
|
class InvalidRankError(errors.ValidationError): pass
|
||||||
|
@ -41,19 +41,23 @@ def create_user(name, password, email, auth_user):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
def update_name(user, name, auth_user):
|
def update_name(user, name, auth_user):
|
||||||
|
if not name:
|
||||||
|
raise InvalidUserNameError('Name cannot be empty.')
|
||||||
if misc.value_exceeds_column_size(name, db.User.name):
|
if misc.value_exceeds_column_size(name, db.User.name):
|
||||||
raise InvalidNameError('User name is too long.')
|
raise InvalidUserNameError('User name is too long.')
|
||||||
other_user = get_user_by_name(name)
|
other_user = get_user_by_name(name)
|
||||||
if other_user and other_user.user_id != auth_user.user_id:
|
if other_user and other_user.user_id != auth_user.user_id:
|
||||||
raise UserAlreadyExistsError('User %r already exists.' % name)
|
raise UserAlreadyExistsError('User %r already exists.' % name)
|
||||||
name = name.strip()
|
name = name.strip()
|
||||||
name_regex = config.config['user_name_regex']
|
name_regex = config.config['user_name_regex']
|
||||||
if not re.match(name_regex, name):
|
if not re.match(name_regex, name):
|
||||||
raise InvalidNameError(
|
raise InvalidUserNameError(
|
||||||
'User name %r must satisfy regex %r.' % (name, name_regex))
|
'User name %r must satisfy regex %r.' % (name, name_regex))
|
||||||
user.name = name
|
user.name = name
|
||||||
|
|
||||||
def update_password(user, password):
|
def update_password(user, password):
|
||||||
|
if not password:
|
||||||
|
raise InvalidPasswordError('Password cannot be empty.')
|
||||||
password_regex = config.config['password_regex']
|
password_regex = config.config['password_regex']
|
||||||
if not re.match(password_regex, password):
|
if not re.match(password_regex, password):
|
||||||
raise InvalidPasswordError(
|
raise InvalidPasswordError(
|
||||||
|
@ -62,7 +66,10 @@ def update_password(user, password):
|
||||||
user.password_hash = auth.get_password_hash(user.password_salt, password)
|
user.password_hash = auth.get_password_hash(user.password_salt, password)
|
||||||
|
|
||||||
def update_email(user, email):
|
def update_email(user, email):
|
||||||
email = email.strip() or None
|
if email:
|
||||||
|
email = email.strip()
|
||||||
|
if not email:
|
||||||
|
email = None
|
||||||
if email and misc.value_exceeds_column_size(email, db.User.email):
|
if email and misc.value_exceeds_column_size(email, db.User.email):
|
||||||
raise InvalidEmailError('Email is too long.')
|
raise InvalidEmailError('Email is too long.')
|
||||||
if not misc.is_valid_email(email):
|
if not misc.is_valid_email(email):
|
||||||
|
@ -70,6 +77,8 @@ def update_email(user, email):
|
||||||
user.email = email
|
user.email = email
|
||||||
|
|
||||||
def update_rank(user, rank, authenticated_user):
|
def update_rank(user, rank, authenticated_user):
|
||||||
|
if not rank:
|
||||||
|
raise InvalidRankError('Rank cannot be empty.')
|
||||||
rank = rank.strip()
|
rank = rank.strip()
|
||||||
available_ranks = config.config['ranks']
|
available_ranks = config.config['ranks']
|
||||||
if not rank in available_ranks:
|
if not rank in available_ranks:
|
||||||
|
|
Loading…
Reference in New Issue