Drew Short
6 years ago
45 changed files with 1907 additions and 122 deletions
-
4.dockerignore
-
15.gitignore
-
31.gitlab-ci.yml
-
6CONTRIBUTING.md
-
23Dockerfile
-
33README.md
-
12server/.dockerignore
-
0server/CHANGELOG.md
-
4server/Dockerfile
-
25server/Pipfile
-
223server/Pipfile.lock
-
57server/README.md
-
4server/corvus/__init__.py
-
6server/corvus/api/authentication_api.py
-
74server/corvus/api/model.py
-
80server/corvus/api/user_api.py
-
11server/corvus/errors.py
-
2server/corvus/middleware/authentication_middleware.py
-
26server/corvus/service/authentication_service.py
-
117server/corvus/service/patch_service.py
-
22server/corvus/service/role_service.py
-
28server/corvus/service/transformation_service.py
-
89server/corvus/service/user_service.py
-
12server/corvus/service/user_token_service.py
-
170server/corvus/service/validation_service.py
-
4server/corvus/utility/json_utility.py
-
20server/corvus/utility/pagination_utility.py
-
1server/corvus/utility/session_utility.py
-
20server/documentation/Makefile
-
128server/documentation/api/authentication.rst
-
9server/documentation/api/index.rst
-
273server/documentation/api/user.rst
-
157server/documentation/conf.py
-
20server/documentation/index.rst
-
4server/documentation/introduction.rst
-
36server/documentation/make.bat
-
2server/manage.py
-
11server/run_tests.sh
-
2server/tests/api/test_authentication_api.py
-
109server/tests/api/test_user_api.py
-
2server/tests/conftest.py
-
87server/tests/service/test_patch_service.py
-
7server/tests/service/test_role_service.py
-
38server/tests/service/test_transformation_service.py
-
25server/tests/service/test_validation_service.py
@ -1,4 +0,0 @@ |
|||||
server/instance/ |
|
||||
server/setup.py |
|
||||
server/test/ |
|
||||
.admin_credentials |
|
@ -1,8 +1,11 @@ |
|||||
instance/ |
|
||||
.idea |
|
||||
*.iml |
*.iml |
||||
.admin_credentials |
|
||||
|
.idea/ |
||||
*__pycache__/ |
*__pycache__/ |
||||
.pytest_cache/ |
|
||||
.coverage |
|
||||
.mypy_cache/ |
|
||||
|
|
||||
|
# Atheneum Specific Ignores |
||||
|
/server/.admin_credentials |
||||
|
/server/.coverage |
||||
|
/server/.mypy_cache/ |
||||
|
/server/.pytest_cache/ |
||||
|
/server/documentation/_build/ |
||||
|
/server/instance/ |
@ -1,21 +1,32 @@ |
|||||
stages: |
stages: |
||||
- test |
- test |
||||
|
- deploy |
||||
|
|
||||
Corvus:Tests: |
|
||||
|
tests: |
||||
image: python:3.6-slim-stretch |
image: python:3.6-slim-stretch |
||||
stage: test |
stage: test |
||||
script: |
script: |
||||
- python3 --version |
|
||||
- python3 -m pip --version |
|
||||
- python3 -m pip install pipenv |
- python3 -m pip install pipenv |
||||
- python3 -m pipenv --version |
|
||||
- cd server |
- cd server |
||||
- pipenv install --dev --system |
- pipenv install --dev --system |
||||
- pylint corvus |
|
||||
- mypy corvus tests |
|
||||
- PYTHONPATH=$(pwd) coverage run --source corvus -m pytest |
|
||||
- coverage report --fail-under=85 -m --skip-covered |
|
||||
- pycodestyle corvus tests |
|
||||
- pydocstyle corvus |
|
||||
|
- bash ./run_tests.sh |
||||
tags: |
tags: |
||||
- docker |
- docker |
||||
|
|
||||
|
pages: |
||||
|
image: python:3.6-slim-stretch |
||||
|
stage: deploy |
||||
|
script: |
||||
|
- python3 -m pip install pipenv |
||||
|
- cd server |
||||
|
- pipenv install --dev --system |
||||
|
- cd documentation |
||||
|
- sphinx-build -M html "." "_build" |
||||
|
- mv _build/html/ ../../public/ |
||||
|
artifacts: |
||||
|
paths: |
||||
|
- public |
||||
|
tags: |
||||
|
- docker |
||||
|
only: |
||||
|
- master |
@ -0,0 +1,6 @@ |
|||||
|
# Contributing |
||||
|
|
||||
|
* Fork the repository |
||||
|
* Make changes |
||||
|
* Test everything |
||||
|
* Open issue and attach patchfile for changes |
@ -1,23 +0,0 @@ |
|||||
FROM python:3.6-slim-stretch |
|
||||
MAINTAINER Drew Short <warrick@sothr.com> |
|
||||
|
|
||||
ENV CORVUS_APP_DIRECTORY /opt/corvus |
|
||||
ENV CORVUS_CONFIG_DIRECTORY /srv/corvus/config |
|
||||
ENV CORVUS_DATA_DIRECTORY /srv/corvus/data |
|
||||
|
|
||||
RUN mkdir -p ${CORVUS_APP_DIRECTORY} \ |
|
||||
&& mkdir -p ${CORVUS_CONFIG_DIRECTORY} \ |
|
||||
&& mkdir -p ${CORVUS_DATA_DIRECTORY} \ |
|
||||
&& pip install pipenv gunicorn |
|
||||
|
|
||||
VOLUME ${CORVUS_CONFIG_DIRECTORY} |
|
||||
VOLUME ${CORVUS_DATA_DIRECTORY} |
|
||||
|
|
||||
COPY ./server/ ${CORVUS_APP_DIRECTORY}/ |
|
||||
|
|
||||
RUN cd ${CORVUS_APP_DIRECTORY} \ |
|
||||
&& pipenv install --system --deploy --ignore-pipfile |
|
||||
|
|
||||
WORKDIR ${CORVUS_APP_DIRECTORY} |
|
||||
|
|
||||
CMD ./entrypoint.sh |
|
@ -0,0 +1,33 @@ |
|||||
|
# Corvus [![pipeline status](https://gitlab.com/WarrickSothr/Corvus/badges/master/pipeline.svg)](https://gitlab.com/WarrickSothr/Corvus/commits/master) |
||||
|
|
||||
|
A python flask framework and web client. |
||||
|
|
||||
|
## Parts |
||||
|
|
||||
|
### Server |
||||
|
|
||||
|
The core API server |
||||
|
|
||||
|
More information available at server/README.md |
||||
|
|
||||
|
### Administration |
||||
|
|
||||
|
The administration SPA. |
||||
|
|
||||
|
More information available at administration/README.md |
||||
|
|
||||
|
## Release History |
||||
|
|
||||
|
## Changelog |
||||
|
|
||||
|
See: |
||||
|
* server/CHANGELOG.md |
||||
|
* administration/CHANGELOG.md |
||||
|
|
||||
|
## FAQ |
||||
|
|
||||
|
* TODO |
||||
|
|
||||
|
## Maintainers |
||||
|
|
||||
|
* Drew Short <warrick(AT)sothr(DOT)com> |
@ -0,0 +1,12 @@ |
|||||
|
.admin_credentials |
||||
|
.coverage |
||||
|
.pylintrc |
||||
|
mypy.ini |
||||
|
run_tests.sh |
||||
|
setup.py |
||||
|
test_settings.py |
||||
|
.mypy_cache/ |
||||
|
.pytest_cache/ |
||||
|
documentation/ |
||||
|
instance/ |
||||
|
tests/ |
@ -0,0 +1,57 @@ |
|||||
|
# Corvus Server |
||||
|
|
||||
|
## [API Documentation](https://warricksothr.gitlab.io/Corvus) |
||||
|
|
||||
|
## Requirements |
||||
|
|
||||
|
* Python 3.6 |
||||
|
* Pipenv |
||||
|
|
||||
|
## Installation |
||||
|
|
||||
|
```bash |
||||
|
git clone https://gitlab.com/WarrickSothr/Corvus.git |
||||
|
cd Corvus/server |
||||
|
pipenv install |
||||
|
pipenv shell |
||||
|
``` |
||||
|
|
||||
|
## Configuration |
||||
|
|
||||
|
## Running |
||||
|
|
||||
|
### Docker |
||||
|
|
||||
|
```bash |
||||
|
docker build -t corvus:local-test . |
||||
|
docker run -d corvus:local-test |
||||
|
``` |
||||
|
|
||||
|
### Local Development Version |
||||
|
|
||||
|
```bash |
||||
|
FLASK_APP=corvus:corvus flask db upgrade |
||||
|
python manage.py user register-admin |
||||
|
FLASK_APP=corvus:corvus flask run |
||||
|
``` |
||||
|
|
||||
|
## FAQ |
||||
|
|
||||
|
## Development |
||||
|
|
||||
|
```bash |
||||
|
pipenv install --dev |
||||
|
``` |
||||
|
|
||||
|
* Make changes |
||||
|
* Add/Update tests |
||||
|
|
||||
|
```bash |
||||
|
./run_tests |
||||
|
``` |
||||
|
|
||||
|
* If everything passes follow contributing guide. |
||||
|
|
||||
|
## Contributing |
||||
|
|
||||
|
See ../CONTRIBUTING.md |
@ -0,0 +1,117 @@ |
|||||
|
"""Patching support for db.Model objects.""" |
||||
|
from typing import Type, Set, Optional, Any, Dict |
||||
|
|
||||
|
from corvus import db |
||||
|
from corvus import errors |
||||
|
from corvus.model import User |
||||
|
from corvus.service import transformation_service |
||||
|
from corvus.service import validation_service |
||||
|
|
||||
|
|
||||
|
def get_patch_fields(patch_json: Dict[str, Any]) -> Set[str]: |
||||
|
"""Convert json fields to python fields.""" |
||||
|
return { |
||||
|
transformation_service.convert_key_from_json(key) for key in |
||||
|
patch_json.keys()} |
||||
|
|
||||
|
|
||||
|
def perform_patch(request_user: User, |
||||
|
original_model: Type[db.Model], |
||||
|
patch_model: Type[db.Model], |
||||
|
model_attributes: Set[str], |
||||
|
patched_fields: Optional[Set[str]]) \ |
||||
|
-> Type[db.Model]: |
||||
|
""" |
||||
|
Patch changed attributes onto original model. |
||||
|
|
||||
|
:param request_user: |
||||
|
:param original_model: The model to apply the patches to |
||||
|
:param patch_model: The model to pull the patch information from |
||||
|
:param model_attributes: The attributes that are valid for patching |
||||
|
:param patched_fields: The explicitly passed fields for patching |
||||
|
:return: Thd patched original_model |
||||
|
""" |
||||
|
change_set = validation_service.determine_change_set( |
||||
|
original_model, patch_model, model_attributes, patched_fields) |
||||
|
model_validation = validation_service.validate_model( |
||||
|
request_user, original_model, change_set) |
||||
|
if model_validation.success: |
||||
|
for attribute, value in change_set.items(): |
||||
|
setattr(original_model, attribute, value) |
||||
|
db.session.commit() |
||||
|
else: |
||||
|
raise errors.ValidationError( |
||||
|
'Restricted attributes modified. Invalid Patch Set.') |
||||
|
return original_model |
||||
|
|
||||
|
|
||||
|
def versioning_aware_patch(request_user: User, |
||||
|
original_model: Type[db.Model], |
||||
|
patch_model: Type[db.Model], |
||||
|
model_attributes: Set[str], |
||||
|
patched_fields: Optional[Set[str]]) \ |
||||
|
-> Type[db.Model]: |
||||
|
""" |
||||
|
Account for version numbers in the model. |
||||
|
|
||||
|
Versions must match to perform the patching. Otherwise a simultaneous edit |
||||
|
error has occurred. If the versions match and the patch moves forward, bump |
||||
|
the version on the model by 1 to prevent other reads from performing a |
||||
|
simultaneous edit. |
||||
|
|
||||
|
:param patched_fields: |
||||
|
:param request_user: |
||||
|
:param original_model: The model to apply the patches to |
||||
|
:param patch_model: The model to pull the patch information from |
||||
|
:param model_attributes: The attributes that are valid for patching |
||||
|
:return: Thd patched original_model |
||||
|
""" |
||||
|
if original_model.version == patch_model.version: |
||||
|
patch_model.version = patch_model.version + 1 |
||||
|
return perform_patch( |
||||
|
request_user, |
||||
|
original_model, |
||||
|
patch_model, |
||||
|
model_attributes, |
||||
|
patched_fields) |
||||
|
raise errors.ValidationError( |
||||
|
'Versions do not match. Concurrent edit in progress.') |
||||
|
|
||||
|
|
||||
|
def patch( |
||||
|
request_user: User, |
||||
|
original_model: Type[db.Model], |
||||
|
patch_model: Type[db.Model], |
||||
|
patched_fields: Optional[Set[str]] = None) -> Type[db.Model]: |
||||
|
""" |
||||
|
Patch the original model with the patch model data. |
||||
|
|
||||
|
:param request_user: |
||||
|
:param original_model: The model to apply the patches to |
||||
|
:param patch_model: The model to pull the patch information from |
||||
|
:param patched_fields: |
||||
|
:return: The patched original_model |
||||
|
""" |
||||
|
if type(original_model) is type(patch_model): |
||||
|
model_attributes = validation_service.get_changable_attribute_names( |
||||
|
original_model) |
||||
|
if patch_model.id is not None and original_model.id != patch_model.id: |
||||
|
raise errors.ValidationError('Cannot change ids through patching') |
||||
|
if 'version' in model_attributes: |
||||
|
return versioning_aware_patch( |
||||
|
request_user, |
||||
|
original_model, |
||||
|
patch_model, |
||||
|
model_attributes, |
||||
|
patched_fields) |
||||
|
return perform_patch( |
||||
|
request_user, |
||||
|
original_model, |
||||
|
patch_model, |
||||
|
model_attributes, |
||||
|
patched_fields) |
||||
|
raise errors.ValidationError( |
||||
|
'Model types "{}" and "{}" do not match'.format( |
||||
|
original_model.__class__.__name__, |
||||
|
patch_model.__class__.__name__ |
||||
|
)) |
@ -0,0 +1,170 @@ |
|||||
|
"""Validation service for Corvus models.""" |
||||
|
|
||||
|
from typing import Type, Dict, Callable, Any, Set, Optional, Tuple |
||||
|
|
||||
|
from sqlalchemy import orm |
||||
|
|
||||
|
from corvus import db, errors |
||||
|
from corvus.model import User |
||||
|
|
||||
|
_changable_attribute_names: Dict[str, Set[str]] = {} |
||||
|
|
||||
|
|
||||
|
def get_changable_attribute_names(model: Type[db.Model]) -> Set[str]: |
||||
|
""" |
||||
|
Retrieve columns from a SQLAlchemy model. |
||||
|
|
||||
|
Caches already seen models to improve performance. |
||||
|
|
||||
|
:param model: |
||||
|
:return: A list of changeable model attribute names |
||||
|
""" |
||||
|
class_name = model.__class__.__name__ |
||||
|
if class_name in _changable_attribute_names: |
||||
|
return _changable_attribute_names[class_name] |
||||
|
|
||||
|
model_attributes = {prop.key for prop in |
||||
|
orm.class_mapper(model.__class__).iterate_properties |
||||
|
if isinstance(prop, orm.ColumnProperty)} |
||||
|
_changable_attribute_names[class_name] = model_attributes |
||||
|
return model_attributes |
||||
|
|
||||
|
|
||||
|
def determine_change_set(original_model: Type[db.Model], |
||||
|
update_model: Type[db.Model], |
||||
|
model_attributes: Set[str], |
||||
|
options: Optional[Set[str]]) -> Dict[str, Any]: |
||||
|
""" |
||||
|
Determine the change set for two models. |
||||
|
|
||||
|
:param options: |
||||
|
:param original_model: |
||||
|
:param update_model: |
||||
|
:param model_attributes: |
||||
|
:return: |
||||
|
""" |
||||
|
if options is None: |
||||
|
options = model_attributes |
||||
|
else: |
||||
|
options = model_attributes.intersection(options) |
||||
|
change_set = {} |
||||
|
for attribute in options: |
||||
|
original_attribute = getattr(original_model, attribute) |
||||
|
changed_attribute = getattr(update_model, attribute) |
||||
|
if original_attribute != changed_attribute: |
||||
|
change_set[attribute] = changed_attribute |
||||
|
return change_set |
||||
|
|
||||
|
|
||||
|
class ModelValidationResult: # pylint: disable=too-few-public-methods |
||||
|
"""Result from model validation.""" |
||||
|
|
||||
|
field_results: Dict[str, Tuple[bool, str]] |
||||
|
success: bool |
||||
|
failed: Dict[str, str] = {} |
||||
|
|
||||
|
def __init__(self, field_results: Dict[str, Tuple[bool, str]]) -> None: |
||||
|
"""Initialize the validation results.""" |
||||
|
self.field_results = field_results |
||||
|
self.success = len( |
||||
|
[result for (result, _) in self.field_results.values() if |
||||
|
result is False]) == 0 |
||||
|
if not self.success: |
||||
|
failed = [(field, rslt[1]) for (field, rslt) in |
||||
|
self.field_results.items() if rslt[0] is False] |
||||
|
self.failed = {} |
||||
|
for field, reason in failed: |
||||
|
self.failed[field] = reason |
||||
|
|
||||
|
|
||||
|
def get_change_set_value( |
||||
|
change_set: Optional[Dict[str, Any]], field: str) -> Any: |
||||
|
"""Read a value or default from changeset.""" |
||||
|
if change_set is not None and field in change_set.keys(): |
||||
|
return change_set[field] |
||||
|
return None |
||||
|
|
||||
|
|
||||
|
class BaseValidator: |
||||
|
"""Base Model validator.""" |
||||
|
|
||||
|
type: Type[db.Model] |
||||
|
|
||||
|
def __init__(self, request_user: User, model: Type[db.Model]) -> None: |
||||
|
"""Initialize the base validator.""" |
||||
|
self.request_user = request_user |
||||
|
self._fields: Set[str] = get_changable_attribute_names(model) |
||||
|
self.model = model |
||||
|
|
||||
|
def validate(self, |
||||
|
change_set: Optional[Dict[str, Any]] = None) \ |
||||
|
-> ModelValidationResult: |
||||
|
"""Validate Model fields.""" |
||||
|
field_validators = self._validators() |
||||
|
fields_to_validate = self._fields |
||||
|
if change_set: |
||||
|
fields_to_validate = set(change_set.keys()) |
||||
|
validation_results: Dict[str, Tuple[bool, str]] = {} |
||||
|
for field in fields_to_validate: |
||||
|
if field not in field_validators: |
||||
|
raise errors.ValidationError( |
||||
|
'Invalid key: %r. Valid keys: %r.' % ( |
||||
|
field, list(sorted(field_validators.keys())))) |
||||
|
field_validator = field_validators[field] |
||||
|
field_result = field_validator( |
||||
|
get_change_set_value(change_set, field)) |
||||
|
validation_results[field] = field_result |
||||
|
return ModelValidationResult(validation_results) |
||||
|
|
||||
|
def _validators( |
||||
|
self) -> Dict[str, Callable[[Any], Tuple[bool, str]]]: |
||||
|
"""Field definitions.""" |
||||
|
raise NotImplementedError() |
||||
|
|
||||
|
@staticmethod |
||||
|
def no_validation(_new_value: Any) -> Tuple[bool, str]: |
||||
|
"""Perform no validation.""" |
||||
|
return True, '' |
||||
|
|
||||
|
def validate_version(self, new_version: Any) -> Tuple[bool, str]: |
||||
|
"""Perform a standard version validation.""" |
||||
|
if new_version is not None: |
||||
|
version_increasing = self.model.version <= new_version |
||||
|
if version_increasing: |
||||
|
return version_increasing, '' |
||||
|
return version_increasing, 'Unacceptable version change' |
||||
|
return True, '' |
||||
|
|
||||
|
|
||||
|
_model_validators: Dict[str, Type[BaseValidator]] = {} |
||||
|
|
||||
|
|
||||
|
def register_validator( |
||||
|
model_validator: Type[BaseValidator]) -> Type[BaseValidator]: |
||||
|
"""Add a model to the serializer mapping.""" |
||||
|
model_name = model_validator.type.__name__ |
||||
|
if model_name not in _model_validators: |
||||
|
_model_validators[model_name] = model_validator |
||||
|
else: |
||||
|
raise KeyError( |
||||
|
' '.join([ |
||||
|
'A validator for type "{}" already exists with class "{}".', |
||||
|
'Cannot register a new validator with class "{}"' |
||||
|
]).format( |
||||
|
model_name, |
||||
|
_model_validators[model_name].__name__, |
||||
|
model_validator.__name__)) |
||||
|
return model_validator |
||||
|
|
||||
|
|
||||
|
def validate_model(request_user: User, |
||||
|
model_obj: db.Model, |
||||
|
change_set: Optional[Dict[str, Any]] = None) \ |
||||
|
-> ModelValidationResult: |
||||
|
"""Lookup a Model and hand off to the validator.""" |
||||
|
try: |
||||
|
return _model_validators[type(model_obj).__name__]( |
||||
|
request_user, model_obj).validate(change_set) |
||||
|
except KeyError: |
||||
|
raise NotImplementedError( |
||||
|
'{} has no registered validator'.format(model_obj.__name__)) |
@ -0,0 +1,20 @@ |
|||||
|
"""Pagination utility functions.""" |
||||
|
from typing import Tuple |
||||
|
|
||||
|
from werkzeug.datastructures import MultiDict |
||||
|
|
||||
|
from corvus import errors |
||||
|
|
||||
|
|
||||
|
def get_pagination_params(request_args: MultiDict) -> Tuple[int, int]: |
||||
|
"""Get page and perPage request parameters.""" |
||||
|
page = request_args.get('page', 1) |
||||
|
per_page = request_args.get('perPage', 20) |
||||
|
try: |
||||
|
return int(page), int(per_page) |
||||
|
except ValueError: |
||||
|
raise errors.ClientError( |
||||
|
' '.join([ |
||||
|
'Invalid pagination parameters:', |
||||
|
'page={}', |
||||
|
'perPage={}']).format(page, per_page)) |
@ -0,0 +1,20 @@ |
|||||
|
# Minimal makefile for Sphinx documentation
|
||||
|
#
|
||||
|
|
||||
|
# You can set these variables from the command line.
|
||||
|
SPHINXOPTS = |
||||
|
SPHINXBUILD = sphinx-build |
||||
|
SPHINXPROJ = Corvus |
||||
|
SOURCEDIR = . |
||||
|
BUILDDIR = _build |
||||
|
|
||||
|
# Put it first so that "make" without argument is like "make help".
|
||||
|
help: |
||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) |
||||
|
|
||||
|
.PHONY: help Makefile |
||||
|
|
||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
|
%: Makefile |
||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) |
@ -0,0 +1,128 @@ |
|||||
|
Authentication API |
||||
|
================== |
||||
|
|
||||
|
.. http:post:: /auth/login |
||||
|
|
||||
|
Authenticate with the server and receive a userToken for requests. |
||||
|
|
||||
|
**Example request**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
POST /auth/login HTTP/1.1 |
||||
|
Host: example.tld |
||||
|
Accept: application/json |
||||
|
Authorization: Basic <Base64 Encoded Basic Auth> |
||||
|
|
||||
|
**Example response**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
HTTP/1.1 200 OK |
||||
|
Vary: Accept |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"creationTime": "2018-07-29T11:59:29-05:00", |
||||
|
"enabled": true, |
||||
|
"token": "b94cf5c7-cddc-4610-9d4c-6b8e04088ae8", |
||||
|
"version": 0 |
||||
|
} |
||||
|
|
||||
|
:<header Accept: Response content type depends on :mailheader:`Accept` header |
||||
|
:<header Authorization: Encoded basic authorization |
||||
|
:>header Content-Type: Depends on :mailheader:`Accept` header of request |
||||
|
:>json datetime creationTime: Creation time for the userToken |
||||
|
:>json datetime expirationTime: Expiration time for the userToken |
||||
|
:>json boolean enabled: Whether the userToken is enabled |
||||
|
:>json string token: UserToken to use for further authentication |
||||
|
:>json int version: Version for the object |
||||
|
:statuscode 200: User successfully logged in |
||||
|
:statuscode 401: Authorization failed |
||||
|
|
||||
|
.. http:post:: /auth/bump |
||||
|
|
||||
|
Bump user login information. |
||||
|
|
||||
|
**Example request**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
POST /auth/bump HTTP/1.1 |
||||
|
Host: example.tld |
||||
|
Accept: application/json |
||||
|
Authorization: Token <Base64(user:userToken)> |
||||
|
|
||||
|
**Example response**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
HTTP/1.1 200 OK |
||||
|
Vary: Accept |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"lastLoginTime": "2018-07-29T12:15:51-05:00" |
||||
|
} |
||||
|
|
||||
|
:<header Accept: Response content type depends on :mailheader:`Accept` header |
||||
|
:<header Authorization: Encoded token authorization |
||||
|
:>header Content-Type: Depends on :mailheader:`Accept` header of request |
||||
|
:>json datetime lastLoginTime: Updated lastLoginTime for the user |
||||
|
:statuscode 200: User last_login_time successfully bumped |
||||
|
:statuscode 401: Authorization failed |
||||
|
|
||||
|
.. http:post:: /auth/logout |
||||
|
|
||||
|
Logout a user and remove the provided userToken from valid tokens. |
||||
|
|
||||
|
**Example request**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
POST /auth/logout HTTP/1.1 |
||||
|
Host: example.tld |
||||
|
Accept: application/json |
||||
|
Authorization: Token <Base64(user:userToken)> |
||||
|
|
||||
|
**Example response**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
HTTP/1.1 200 OK |
||||
|
Vary: Accept |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"success": true |
||||
|
} |
||||
|
|
||||
|
:<header Accept: Response content type depends on :mailheader:`Accept` header |
||||
|
:<header Authorization: Rncoded token authorization |
||||
|
:>header Content-Type: Depends on :mailheader:`Accept` header of request |
||||
|
:>json boolean success: Whether the logout was successful |
||||
|
:statuscode 200: User successfully logged out |
||||
|
:statuscode 401: Authorization failed |
||||
|
|
||||
|
Authentication Object Models |
||||
|
============================ |
||||
|
|
||||
|
.. json:object:: UserToken |
||||
|
:showexample: |
||||
|
|
||||
|
UserToken definition |
||||
|
|
||||
|
:property token: The token value, used for authentication |
||||
|
:proptype token: string |
||||
|
:property note: Additional information about the token |
||||
|
:proptype note: string |
||||
|
:property enabled: Determine if a token will be accepted |
||||
|
:proptype enabled: boolean |
||||
|
:property expirationTime: The time that the token becomes invalid, regardless of enabled state |
||||
|
:proptype expirationTime: iso8601 |
||||
|
:property creationTime: The time that the token was created |
||||
|
:proptype creationTime: iso8601 |
||||
|
:property lastUsageTime: The time that the token was last used |
||||
|
:proptype lastUsageTime: iso8601 |
||||
|
:property version: An identifier for the token version |
||||
|
:proptype version: integer |
@ -0,0 +1,9 @@ |
|||||
|
Corvus API documentation |
||||
|
========================== |
||||
|
|
||||
|
.. toctree:: |
||||
|
:maxdepth: 2 |
||||
|
:caption: Contents: |
||||
|
|
||||
|
authentication |
||||
|
user |
@ -0,0 +1,273 @@ |
|||||
|
User API |
||||
|
======== |
||||
|
|
||||
|
.. http:get:: /user |
||||
|
|
||||
|
Get a page of users. |
||||
|
|
||||
|
**Example request** |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
GET /user HTTP/1.1 |
||||
|
Host: example.tld |
||||
|
Accept: application/json |
||||
|
Authorization: Token <Base64(user:userToken)> |
||||
|
|
||||
|
**Example response**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
HTTP/1.1 200 OK |
||||
|
Vary: Accept |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"page": 1, |
||||
|
"count": 1, |
||||
|
"totalCount": 1, |
||||
|
"lastPage": 1, |
||||
|
"items": [ |
||||
|
{ |
||||
|
"creationTime": "2018-07-29T11:58:17-05:00", |
||||
|
"lastLoginTime": "2018-07-29T12:43:27-05:00", |
||||
|
"name": "corvus_administrator", |
||||
|
"role": "ADMIN", |
||||
|
"version": 0 |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
|
||||
|
:query int page: User page to retrieve |
||||
|
:query int perPage: Number of records to retrieve per page (max 100) |
||||
|
:<header Accept: Response content type depends on :mailheader:`Accept` header |
||||
|
:<header Authorization: The encoded basic authorization |
||||
|
:>header Content-Type: Depends on :mailheader:`Accept` header of request |
||||
|
:>json int page: Page retrieved |
||||
|
:>json int count: Number of items returned |
||||
|
:>json int totalCount: Total number of items available |
||||
|
:>json int lastPage: Last page that can be requested before 404 |
||||
|
:>json int items: List of Users |
||||
|
:statuscode 200: Successfully retrieved the user |
||||
|
:statuscode 400: Invalid page or perPage values |
||||
|
:statuscode 401: Authorization failed |
||||
|
:statuscode 404: User page doesn't exist |
||||
|
|
||||
|
|
||||
|
.. http:get:: /user/(str:user_name) |
||||
|
|
||||
|
Find a user by name. |
||||
|
|
||||
|
**Example request**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
GET /user/corvus_administrator HTTP/1.1 |
||||
|
Host: example.tld |
||||
|
Accept: application/json |
||||
|
Authorization: Token <Base64(user:userToken)> |
||||
|
|
||||
|
**Example response**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
HTTP/1.1 200 OK |
||||
|
Vary: Accept |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"creationTime": "2018-07-29T11:58:17-05:00", |
||||
|
"lastLoginTime": "2018-07-29T12:43:27-05:00", |
||||
|
"name": "corvus_administrator", |
||||
|
"role": "ADMIN", |
||||
|
"version": 0 |
||||
|
} |
||||
|
|
||||
|
:param string user_name: Name of the user to retrieve information about |
||||
|
:<header Accept: Response content type depends on :mailheader:`Accept` header |
||||
|
:<header Authorization: The encoded basic authorization |
||||
|
:>header Content-Type: Depends on :mailheader:`Accept` header of request |
||||
|
:>json datetime creationTime: Creation time for the user |
||||
|
:>json datetime lastLoginTime: When the user last logged in, or was last bumped |
||||
|
:>json string name: The user name |
||||
|
:>json string role: The role assigned to the user |
||||
|
:>json int version: Version information |
||||
|
:statuscode 200: Successfully retrieved the user |
||||
|
:statuscode 401: Authorization failed |
||||
|
:statuscode 404: User doesn't exist |
||||
|
|
||||
|
.. http:patch:: /user/(str:user_name) |
||||
|
|
||||
|
Patch a user. |
||||
|
|
||||
|
**Example request**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
PATCH /user/corvus_administrator HTTP/1.1 |
||||
|
Host: example.tld |
||||
|
Accept: application/json |
||||
|
Authorization: Token <Base64(user:userToken)> |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"lastLoginTime": "2019-07-29T12:43:27-05:00", |
||||
|
"version": 0 |
||||
|
} |
||||
|
|
||||
|
**Example response**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
HTTP/1.1 200 OK |
||||
|
Vary: Accept |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"creationTime": "2018-07-29T11:58:17-05:00", |
||||
|
"lastLoginTime": "2019-07-29T12:43:27-05:00", |
||||
|
"name": "corvus_administrator", |
||||
|
"role": "ADMIN", |
||||
|
"version": 1 |
||||
|
} |
||||
|
|
||||
|
:param string user_name: Name of the user to update |
||||
|
:<header Accept: Response content type depends on :mailheader:`Accept` header |
||||
|
:<header Authorization: Encoded token authorization |
||||
|
:<header Content-Type: application/json |
||||
|
:<json datetime createDateTime: Update createDateTime (Administrator Only) |
||||
|
:<json datetime lastLoginTime: Update lastLoginTime |
||||
|
:<json string name: Update user name (Administrator Only) |
||||
|
:<json string role: Update user role (Must be less than or equal to the role authenticating the action) |
||||
|
:<json int version: Must match the latest version of the user |
||||
|
:>header Content-Type: Depends on :mailheader:`Accept` header of request |
||||
|
:>json datetime creationTime: Creation time for the user |
||||
|
:>json datetime lastLoginTime: When the user last logged in, or was last bumped |
||||
|
:>json string name: The user name |
||||
|
:>json string role: The role assigned to the user |
||||
|
:>json int version: Version information |
||||
|
:statuscode 200: Successfully patched the user |
||||
|
:statuscode 400: An issue in the payload was discovered |
||||
|
:statuscode 401: Authorization failed |
||||
|
:statuscode 404: User doesn't exist |
||||
|
|
||||
|
.. http:post:: /user |
||||
|
|
||||
|
Register a new user with the service. |
||||
|
|
||||
|
**Example request**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
POST /user HTTP/1.1 |
||||
|
Host: example.tld |
||||
|
Accept: application/json |
||||
|
Authorization: Token <Base64(user:userToken)> |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"name": "test_user", |
||||
|
"password": "JvZ9bm79", |
||||
|
"role": "USER" |
||||
|
} |
||||
|
|
||||
|
**Example response**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
HTTP/1.1 200 OK |
||||
|
Vary: Accept |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"creationTime": "2018-07-29T14:16:48-05:00", |
||||
|
"name": "test_user", |
||||
|
"role": "USER", |
||||
|
"version": 0 |
||||
|
} |
||||
|
|
||||
|
:<header Accept: Response content type depends on :mailheader:`Accept` header |
||||
|
:<header Authorization: Encoded token authorization |
||||
|
:<header Content-Type: application/json |
||||
|
:<json string name: Name of the user |
||||
|
:<json string password: Password to use |
||||
|
:<json string role: Role to assign to the user (Must be less than or equal to the role of the authenticating user) |
||||
|
:>header Content-Type: Depends on :mailheader:`Accept` header of request |
||||
|
:>json datetime creationTime: Datetime the user was created |
||||
|
:>json string name: Name of the created user |
||||
|
:>json string role: Role of the created user |
||||
|
:>json int version: Version number of the created user |
||||
|
:statuscode 200: Successfully registered the user |
||||
|
:statuscode 400: An issue in the payload was discovered |
||||
|
:statuscode 401: Authorization failed |
||||
|
|
||||
|
.. http:delete:: /user/(str:user_name) |
||||
|
|
||||
|
Register a new user with the service. |
||||
|
|
||||
|
**Example request**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
DELETE /user/test_user HTTP/1.1 |
||||
|
Host: example.tld |
||||
|
Accept: application/json |
||||
|
Authorization: Token <Base64(user:userToken)> |
||||
|
|
||||
|
**Example response**: |
||||
|
|
||||
|
.. sourcecode:: http |
||||
|
|
||||
|
HTTP/1.1 200 OK |
||||
|
Vary: Accept |
||||
|
Content-Type: application/json |
||||
|
|
||||
|
{ |
||||
|
"message": "Successfully Deleted", |
||||
|
"success": true |
||||
|
} |
||||
|
|
||||
|
:param string user_name: Name of the user to delete |
||||
|
:<header Accept: Response content type depends on :mailheader:`Accept` header |
||||
|
:<header Authorization: Encoded token authorization |
||||
|
:>header Content-Type: Depends on :mailheader:`Accept` header of request |
||||
|
:>json string message: Success or failure message |
||||
|
:>json boolean success: Action status indicator |
||||
|
:statuscode 200: Successfully deleted the user |
||||
|
:statuscode 401: Authorization failed |
||||
|
:statuscode 404: User doesn't exist |
||||
|
|
||||
|
User Object Models |
||||
|
================== |
||||
|
|
||||
|
.. json:object:: Page<User> |
||||
|
:showexample: |
||||
|
|
||||
|
Page<User> definition |
||||
|
|
||||
|
:property page: The page returned |
||||
|
:proptype page: integer |
||||
|
:property count: The number of items on this page |
||||
|
:proptype count: integer |
||||
|
:property totalCount: The total number of items available |
||||
|
:proptype totalCount: integer |
||||
|
:property lastPage: The last page that is accessible before 404 |
||||
|
:proptype lastPage: integer |
||||
|
:property items: The list of items on the page |
||||
|
:proptype items: :json:list: 'User' |
||||
|
|
||||
|
.. json:object:: User |
||||
|
:showexample: |
||||
|
|
||||
|
User definition |
||||
|
|
||||
|
:property name: The unique name of the user |
||||
|
:proptype name: string |
||||
|
:property creationTime: The time that the user was created |
||||
|
:proptype creationTime: iso8601 |
||||
|
:property lastLoginTime: The time that the user last logged in |
||||
|
:proptype lastLoginTime: iso8601 |
||||
|
:property version: An identifier for the user version |
||||
|
:proptype version: integer |
||||
|
:property role: The assigned role for the user |
||||
|
:proptype role: string |
@ -0,0 +1,157 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# |
||||
|
# Configuration file for the Sphinx documentation builder. |
||||
|
# |
||||
|
# This file does only contain a selection of the most common options. For a |
||||
|
# full list see the documentation: |
||||
|
# http://www.sphinx-doc.org/en/master/config |
||||
|
|
||||
|
# -- Path setup -------------------------------------------------------------- |
||||
|
|
||||
|
# If extensions (or modules to document with autodoc) are in another directory, |
||||
|
# add these directories to sys.path here. If the directory is relative to the |
||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here. |
||||
|
# |
||||
|
# import os |
||||
|
# import sys |
||||
|
# sys.path.insert(0, os.path.abspath('.')) |
||||
|
|
||||
|
|
||||
|
# -- Project information ----------------------------------------------------- |
||||
|
|
||||
|
project = 'Corvus' |
||||
|
copyright = '2018, Drew Short' |
||||
|
author = 'Drew Short' |
||||
|
|
||||
|
# The short X.Y version |
||||
|
version = '2018.8' |
||||
|
# The full version, including alpha/beta/rc tags |
||||
|
release = '2018.8.1' |
||||
|
|
||||
|
|
||||
|
# -- General configuration --------------------------------------------------- |
||||
|
|
||||
|
# If your documentation needs a minimal Sphinx version, state it here. |
||||
|
# |
||||
|
# needs_sphinx = '1.0' |
||||
|
|
||||
|
# Add any Sphinx extension module names here, as strings. They can be |
||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom |
||||
|
# ones. |
||||
|
extensions = [ |
||||
|
'sphinxcontrib.httpdomain', |
||||
|
'sphinxjsondomain' |
||||
|
] |
||||
|
|
||||
|
# Add any paths that contain templates here, relative to this directory. |
||||
|
templates_path = ['_templates'] |
||||
|
|
||||
|
# The suffix(es) of source filenames. |
||||
|
# You can specify multiple suffix as a list of string: |
||||
|
# |
||||
|
# source_suffix = ['.rst', '.md'] |
||||
|
source_suffix = '.rst' |
||||
|
|
||||
|
# The master toctree document. |
||||
|
master_doc = 'index' |
||||
|
|
||||
|
# The language for content autogenerated by Sphinx. Refer to documentation |
||||
|
# for a list of supported languages. |
||||
|
# |
||||
|
# This is also used if you do content translation via gettext catalogs. |
||||
|
# Usually you set "language" from the command line for these cases. |
||||
|
language = None |
||||
|
|
||||
|
# List of patterns, relative to source directory, that match files and |
||||
|
# directories to ignore when looking for source files. |
||||
|
# This pattern also affects html_static_path and html_extra_path . |
||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] |
||||
|
|
||||
|
# The name of the Pygments (syntax highlighting) style to use. |
||||
|
pygments_style = 'sphinx' |
||||
|
|
||||
|
|
||||
|
# -- Options for HTML output ------------------------------------------------- |
||||
|
|
||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for |
||||
|
# a list of builtin themes. |
||||
|
# |
||||
|
html_theme = 'sphinx_rtd_theme' |
||||
|
|
||||
|
# Theme options are theme-specific and customize the look and feel of a theme |
||||
|
# further. For a list of options available for each theme, see the |
||||
|
# documentation. |
||||
|
# |
||||
|
# html_theme_options = {} |
||||
|
|
||||
|
# Add any paths that contain custom static files (such as style sheets) here, |
||||
|
# relative to this directory. They are copied after the builtin static files, |
||||
|
# so a file named "default.css" will overwrite the builtin "default.css". |
||||
|
html_static_path = ['_static'] |
||||
|
|
||||
|
# Custom sidebar templates, must be a dictionary that maps document names |
||||
|
# to template names. |
||||
|
# |
||||
|
# The default sidebars (for documents that don't match any pattern) are |
||||
|
# defined by theme itself. Builtin themes are using these templates by |
||||
|
# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', |
||||
|
# 'searchbox.html']``. |
||||
|
# |
||||
|
# html_sidebars = {} |
||||
|
|
||||
|
|
||||
|
# -- Options for HTMLHelp output --------------------------------------------- |
||||
|
|
||||
|
# Output file base name for HTML help builder. |
||||
|
htmlhelp_basename = 'Corvusdoc' |
||||
|
|
||||
|
|
||||
|
# -- Options for LaTeX output ------------------------------------------------ |
||||
|
|
||||
|
latex_elements = { |
||||
|
# The paper size ('letterpaper' or 'a4paper'). |
||||
|
# |
||||
|
# 'papersize': 'letterpaper', |
||||
|
|
||||
|
# The font size ('10pt', '11pt' or '12pt'). |
||||
|
# |
||||
|
# 'pointsize': '10pt', |
||||
|
|
||||
|
# Additional stuff for the LaTeX preamble. |
||||
|
# |
||||
|
# 'preamble': '', |
||||
|
|
||||
|
# Latex figure (float) alignment |
||||
|
# |
||||
|
# 'figure_align': 'htbp', |
||||
|
} |
||||
|
|
||||
|
# Grouping the document tree into LaTeX files. List of tuples |
||||
|
# (source start file, target name, title, |
||||
|
# author, documentclass [howto, manual, or own class]). |
||||
|
latex_documents = [ |
||||
|
(master_doc, 'Corvus.tex', 'Corvus Documentation', |
||||
|
'Drew Short', 'manual'), |
||||
|
] |
||||
|
|
||||
|
|
||||
|
# -- Options for manual page output ------------------------------------------ |
||||
|
|
||||
|
# One entry per manual page. List of tuples |
||||
|
# (source start file, name, description, authors, manual section). |
||||
|
man_pages = [ |
||||
|
(master_doc, 'corvus', 'Corvus Documentation', |
||||
|
[author], 1) |
||||
|
] |
||||
|
|
||||
|
|
||||
|
# -- Options for Texinfo output ---------------------------------------------- |
||||
|
|
||||
|
# Grouping the document tree into Texinfo files. List of tuples |
||||
|
# (source start file, target name, title, author, |
||||
|
# dir menu entry, description, category) |
||||
|
texinfo_documents = [ |
||||
|
(master_doc, 'Corvus', 'Corvus Documentation', |
||||
|
author, 'Corvus', 'One line description of project.', |
||||
|
'Miscellaneous'), |
||||
|
] |
@ -0,0 +1,20 @@ |
|||||
|
.. Corvus documentation master file, created by |
||||
|
sphinx-quickstart on Sun Jul 29 11:09:44 2018. |
||||
|
You can adapt this file completely to your liking, but it should at least |
||||
|
contain the root `toctree` directive. |
||||
|
|
||||
|
Welcome to Corvus's documentation! |
||||
|
==================================== |
||||
|
|
||||
|
.. toctree:: |
||||
|
:maxdepth: 2 |
||||
|
:caption: Contents: |
||||
|
|
||||
|
introduction |
||||
|
api/index |
||||
|
|
||||
|
Indices and tables |
||||
|
================== |
||||
|
|
||||
|
* :ref:`genindex` |
||||
|
* :ref:`search` |
@ -0,0 +1,4 @@ |
|||||
|
Introduction To Corvus |
||||
|
======================== |
||||
|
|
||||
|
TODO |
@ -0,0 +1,36 @@ |
|||||
|
@ECHO OFF |
||||
|
|
||||
|
pushd %~dp0 |
||||
|
|
||||
|
REM Command file for Sphinx documentation |
||||
|
|
||||
|
if "%SPHINXBUILD%" == "" ( |
||||
|
set SPHINXBUILD=sphinx-build |
||||
|
) |
||||
|
set SOURCEDIR=. |
||||
|
set BUILDDIR=_build |
||||
|
set SPHINXPROJ=Corvus |
||||
|
|
||||
|
if "%1" == "" goto help |
||||
|
|
||||
|
%SPHINXBUILD% >NUL 2>NUL |
||||
|
if errorlevel 9009 ( |
||||
|
echo. |
||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx |
||||
|
echo.installed, then set the SPHINXBUILD environment variable to point |
||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you |
||||
|
echo.may add the Sphinx directory to PATH. |
||||
|
echo. |
||||
|
echo.If you don't have Sphinx installed, grab it from |
||||
|
echo.http://sphinx-doc.org/ |
||||
|
exit /b 1 |
||||
|
) |
||||
|
|
||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% |
||||
|
goto end |
||||
|
|
||||
|
:help |
||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% |
||||
|
|
||||
|
:end |
||||
|
popd |
@ -0,0 +1,87 @@ |
|||||
|
from datetime import datetime, timedelta |
||||
|
|
||||
|
import pytest |
||||
|
from mock import MagicMock, patch |
||||
|
|
||||
|
from corvus import errors |
||||
|
from corvus.model import UserToken, User |
||||
|
from corvus.service import patch_service, role_service |
||||
|
|
||||
|
service_module = 'corvus.service.patch_service' |
||||
|
|
||||
|
|
||||
|
@patch(service_module + '.db.session.commit') |
||||
|
def test_patch_models( |
||||
|
mock_db_session_commit: MagicMock): |
||||
|
request_user = User() |
||||
|
request_user.role = role_service.Role.ADMIN |
||||
|
|
||||
|
user = User() |
||||
|
user.name = 'TestUser' |
||||
|
user.version = 1 |
||||
|
user.last_login_time = datetime.now() - timedelta(days=1) |
||||
|
|
||||
|
user_patch = User() |
||||
|
user_patch.name = 'TestUser' |
||||
|
user_patch.version = 1 |
||||
|
user_patch.last_login_time = datetime.now() |
||||
|
|
||||
|
patched_user = patch_service.patch(request_user, user, user_patch) |
||||
|
assert patched_user.version > 1 |
||||
|
assert patched_user.last_login_time == user_patch.last_login_time |
||||
|
mock_db_session_commit.assert_called_once() |
||||
|
|
||||
|
|
||||
|
def test_patch_of_different_types(): |
||||
|
request_user = User() |
||||
|
request_user.role = role_service.Role.ADMIN |
||||
|
|
||||
|
user = User() |
||||
|
user_token = UserToken() |
||||
|
|
||||
|
with pytest.raises(errors.ValidationError) as error_info: |
||||
|
patch_service.patch(request_user, user, user_token) |
||||
|
|
||||
|
|
||||
|
def test_patch_different_ids(): |
||||
|
request_user = User() |
||||
|
request_user.role = role_service.Role.ADMIN |
||||
|
|
||||
|
user1 = User() |
||||
|
user1.id = 1 |
||||
|
|
||||
|
user2 = User() |
||||
|
user2.id = 2 |
||||
|
|
||||
|
with pytest.raises(errors.ValidationError) as error_info: |
||||
|
patch_service.patch(request_user, user1, user2) |
||||
|
|
||||
|
|
||||
|
def test_patch_different_versions(): |
||||
|
request_user = User() |
||||
|
request_user.role = role_service.Role.ADMIN |
||||
|
|
||||
|
user1 = User() |
||||
|
user1.version = 1 |
||||
|
|
||||
|
user2 = User() |
||||
|
user2.version = 2 |
||||
|
|
||||
|
with pytest.raises(errors.ValidationError) as error_info: |
||||
|
patch_service.patch(request_user, user1, user2) |
||||
|
|
||||
|
|
||||
|
def test_patch_restricted_attributes(): |
||||
|
request_user = User() |
||||
|
request_user.role = role_service.Role.USER |
||||
|
|
||||
|
user1 = User() |
||||
|
user1.version = 1 |
||||
|
user1.name = 'Bob' |
||||
|
|
||||
|
user2 = User() |
||||
|
user2.version = 1 |
||||
|
user2.name = 'Chris' |
||||
|
|
||||
|
with pytest.raises(errors.ValidationError) as error_info: |
||||
|
patch_service.patch(request_user, user1, user2) |
@ -0,0 +1,25 @@ |
|||||
|
from corvus.model import User |
||||
|
from corvus.service import role_service, validation_service |
||||
|
|
||||
|
|
||||
|
def test_successful_validation(): |
||||
|
request_user = User() |
||||
|
request_user.role = role_service.Role.USER |
||||
|
|
||||
|
user = User() |
||||
|
user.role = role_service.Role.USER |
||||
|
|
||||
|
validation_result = validation_service.validate_model(request_user, user) |
||||
|
assert validation_result.success |
||||
|
|
||||
|
|
||||
|
def test_failed_validation(): |
||||
|
request_user = User() |
||||
|
request_user.role = role_service.Role.ANONYMOUS |
||||
|
|
||||
|
user = User() |
||||
|
user.role = role_service.Role.USER |
||||
|
|
||||
|
validation_result = validation_service.validate_model(request_user, user) |
||||
|
assert validation_result.success is False |
||||
|
assert 'role' in validation_result.failed |
Write
Preview
Loading…
Cancel
Save
Reference in new issue