Skip to content

Commit

Permalink
add json schema test
Browse files Browse the repository at this point in the history
  • Loading branch information
ginsstaahh committed Oct 19, 2018
1 parent de2a2a4 commit 81195ab
Show file tree
Hide file tree
Showing 9 changed files with 188 additions and 155 deletions.
71 changes: 68 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ Before you can install and run Bounce you'll need the following:
* Docker (see [Install Docker](https://docs.docker.com/install/))
* Docker Compose (see [Install Docker Compose](https://docs.docker.com/compose/install/))

For linux users, to use docker without using sudo for every command,
follow the steps in this link:
https://docs.docker.com/install/linux/linux-postinstall/#configuring-remote-access-with-systemd-unit-file
For linux users, there are options to make docker work better, for instance, configuring docker to be used without requiring superuser privileges. Follow the steps in this link:
(https://docs.docker.com/install/linux/linux-postinstall/)

### Configuration

Expand Down Expand Up @@ -180,6 +179,72 @@ The `__body__` field is used to specify the schema that the response body must m

Note that in this example our request resource contained only a schema for params, and our response resource contained only a schema for the body. If you like you can specify neither or both schemas for `__params__` and `__body__` on your resource class.

If you need to add an array type to the schema, specify the array's items with an `items` as shown below. The `items` key needs to be inside a parent key (such as `results`, but the name can whatever you'd like i.e. `sources`, `values`, etc.). The `items` and parent keys are used by the middleware code to correctly set defaults for the array:

```python
class SearchClubsResponse(metaclass=ResourceMeta):
"""Defines the schema for a search query response."""
__body__ = {
'results': {
'type': 'array',
'items': {
'type':
'object',
'required': [
'name', 'description', 'website_url', 'facebook_url',
'instagram_url', 'twitter_url', 'id', 'created_at'
],
'additionalProperties':
False,
'properties': {
'name': {
'type': 'string',
},
'description': {
'type': 'string',
},
'website_url': {
'type': 'string',
'default': 'no link',
},
'facebook_url': {
'type': 'string',
'default': 'no link',
},
'instagram_url': {
'type': 'string',
'default': 'no link',
},
'twitter_url': {
'type': 'string',
'default': 'no link',
},
'id': {
'type': 'integer',
'minimum': 0,
},
'created_at': {
'type': 'integer',
},
}
}
},
'resultCount': {
'type': 'integer',
'minimum': 0,
},
'page': {
'type': 'integer',
'minimum': 0,
},
'totalPages': {
'type': 'integer',
'minimum': 0,
}
}
```


**Step 2: Create a new Endpoint**

Now we create a new file in `bounce/server/api` called `users.py` and create a `UsersEndpoint` class in `users.py` that will contain all of our HTTP request handlers for the endpoint.
Expand Down
9 changes: 4 additions & 5 deletions bounce/server/api/clubs.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ class ClubsEndpoint(Endpoint):
@validate(PostClubsRequest, None)
async def post(self, request):
"""Handles a POST /clubs request by creating a new club."""
# Put the club in the DB
body = util.strip_whitespace(request.json)
try:
club.insert(
Expand All @@ -91,10 +90,10 @@ async def get(self, request):
clubs that contain content from the query."""

query = None
if ('query' in request):
query = request.get('query')
page = int(request.get('page'))
size = int(request.get('size'))
if 'query' in request.args:
query = request.args['query']
page = int(request.args['page'])
size = int(request.args['size'])
if size > MAX_SIZE:
raise APIError('size too high', status=400)

Expand Down
13 changes: 7 additions & 6 deletions bounce/server/api/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
from . import APIError, Endpoint, verify_token
from ...db import club, membership
from ..resource import validate
from ..resource.membership import (DeleteMembershipRequest,
GetMembershipRequest, GetMembershipResponse,
PutMembershipRequest)
from ..resource.membership import (
DeleteMembershipRequest, GetMembershipsRequest, GetMembershipsResponse,
PutMembershipRequest)


class MembershipEndpoint(Endpoint):
"""Handles requests to /memberships/<club_name>."""

__uri__ = "/memberships/<club_name:string>"

@validate(GetMembershipRequest, GetMembershipResponse)
@validate(GetMembershipsRequest, GetMembershipsResponse)
async def get(self, request, club_name):
"""
Handles a GET /memberships/<club_name>?user_id=<user_id> request
Expand All @@ -36,9 +36,10 @@ async def get(self, request, club_name):
raise APIError('No such club', status=404)

# Fetch the club's memberships
membership_info = membership.select(
results = membership.select(
self.server.db_session, club_name, user_id=user_id)
return response.json(membership_info, status=200)
info = {'results': results}
return response.json(info, status=200)

# pylint: disable=unused-argument
@verify_token()
Expand Down
61 changes: 36 additions & 25 deletions bounce/server/resource/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,39 @@ def validate(request_cls, response_cls):
body should match
"""

def set_defaults(schema, info, parent_key=None):
# if there is no value specified for the keys
# set the default values if there are
# corresponding default values found in the json schema
import pdb
pdb.set_trace()
def set_defaults(schema, info, parent_key=None, parent_schema=None):
"""Sets the default values for params and bodies"""
for key in schema:
value = schema[key]
# base case used for recursion
if key == 'default':
if parent_key not in info:
info.update({parent_key: value})
elif not info[parent_key]:
info.update({parent_key: value})
if isinstance(value, dict):
# need to make sure the key is in the schema,
# else we'd be putting in keys
# that are not valid properties
if parent_key in parent_schema:
# if the key is not specified or there's
# an empty string value for the key,
# update info with the default value
if parent_key not in info:
info[parent_key] = value
elif not info[parent_key]:
info[parent_key] = value
# recurse if there's more keys at the next level of the schema
if isinstance(value, dict) and key != 'items':
sub_schema = value
parent_key = key
set_defaults(sub_schema, info, parent_key)
set_defaults(sub_schema, info, parent_key, schema)
# if value is an array, then update each item in the array
# using the json schema
if value == 'array':
sub_schema = schema['items']['properties']
# get the items in the array
items = info[parent_key]
for item in items:
item = set_defaults(sub_schema, item, parent_key,
sub_schema)
# add updated items to info
info[parent_key] = items
return info

# pylint: disable=missing-docstring
Expand All @@ -98,14 +114,10 @@ async def wrapper(endpoint, request, *args, **kwargs):
# required schema
# Body values come as arrays of length 1 so turn
# them into single values
body_no_defaults = {
key: request.form.get(key)
for key in request.form
}
body = set_defaults(request_cls.__body__, body_no_defaults)
set_defaults(request_cls.__body__, request.json)
try:
jsonschema.validate(
body or {},
request.json or {},
request_cls.__body__,
format_checker=jsonschema.FormatChecker())
except jsonschema.ValidationError as err:
Expand All @@ -119,15 +131,12 @@ async def wrapper(endpoint, request, *args, **kwargs):
# required schema
# Params values always come as arrays of length 1 so turn
# them into single values
params_no_defaults = {
key: request.args.get(key)
for key in request.args
}
params = set_defaults(request_cls.__params__,
params_no_defaults)
for key in request.args:
request.args[key] = request.args.get(key)
set_defaults(request_cls.__params__, request.args)
try:
jsonschema.validate(
params,
request.args,
request_cls.__params__,
format_checker=jsonschema.FormatChecker())
except jsonschema.ValidationError as err:
Expand All @@ -144,6 +153,8 @@ async def wrapper(endpoint, request, *args, **kwargs):
# format and raise an error
body_no_defaults = json.loads(result.body)
body = set_defaults(response_cls.__body__, body_no_defaults)
# TODO: is this the right type of encoding
result.body = bytes(json.dumps(body), 'utf-8')
try:
jsonschema.validate(
body,
Expand Down
8 changes: 7 additions & 1 deletion bounce/server/resource/club.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ class PostClubsRequest(metaclass=ResourceMeta):
},
'website_url': {
'type': 'string',
'default': 'no link',
},
'facebook_url': {
'type': 'string',
Expand Down Expand Up @@ -86,12 +85,15 @@ class GetClubResponse(metaclass=ResourceMeta):
},
'facebook_url': {
'type': 'string',
'default': 'no link',
},
'instagram_url': {
'type': 'string',
'default': 'no link',
},
'twitter_url': {
'type': 'string',
'default': 'no link',
},
'id': {
'type': 'integer',
Expand Down Expand Up @@ -144,15 +146,19 @@ class SearchClubsResponse(metaclass=ResourceMeta):
},
'website_url': {
'type': 'string',
'default': 'no link',
},
'facebook_url': {
'type': 'string',
'default': 'no link',
},
'instagram_url': {
'type': 'string',
'default': 'no link',
},
'twitter_url': {
'type': 'string',
'default': 'no link',
},
'id': {
'type': 'integer',
Expand Down
50 changes: 26 additions & 24 deletions bounce/server/resource/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from . import ResourceMeta


class GetMembershipRequest(metaclass=ResourceMeta):
class GetMembershipsRequest(metaclass=ResourceMeta):
"""Defines the schema for a GET /membership/<club_name> request."""
__params__ = {
'type': 'object',
Expand All @@ -17,31 +17,33 @@ class GetMembershipRequest(metaclass=ResourceMeta):
}


class GetMembershipResponse(metaclass=ResourceMeta):
class GetMembershipsResponse(metaclass=ResourceMeta):
"""Defines the schema for a GET /membership/<club_name> response."""
__body__ = {
'type': 'array',
'items': {
'type': 'object',
'required': [
'user_id',
'created_at',
'full_name',
'username',
],
'properties': {
'user_id': {
'type': 'integer'
},
'created_at': {
'type': 'integer',
},
'full_name': {
'type': 'string',
},
'username': {
'type': 'string',
},
'results': {
'type': 'array',
'items': {
'type': 'object',
'required': [
'user_id',
'created_at',
'full_name',
'username',
],
'properties': {
'user_id': {
'type': 'integer'
},
'created_at': {
'type': 'integer',
},
'full_name': {
'type': 'string',
},
'username': {
'type': 'string',
},
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions tests/api/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,8 @@ def test_put_memberships__failure(server):
def test_get_memberships__success(server):
_, response = server.app.test_client.get('/memberships/newtest?user_id=2')
assert response.status == 200
assert len(response.json) == 1
membership = response.json[0]
assert len(response.json['results']) == 1
membership = response.json['results'][0]
assert membership['user_id'] == 2
assert membership['full_name'] == 'Test Guy'
assert membership['username'] == 'test'
Expand Down
Loading

0 comments on commit 81195ab

Please sign in to comment.