diff --git a/tests/app.py b/tests/app.py index b45507c..74e0f1a 100644 --- a/tests/app.py +++ b/tests/app.py @@ -1,9 +1,6 @@ """ SQLAlchemy JSONAPI Test App. -This app implements the backend of a web forum. It's rather simple but -provides us with a more comprehensive example for testing. - Colton Provias MIT License """ @@ -12,15 +9,13 @@ from flask import Flask, request from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import Boolean, Column, Enum, ForeignKey, Unicode, UnicodeText +from sqlalchemy import Boolean, Column, ForeignKey, Unicode, UnicodeText from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import backref, relationship, validates -from sqlalchemy_utils import (EmailType, IPAddressType, PasswordType, - Timestamp, URLType, UUIDType) - -import enum - -# ================================ APP CONFIG ================================ +from sqlalchemy_jsonapi import (INTERACTIVE_PERMISSIONS, Endpoint, + FlaskJSONAPI, Method, Permissions, + permission_test) +from sqlalchemy_utils import EmailType, PasswordType, Timestamp, UUIDType app = Flask(__name__) @@ -31,21 +26,29 @@ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://' app.config['SQLALCHEMY_ECHO'] = False -#api = FlaskJSONAPI(app, db) -# ================================== MODELS ================================== +class User(Timestamp, db.Model): + """Quick and dirty user model.""" - -class User(db.Model, Timestamp): + #: If __jsonapi_type__ is not provided, it will use the class name instead. __tablename__ = 'users' - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) + id = Column(UUIDType, default=uuid4, primary_key=True) + username = Column(Unicode(30), unique=True, nullable=False) email = Column(EmailType, nullable=False) - display_name = Column(Unicode(100), nullable=False) - password = Column(PasswordType(schemes=['bcrypt']), nullable=False) + password = Column( + PasswordType(schemes=['bcrypt']), + nullable=False) is_admin = Column(Boolean, default=False) - last_ip_address = Column(IPAddressType) - website = Column(URLType) + + @hybrid_property + def total_comments(self): + """ + Total number of comments. + + Provides an example of a computed property. + """ + return self.comments.count() @validates('email') def validate_email(self, key, email): @@ -53,95 +56,119 @@ def validate_email(self, key, email): assert '@' in email, 'Not an email' return email + @validates('username') + def validate_username(self, key, username): + """ + Check the length of the username. + + Here's hoping nobody submits something in unicode that is 31 characters + long!! + """ + assert len(username) >= 4 and len( + username) <= 30, 'Must be 4 to 30 characters long.' + return username + + @validates('password') + def validate_password(self, key, password): + """Validate a password's length.""" + assert len(password) >= 5, 'Password must be 5 characters or longer.' + return password + + @jsonapi_access(Permissions.VIEW, 'password') + def view_password(self): + """ Never let the password be seen. """ + return False + + @jsonapi_access(Permissions.EDIT) + def prevent_edit(self): + """ Prevent editing for no reason. """ + if request.view_args['api_type'] == 'blog-posts': + return True + return False + + @jsonapi_access(Permissions.DELETE) + def allow_delete(self): + """ Just like a popular social media site, we won't delete users. """ + return False + + +class BlogPost(Timestamp, db.Model): + """Post model, as if this is a blog.""" -class Forum(db.Model, Timestamp): - __tablename__ = 'forums' - - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) - name = Column(Unicode(255), nullable=False) - can_public_read = Column(Boolean, default=True) - can_public_write = Column(Boolean, default=True) - - -class Thread(db.Model, Timestamp): - __tablename__ = 'threads' - - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) - forum_id = Column(UUIDType, ForeignKey('forums.id'), nullable=False) - started_by_id = Column(UUIDType, ForeignKey('users.id'), nullable=False) - title = Column(Unicode(255), nullable=False) - - -class Post(db.Model, Timestamp): __tablename__ = 'posts' - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) - user_id = Column(UUIDType, ForeignKey('posts.id'), nullable=False) + id = Column(UUIDType, default=uuid4, primary_key=True) + title = Column(Unicode(100), nullable=False) + slug = Column(Unicode(100)) content = Column(UnicodeText, nullable=False) - is_removed = Column(Boolean, default=False) - + is_published = Column(Boolean, default=False) + author_id = Column(UUIDType, ForeignKey('users.id')) -class ReportTypes(enum.Enum): - USER = 0 - POST = 1 + author = relationship('User', + lazy='joined', + backref=backref('posts', lazy='dynamic')) + @validates('title') + def validate_title(self, key, title): + """Keep titles from getting too long.""" + assert len(title) >= 5 or len( + title) <= 100, 'Must be 5 to 100 characters long.' + return title -class Report(db.Model, Timestamp): - __tablename__ = 'reports' + @jsonapi_access(Permissions.VIEW) + def allow_view(self): + """ Hide unpublished. """ + return self.is_published - id = Column(UUIDType, default=uuid4, primary_key=True, nullable=False) - report_type = Column(Enum(ReportTypes), nullable=False) - reporter_id = Column(UUIDType, ForeignKey('users.id'), nullable=False) - complaint = Column(UnicodeText, nullable=False) + @jsonapi_access(INTERACTIVE_PERMISSIONS, 'logs') + def prevent_altering_of_logs(self): + return False - __mapper_args__ = { - 'polymorphic_identity': 'employee', - 'polymorphic_on': report_type, - 'with_polymorphic': '*' - } +class BlogComment(Timestamp, db.Model): + """Comment for each Post.""" -class UserReport(db.Model): - __tablename__ = 'user_reports' + __tablename__ = 'comments' - id = Column( - UUIDType, - ForeignKey('reports.id'), - default=uuid4, - primary_key=True, - nullable=False) - user_id = Column(UUIDType, ForeignKey('users.id'), nullable=False) - - __mapper_args__ = {'polymorphic_identity': ReportTypes.USER} + id = Column(UUIDType, default=uuid4, primary_key=True) + post_id = Column(UUIDType, ForeignKey('posts.id')) + author_id = Column(UUIDType, ForeignKey('users.id'), nullable=False) + content = Column(UnicodeText, nullable=False) + post = relationship('BlogPost', + lazy='joined', + backref=backref('comments', lazy='dynamic')) + author = relationship('User', + lazy='joined', + backref=backref('comments', + lazy='dynamic')) -class PostReport(db.Model): - __tablename__ = 'post_reports' - id = Column( - UUIDType, - ForeignKey('reports.id'), - default=uuid4, - primary_key=True, - nullable=False) - post_id = Column(UUIDType, ForeignKey('posts.id'), nullable=False) +class Log(Timestamp, db.Model): + __tablename__ = 'logs' + id = Column(UUIDType, default=uuid4, primary_key=True) + post_id = Column(UUIDType, ForeignKey('posts.id')) + user_id = Column(UUIDType, ForeignKey('users.id')) - __mapper_args__ = {'polymorphic_identity': ReportTypes.POST} + post = relationship('BlogPost', + lazy='joined', + backref=backref('logs', lazy='dynamic')) + user = relationship('User', + lazy='joined', + backref=backref('logs', lazy='dynamic')) -# ============================== EVENT HANDLERS ============================== + @jsonapi_access(INTERACTIVE_PERMISSIONS) + def block_interactive(cls): + return False -@app.before_request -def handle_auth(): - pass +api = FlaskJSONAPI(app, db) -# ============================== API OVERRIDES ============================== -#@api.wrap_handler(['blog-posts'], [Method.GET], [Endpoint.COLLECTION]) -#def sample_override(next, *args, **kwargs): -# return next(*args, **kwargs) +@api.wrap_handler(['blog-posts'], [Method.GET], [Endpoint.COLLECTION]) +def sample_override(next, *args, **kwargs): + return next(*args, **kwargs) -# ================================ APP RUNNER ================================ if __name__ == '__main__': app.run() diff --git a/tests/conftest.py b/tests/conftest.py index 67c026c..9e93e06 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,24 +7,18 @@ import json -import jsonschema import pytest -from addict import Dict -from faker import Faker from flask import Response from flask.testing import FlaskClient from sqlalchemy.orm import sessionmaker - from app import db as db_ -from app import app +from app import app, User, BlogPost, BlogComment, Log +from faker import Faker Session = sessionmaker() fake = Faker() -with open('tests/jsonapi_schema.json', 'r') as f: - api_schema = json.load(f) - @pytest.yield_fixture(scope='session') def flask_app(): @@ -63,9 +57,7 @@ def validate(self, status_code, error=None): assert self.status_code == status_code assert self.headers['Content-Type'] == 'application/vnd.api+json' if status_code != 204: - json_data = json.loads(self.data.decode()) - jsonschema.validate(json_data, api_schema) - self.json_data = Dict(json_data) + self.json_data = json.loads(self.data.decode()) if error: assert self.status_code == error.status_code assert self.json_data['errors'][0]['code'] == error.code @@ -77,7 +69,68 @@ def validate(self, status_code, error=None): @pytest.fixture def client(flask_app): """Set up the testing client.""" - with FlaskClient( - flask_app, use_cookies=True, - response_wrapper=TestingResponse) as c: + with FlaskClient(flask_app, + use_cookies=True, + response_wrapper=TestingResponse) as c: return c + + +@pytest.fixture +def user(session): + new_user = User(email=fake.email(), + password=fake.sentence(), + username=fake.user_name()) + session.add(new_user) + session.commit() + return new_user + + +@pytest.fixture +def post(user, session): + new_post = BlogPost(author=user, + title=fake.sentence(), + content=fake.paragraph(), + is_published=True) + session.add(new_post) + session.commit() + return new_post + + +@pytest.fixture +def unpublished_post(user, session): + new_post = BlogPost(author=user, + title=fake.sentence(), + content=fake.paragraph(), + is_published=False) + session.add(new_post) + session.commit() + return new_post + + +@pytest.fixture +def bunch_of_posts(user, session): + for x in range(30): + new_post = BlogPost(author=user, + title=fake.sentence(), + content=fake.paragraph(), + is_published=fake.boolean()) + session.add(new_post) + new_post.comments.append(BlogComment(author=user, + content=fake.paragraph())) + session.commit() + + +@pytest.fixture +def comment(user, post, session): + new_comment = BlogComment(author=user, post=post, content=fake.paragraph()) + session.add(new_comment) + session.commit() + return new_comment + + +@pytest.fixture +def log(user, post, session): + new_log = Log(user=user, post=post) + session.add(new_log) + session.commit() + return new_log diff --git a/tests/jsonapi_schema.json b/tests/jsonapi_schema.json deleted file mode 100644 index 32132bc..0000000 --- a/tests/jsonapi_schema.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "JSON API Schema", - "description": "This is a schema for responses in the JSON API format. For more, see http://jsonapi.org", - "oneOf": [ - { - "$ref": "#/definitions/success" - }, - { - "$ref": "#/definitions/failure" - }, - { - "$ref": "#/definitions/info" - } - ], - "definitions": { - "success": { - "type": "object", - "required": [ - "data" - ], - "properties": { - "data": { - "$ref": "#/definitions/data" - }, - "included": { - "description": "To reduce the number of HTTP requests, servers **MAY** allow responses that include related resources along with the requested primary resources. Such responses are called \"compound documents\".", - "type": "array", - "items": { - "$ref": "#/definitions/resource" - }, - "uniqueItems": true - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "links": { - "description": "Link members related to the primary data.", - "allOf": [ - { - "$ref": "#/definitions/links" - }, - { - "$ref": "#/definitions/pagination" - } - ] - }, - "jsonapi": { - "$ref": "#/definitions/jsonapi" - } - }, - "additionalProperties": false - }, - "failure": { - "type": "object", - "required": [ - "errors" - ], - "properties": { - "errors": { - "type": "array", - "items": { - "$ref": "#/definitions/error" - }, - "uniqueItems": true - }, - "meta": { - "$ref": "#/definitions/meta" - }, - "jsonapi": { - "$ref": "#/definitions/jsonapi" - } - }, - "additionalProperties": false - }, - "info": { - "type": "object", - "required": [ - "meta" - ], - "properties": { - "meta": { - "$ref": "#/definitions/meta" - }, - "links": { - "$ref": "#/definitions/links" - }, - "jsonapi": { - "$ref": "#/definitions/jsonapi" - } - }, - "additionalProperties": false - }, - "meta": { - "description": "Non-standard meta-information that can not be represented as an attribute or relationship.", - "type": "object", - "additionalProperties": true - }, - "data": { - "description": "The document's \"primary data\" is a representation of the resource or collection of resources targeted by a request.", - "oneOf": [ - { - "$ref": "#/definitions/resource" - }, - { - "description": "An array of resource objects, an array of resource identifier objects, or an empty array ([]), for requests that target resource collections.", - "type": "array", - "items": { - "$ref": "#/definitions/resource" - }, - "uniqueItems": true - }, - { - "description": "null if the request is one that might correspond to a single resource, but doesn't currently.", - "type": "null" - } - ] - }, - "resource": { - "description": "\"Resource objects\" appear in a JSON API document to represent resources.", - "type": "object", - "required": [ - "type", - "id" - ], - "properties": { - "type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "attributes": { - "$ref": "#/definitions/attributes" - }, - "relationships": { - "$ref": "#/definitions/relationships" - }, - "links": { - "$ref": "#/definitions/links" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - }, - "links": { - "description": "A resource object **MAY** contain references to other resource objects (\"relationships\"). Relationships may be to-one or to-many. Relationships can be specified by including a member in a resource's links object.", - "type": "object", - "properties": { - "self": { - "description": "A `self` member, whose value is a URL for the relationship itself (a \"relationship URL\"). This URL allows the client to directly manipulate the relationship. For example, it would allow a client to remove an `author` from an `article` without deleting the people resource itself.", - "type": "string", - "format": "uri" - }, - "related": { - "$ref": "#/definitions/link" - } - }, - "additionalProperties": true - }, - "link": { - "description": "A link **MUST** be represented as either: a string containing the link's URL or a link object.", - "oneOf": [ - { - "description": "A string containing the link's URL.", - "type": "string", - "format": "uri" - }, - { - "type": "object", - "required": [ - "href" - ], - "properties": { - "href": { - "description": "A string containing the link's URL.", - "type": "string", - "format": "uri" - }, - "meta": { - "$ref": "#/definitions/meta" - } - } - } - ] - }, - "attributes": { - "description": "Members of the attributes object (\"attributes\") represent information about the resource object in which it's defined.", - "type": "object", - "patternProperties": { - "^(?!relationships$|links$)\\w[-\\w_]*$": { - "description": "Attributes may contain any valid JSON value." - } - }, - "additionalProperties": false - }, - "relationships": { - "description": "Members of the relationships object (\"relationships\") represent references from the resource object in which it's defined to other resource objects.", - "type": "object", - "patternProperties": { - "^\\w[-\\w_]*$": { - "properties": { - "links": { - "$ref": "#/definitions/links" - }, - "data": { - "description": "Member, whose value represents \"resource linkage\".", - "oneOf": [ - { - "$ref": "#/definitions/relationshipToOne" - }, - { - "$ref": "#/definitions/relationshipToMany" - } - ] - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "relationshipToOne": { - "description": "References to other resource objects in a to-one (\"relationship\"). Relationships can be specified by including a member in a resource's links object.", - "anyOf": [ - { - "$ref": "#/definitions/empty" - }, - { - "$ref": "#/definitions/linkage" - } - ] - }, - "relationshipToMany": { - "description": "An array of objects each containing \"type\" and \"id\" members for to-many relationships.", - "type": "array", - "items": { - "$ref": "#/definitions/linkage" - }, - "uniqueItems": true - }, - "empty": { - "description": "Describes an empty to-one relationship.", - "type": "null" - }, - "linkage": { - "description": "The \"type\" and \"id\" to non-empty members.", - "type": "object", - "required": [ - "type", - "id" - ], - "properties": { - "type": { - "type": "string" - }, - "id": { - "type": "string" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - }, - "pagination": { - "type": "object", - "properties": { - "first": { - "description": "The first page of data", - "oneOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] - }, - "last": { - "description": "The last page of data", - "oneOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] - }, - "prev": { - "description": "The previous page of data", - "oneOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] - }, - "next": { - "description": "The next page of data", - "oneOf": [ - { - "type": "string", - "format": "uri" - }, - { - "type": "null" - } - ] - } - } - }, - "jsonapi": { - "description": "An object describing the server's implementation", - "type": "object", - "properties": { - "version": { - "type": "string" - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - }, - "error": { - "type": "object", - "properties": { - "id": { - "description": "A unique identifier for this particular occurrence of the problem.", - "type": "string" - }, - "links": { - "$ref": "#/definitions/links" - }, - "status": { - "description": "The HTTP status code applicable to this problem, expressed as a string value.", - "type": "string" - }, - "code": { - "description": "An application-specific error code, expressed as a string value.", - "type": "string" - }, - "title": { - "description": "A short, human-readable summary of the problem. It **SHOULD NOT** change from occurrence to occurrence of the problem, except for purposes of localization.", - "type": "string" - }, - "detail": { - "description": "A human-readable explanation specific to this occurrence of the problem.", - "type": "string" - }, - "source": { - "type": "object", - "properties": { - "pointer": { - "description": "A JSON Pointer [RFC6901] to the associated entity in the request document [e.g. \"/data\" for a primary data object, or \"/data/attributes/title\" for a specific attribute].", - "type": "string" - }, - "parameter": { - "description": "A string indicating which query parameter caused the error.", - "type": "string" - } - } - }, - "meta": { - "$ref": "#/definitions/meta" - } - }, - "additionalProperties": false - } - } -} \ No newline at end of file diff --git a/tests/test_collection_get.py b/tests/test_collection_get.py index 6a1bbed..9cc3903 100644 --- a/tests/test_collection_get.py +++ b/tests/test_collection_get.py @@ -1,43 +1,17 @@ -from schema import Schema -from jsonschema import validate +from sqlalchemy_jsonapi.errors import BadRequestError, NotSortableError -def test_200_with_no_querystring(): - pass - - -def describe_bad_query_params(): - def test_bad_query_param(): - pass - -def describe_resource_inclusions(): - pass - -def describe_sparse_fieldsets(): - pass - -def describe_sorting(): - pass - -def describe_pagination(): - pass - -def describe_filtering(): - pass - - -"""from sqlalchemy_jsonapi.errors import BadRequestError, NotSortableError - # TODO: Vanilla - def test_200_with_no_querystring(bunch_of_posts, client): response = client.get('/api/blog-posts').validate(200) assert response.json_data['data'][0]['type'] == 'blog-posts' assert response.json_data['data'][0]['id'] + # TODO: Bad Query Param + # TODO: Resource Inclusions @@ -55,7 +29,6 @@ def test_200_with_including_model_and_including_inbetween(bunch_of_posts, for data in response.json_data['included']: assert data['type'] in ['blog-posts', 'users'] - def test_200_with_multiple_includes(bunch_of_posts, client): response = client.get('/api/blog-posts/?include=comments,author').validate( 200) @@ -63,6 +36,7 @@ def test_200_with_multiple_includes(bunch_of_posts, client): for data in response.json_data['included']: assert data['type'] in ['blog-comments', 'users'] + # TODO: Sparse Fieldsets @@ -73,7 +47,6 @@ def test_200_with_single_field(bunch_of_posts, client): assert {'title'} == set(item['attributes'].keys()) assert len(item['relationships']) == 0 - def test_200_with_bad_field(bunch_of_posts, client): response = client.get( '/api/blog-posts/?fields[blog-posts]=titles').validate(200) @@ -104,6 +77,7 @@ def test_200_with_single_field_across_a_relationship(bunch_of_posts, client): assert len(item['attributes']) == 0 assert {'author'} == set(item['relationships'].keys()) + # TODO: Sorting @@ -133,6 +107,7 @@ def test_409_when_given_a_missing_field_for_sorting(bunch_of_posts, client): client.get('/api/blog-posts/?sort=never_gonna_give_you_up').validate( 409, NotSortableError) + # TODO: Pagination @@ -157,5 +132,5 @@ def test_400_when_provided_crap_data_for_pagination(bunch_of_posts, client): client.get('/api/blog-posts/?page[offset]=5&page[limit]=crap').validate( 400, BadRequestError) + # TODO: Filtering -""" diff --git a/tests/test_collection_post.py b/tests/test_collection_post.py index df30f27..d01dab6 100644 --- a/tests/test_collection_post.py +++ b/tests/test_collection_post.py @@ -1,4 +1,4 @@ -"""import json +import json from sqlalchemy_jsonapi.errors import ( InvalidTypeForEndpointError, MissingTypeError, PermissionDeniedError, @@ -10,7 +10,6 @@ # TODO: Bad query param - def test_200_resource_creation(client): payload = { 'data': { @@ -57,8 +56,8 @@ def test_200_resource_creation_with_relationships(user, client): assert response.json_data['data']['type'] == 'blog-posts' assert len(response.json_data['included']) == 0 post_id = response.json_data['data']['id'] - response = client.get('/api/blog-posts/{}/'.format(post_id)).validate(200) - + response = client.get('/api/blog-posts/{}/'.format( + post_id)).validate(200) def test_200_resource_creation_with_relationships_and_include(user, client): payload = { @@ -93,7 +92,6 @@ def test_200_resource_creation_with_relationships_and_include(user, client): response = client.get('/api/blog-posts/{}/?include=author'.format( post_id)).validate(200) - def test_200_resource_creation_with_sparse_fieldset(client): payload = { 'data': { @@ -110,19 +108,17 @@ def test_200_resource_creation_with_sparse_fieldset(client): data=json.dumps(payload), content_type='application/vnd.api+json').validate(201) assert response.json_data['data']['type'] == 'users' - assert set(response.json_data['data']['attributes'].keys()) == set( - ['username']) + assert set(response.json_data['data']['attributes'].keys()) == set(['username']) user_id = response.json_data['data']['id'] response = client.get('/api/users/{}/'.format(user_id)).validate(200) def test_403_when_access_is_denied(client): payload = {'data': {'type': 'logs'}} - client.post( - '/api/logs/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) + client.post('/api/logs/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) def test_409_when_id_already_exists(user, client): @@ -137,24 +133,23 @@ def test_409_when_id_already_exists(user, client): } } } - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_when_type_doesnt_match_endpoint(client): payload = {'data': {'type': 'blog-posts'}} - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 409, InvalidTypeForEndpointError) + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, InvalidTypeForEndpointError) def test_409_when_missing_content_type(client): - client.post( - '/api/users/', data='{}').validate(409, MissingContentTypeError) + client.post('/api/users/', + data='{}').validate(409, MissingContentTypeError) def test_409_when_missing_type(client): @@ -167,11 +162,10 @@ def test_409_when_missing_type(client): } } } - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, - MissingTypeError) + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, MissingTypeError) def test_409_for_invalid_value(client): @@ -185,10 +179,10 @@ def test_409_for_invalid_value(client): } } } - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_for_wrong_field_name(client): @@ -203,8 +197,7 @@ def test_409_for_wrong_field_name(client): } } } - client.post( - '/api/users/', - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) -""" + client.post('/api/users/', + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) diff --git a/tests/test_related_get.py b/tests/test_related_get.py index cf16a6c..7345174 100644 --- a/tests/test_related_get.py +++ b/tests/test_related_get.py @@ -1,4 +1,4 @@ -"""from uuid import uuid4 +from uuid import uuid4 from sqlalchemy_jsonapi.errors import (RelationshipNotFoundError, ResourceNotFoundError) @@ -33,4 +33,3 @@ def test_404_when_relationship_not_found(post, client): def test_404_when_resource_not_found(client): client.get('/api/blog-posts/{}/comments/'.format(uuid4())).validate( 404, ResourceNotFoundError) -""" diff --git a/tests/test_relationship_delete.py b/tests/test_relationship_delete.py index f3d0d13..04b07e1 100644 --- a/tests/test_relationship_delete.py +++ b/tests/test_relationship_delete.py @@ -1,4 +1,4 @@ -"""import json +import json from uuid import uuid4 from sqlalchemy_jsonapi.errors import ( @@ -28,38 +28,33 @@ def test_200_on_deletion_from_to_many(comment, client): def test_404_on_resource_not_found(client): - client.delete( - '/api/blog-posts/{}/relationships/comments/'.format(uuid4()), - data='{}', - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.delete('/api/blog-posts/{}/relationships/comments/'.format(uuid4()), + data='{}', + content_type='application/vnd.api+json').validate( + 404, ResourceNotFoundError) def test_404_on_relationship_not_found(post, client): - client.delete( - '/api/blog-posts/{}/relationships/comment/'.format(post.id), - data='{}', - content_type='application/vnd.api+json').validate( - 404, RelationshipNotFoundError) + client.delete('/api/blog-posts/{}/relationships/comment/'.format(post.id), + data='{}', + content_type='application/vnd.api+json').validate( + 404, RelationshipNotFoundError) def test_403_on_permission_denied(user, client): - client.delete( - '/api/users/{}/relationships/logs/'.format(user.id), - data='{"data": []}', - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) + client.delete('/api/users/{}/relationships/logs/'.format(user.id), + data='{"data": []}', + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) def test_409_on_to_one_provided(post, client): - client.delete( - '/api/blog-posts/{}/relationships/author/'.format(post.id), - data='{"data": {}}', - content_type='application/vnd.api+json').validate(409, ValidationError) + client.delete('/api/blog-posts/{}/relationships/author/'.format(post.id), + data='{"data": {}}', + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_missing_content_type_header(post, client): - client.delete( - '/api/blog-posts/{}/relationships/comment/'.format(post.id), - data='{}').validate(409, MissingContentTypeError) -""" + client.delete('/api/blog-posts/{}/relationships/comment/'.format(post.id), + data='{}').validate(409, MissingContentTypeError) diff --git a/tests/test_relationship_get.py b/tests/test_relationship_get.py index a996726..ef48592 100644 --- a/tests/test_relationship_get.py +++ b/tests/test_relationship_get.py @@ -1,4 +1,4 @@ -"""from sqlalchemy_jsonapi.errors import ( +from sqlalchemy_jsonapi.errors import ( RelationshipNotFoundError, ResourceNotFoundError, PermissionDeniedError) from uuid import uuid4 @@ -36,4 +36,3 @@ def test_404_on_relationship_not_found(post, client): def test_403_on_permission_denied(unpublished_post, client): client.get('/api/blog-posts/{}/relationships/comment/'.format( unpublished_post.id)).validate(403, PermissionDeniedError) -""" diff --git a/tests/test_relationship_patch.py b/tests/test_relationship_patch.py index 07e98ef..9c889ee 100644 --- a/tests/test_relationship_patch.py +++ b/tests/test_relationship_patch.py @@ -1,4 +1,4 @@ -"""import json +import json from uuid import uuid4 from sqlalchemy_jsonapi.errors import (PermissionDeniedError, @@ -52,75 +52,69 @@ def test_200_on_to_many_set_to_empty(post, client): def test_409_on_to_one_set_to_empty_list(post, client): payload = {'data': []} - client.patch( - '/api/blog-posts/{}/relationships/author/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_on_to_many_set_to_null(post, client): payload = {'data': None} - client.patch( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/relationships/comments/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_404_on_resource_not_found(client): - client.patch( - '/api/blog-posts/{}/relationships/comments/'.format(uuid4()), - data='{}', - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.patch('/api/blog-posts/{}/relationships/comments/'.format(uuid4()), + data='{}', + content_type='application/vnd.api+json').validate( + 404, ResourceNotFoundError) def test_404_on_relationship_not_found(client, post): - client.patch( - '/api/blog-posts/{}/relationships/comment/'.format(post.id), - data='{}', - content_type='application/vnd.api+json').validate( - 404, RelationshipNotFoundError) + client.patch('/api/blog-posts/{}/relationships/comment/'.format(post.id), + data='{}', + content_type='application/vnd.api+json').validate( + 404, RelationshipNotFoundError) def test_404_on_related_item_not_found(post, client): payload = {'data': [{'type': 'blog-comments', 'id': str(uuid4())}]} - client.patch( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.patch('/api/blog-posts/{}/relationships/comments/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 404, ResourceNotFoundError) def test_403_on_permission_denied(user, log, client): payload = {'data': {'type': 'users', 'id': str(user.id)}} - client.patch( - '/api/logs/{}/relationships/user/'.format(log.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) + client.patch('/api/logs/{}/relationships/user/'.format(log.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) def test_403_on_permission_denied_on_related(log, user, client): payload = {'data': {'type': 'logs', 'id': str(log.id)}} - client.patch( - '/api/users/{}/relationships/logs/'.format(user.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) + client.patch('/api/users/{}/relationships/logs/'.format(user.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) def test_409_on_to_one_with_incompatible_model(post, comment, client): payload = {'data': {'type': 'blog-comments', 'id': str(comment.id)}} - client.patch( - '/api/blog-posts/{}/relationships/author/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_on_to_many_with_incompatible_model(post, client): payload = {'data': [{'type': 'blog-posts', 'id': str(post.id)}]} - client.patch( - '/api/blog-posts/{}/relationships/author/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) -""" + client.patch('/api/blog-posts/{}/relationships/author/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) diff --git a/tests/test_relationship_post.py b/tests/test_relationship_post.py index 2c559fc..a771932 100644 --- a/tests/test_relationship_post.py +++ b/tests/test_relationship_post.py @@ -1,4 +1,4 @@ -"""import json +import json from uuid import uuid4 from sqlalchemy_jsonapi.errors import ValidationError, ResourceNotFoundError, RelationshipNotFoundError @@ -23,18 +23,18 @@ def test_200_on_to_many(comment, post, client): def test_409_on_hash_instead_of_array_provided(comment, post, client): payload = {'data': {'type': 'blog-comments', 'id': str(comment.id)}} - client.post( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.post('/api/blog-posts/{}/relationships/comments/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_on_incompatible_model(user, post, client): payload = {'data': [{'type': 'users', 'id': str(user.id)}]} - client.post( - '/api/blog-posts/{}/relationships/comments/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.post('/api/blog-posts/{}/relationships/comments/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_409_on_to_one_relationship(post, client): @@ -45,17 +45,14 @@ def test_409_on_to_one_relationship(post, client): def test_404_on_resource_not_found(client): - client.post( - '/api/blog-posts/{}/relationships/comments/'.format(uuid4()), - data='{}', - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.post('/api/blog-posts/{}/relationships/comments/'.format(uuid4()), + data='{}', + content_type='application/vnd.api+json').validate( + 404, ResourceNotFoundError) def test_404_on_relationship_not_found(post, client): - client.post( - '/api/blog-posts/{}/relationships/comment/'.format(post.id), - data='{}', - content_type='application/vnd.api+json').validate( - 404, RelationshipNotFoundError) -""" + client.post('/api/blog-posts/{}/relationships/comment/'.format(post.id), + data='{}', + content_type='application/vnd.api+json').validate( + 404, RelationshipNotFoundError) diff --git a/tests/test_resource_delete.py b/tests/test_resource_delete.py index ffc6f1b..a291c5e 100644 --- a/tests/test_resource_delete.py +++ b/tests/test_resource_delete.py @@ -1,8 +1,9 @@ -"""from uuid import uuid4 +from uuid import uuid4 from sqlalchemy_jsonapi.errors import ( PermissionDeniedError, ResourceNotFoundError, ResourceTypeNotFoundError) + # TODO: Bad query param @@ -25,4 +26,3 @@ def test_403_on_permission_denied(user, client): def test_404_on_resource_not_found(client): client.delete('/api/blog-comments/{}/'.format(uuid4())).validate( 404, ResourceNotFoundError) -""" diff --git a/tests/test_resource_get.py b/tests/test_resource_get.py index 3d680b3..30a2e49 100644 --- a/tests/test_resource_get.py +++ b/tests/test_resource_get.py @@ -1,6 +1,7 @@ -"""from sqlalchemy_jsonapi.errors import ResourceNotFoundError, PermissionDeniedError +from sqlalchemy_jsonapi.errors import ResourceNotFoundError, PermissionDeniedError from uuid import uuid4 + # TODO: Sparse Fieldsets # TODO: Related Includes # TODO: Bad query param @@ -73,4 +74,3 @@ def test_200_with_single_field_across_a_relationship(post, client): assert {'title', 'content'} == set(item['attributes'].keys()) assert len(item['attributes']) == 0 assert {'author'} == set(item['relationships'].keys()) -""" diff --git a/tests/test_resource_patch.py b/tests/test_resource_patch.py index c10aadf..d15a73a 100644 --- a/tests/test_resource_patch.py +++ b/tests/test_resource_patch.py @@ -1,9 +1,10 @@ -"""import json +import json from uuid import uuid4 from sqlalchemy_jsonapi.errors import (BadRequestError, PermissionDeniedError, ResourceNotFoundError, ValidationError) + # TODO: Sparse Fieldsets # TODO: Related Includes # TODO: Bad query param @@ -38,17 +39,16 @@ def test_200(client, post, user): def test_400_missing_type(post, client): - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps({}), - content_type='application/vnd.api+json').validate(400, BadRequestError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps({}), + content_type='application/vnd.api+json').validate( + 400, BadRequestError) def test_404_resource_not_found(client): - client.patch( - '/api/blog-posts/{}/'.format(uuid4()), - content_type='application/vnd.api+json', - data='{}').validate(404, ResourceNotFoundError) + client.patch('/api/blog-posts/{}/'.format(uuid4()), + content_type='application/vnd.api+json', + data='{}').validate(404, ResourceNotFoundError) def test_404_related_resource_not_found(client, post): @@ -66,11 +66,10 @@ def test_404_related_resource_not_found(client, post): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate( - 404, ResourceNotFoundError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 404, ResourceNotFoundError) def test_400_field_not_found(client, post, user): @@ -88,10 +87,10 @@ def test_400_field_not_found(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(400, BadRequestError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 400, BadRequestError) def test_409_type_mismatch_to_one(client, post, user): @@ -109,10 +108,10 @@ def test_409_type_mismatch_to_one(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_400_type_mismatch_to_many(client, post, user): @@ -130,10 +129,10 @@ def test_400_type_mismatch_to_many(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(400, BadRequestError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 400, BadRequestError) def test_409_validation_failed(client, post, user): @@ -154,10 +153,10 @@ def test_409_validation_failed(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(409, ValidationError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 409, ValidationError) def test_400_type_does_not_match_endpoint(client, post, user): @@ -178,16 +177,14 @@ def test_400_type_does_not_match_endpoint(client, post, user): } } } - client.patch( - '/api/blog-posts/{}/'.format(post.id), - data=json.dumps(payload), - content_type='application/vnd.api+json').validate(400, BadRequestError) + client.patch('/api/blog-posts/{}/'.format(post.id), + data=json.dumps(payload), + content_type='application/vnd.api+json').validate( + 400, BadRequestError) def test_403_permission_denied(user, client): - client.patch( - '/api/users/{}/'.format(user.id), - data='{}', - content_type='application/vnd.api+json').validate( - 403, PermissionDeniedError) -""" + client.patch('/api/users/{}/'.format(user.id), + data='{}', + content_type='application/vnd.api+json').validate( + 403, PermissionDeniedError) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 18a4506..38fe038 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,4 +1,4 @@ -"""from app import api +from app import api import uuid @@ -10,4 +10,3 @@ def test_include_different_types_same_id(session, comment): r = api.serializer.get_resource(session, {'include': 'post,author'}, 'blog-comments', comment.id) assert len(r.data['included']) == 2 -"""