Skip to content

Commit

Permalink
Fix membership request handlers and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bfbachmann committed Sep 30, 2018
1 parent 7a7865b commit dca93a9
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 88 deletions.
7 changes: 1 addition & 6 deletions bounce/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,9 @@

from .server import Server
from .server.api.auth import LoginEndpoint
<<<<<<< HEAD
from .server.api.clubs import ClubEndpoint, ClubsEndpoint, SearchClubsEndpoint
from .server.api.membership import MembershipEndpoint
from .server.api.users import UserEndpoint, UserImagesEndpoint, UsersEndpoint
=======
from .server.api.clubs import ClubEndpoint, ClubsEndpoint
from .server.api.membership import MembershipEndpoint, MembershipsEndpoint
from .server.api.users import UserEndpoint, UsersEndpoint
>>>>>>> Fix DELETE membership
from .server.config import ServerConfig


Expand Down
2 changes: 1 addition & 1 deletion bounce/db/club.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import math

from sqlalchemy import Column, Integer, String, desc, func
from sqlalchemy.types import TIMESTAMP
from sqlalchemy.orm import relationship
from sqlalchemy.types import TIMESTAMP

Expand All @@ -15,6 +14,7 @@
# Used in the search method.
MAX_SIZE = 20


class Club(BASE):
"""
Specifies a mapping between a Club as a Python object and the Clubs table
Expand Down
25 changes: 16 additions & 9 deletions bounce/db/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from sqlalchemy.types import TIMESTAMP

from . import BASE
from .club import Club
from .user import User


class Membership(BASE):
Expand Down Expand Up @@ -42,10 +40,19 @@ def to_dict(self):
}


def insert(session, user_id, club_id):
"""Insert a new user into the Users table."""
membership = Membership(user_id=user_id, club_id=club_id)
session.add(membership)
def insert(session, club_name, user_id):
"""Creates a new membership that associates the given user with the given
club. """
# For now we do nothing on conflict, but when we have roles on these
# memberships we need to update on conflict.
query = f"""
INSERT INTO memberships (user_id, club_id) VALUES (
'{user_id}',
(SELECT id FROM clubs WHERE name = '{club_name}')
)
ON CONFLICT DO NOTHING
"""
session.execute(query)
session.commit()


Expand All @@ -67,9 +74,9 @@ def select(session, club_name, user_id=None):
result_proxy = session.execute(query)
results = []
for row in result_proxy.fetchall():
results.append({
key: row[i] for i, key in enumerate(result_proxy.keys())
})
results.append(
{key: row[i]
for i, key in enumerate(result_proxy.keys())})
return results


Expand Down
50 changes: 27 additions & 23 deletions bounce/server/api/membership.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
from sanic import response
from sqlalchemy.exc import IntegrityError

from . import APIError, Endpoint, util, verify_token
from . import APIError, Endpoint, verify_token
from ...db import club, membership
from ..resource import validate
from ..resource.membership import (DeleteMembershipRequest,
GetMembershipRequest, GetMembershipResponse,
PostMembershipRequest, PutMembershipRequest)
PutMembershipRequest)


class MembershipEndpoint(Endpoint):
Expand Down Expand Up @@ -40,22 +40,44 @@ async def get(self, request, club_name):
self.server.db_session, club_name, user_id=user_id)
return response.json(membership_info, status=200)

# pylint: disable=unused-argument
@verify_token()
@validate(PutMembershipRequest, None)
async def put(self, request, club_name, id_from_token=None):
"""Handles a PUT /memberships/<club_name>?user_id=<user_id> request by
creating or updating the membership for the given user and club."""
# Decode the club name
club_name = unquote(club_name)
try:
user_id = int(request.args.get('user_id'))
except Exception:
raise APIError('Invalid user ID', status=400)
try:
membership.insert(self.server.db_session, club_name, user_id)
except IntegrityError:
raise APIError('Invalid user or club ID', status=400)
return response.text('', status=201)

# pylint: enable=unused-argument

@verify_token()
@validate(DeleteMembershipRequest, None)
async def delete(self, request, club_name, id_from_token=None):
"""
Handles a DELETE /memberships/<club_name>?user_id=<user_id> request
by deleting the membership that associates the given user with the
given club. If no user ID is given, deletes all memberships for the
given club.
given club..
"""
# TODO: fix this when we have user roles set up. A user should only be
# able to delete their own memberships and memberships on clubs they
# are an admin/owner of.

# Decode the club name
club_name = unquote(club_name)
user_id = request.args.get('user_id', None)
try:
user_id = int(request.args.get('user_id'))
except ValueError:
raise APIError('Invalid user ID', status=400)

if id_from_token != user_id:
# Regular members can only delete their own memberships
Expand All @@ -69,21 +91,3 @@ async def delete(self, request, club_name, id_from_token=None):
# Delete the memberships
membership.delete(self.server.db_session, club_name, user_id=user_id)
return response.text('', status=204)


class MembershipsEndpoint(Endpoint):
"""Handles requests to /memberships."""

__uri__ = '/memberships'

@validate(PostMembershipRequest, None)
async def post(self, request):
"""Handles a POST /users request by creating a new user."""
body = request.json
# Put the membership in the DB
try:
membership.insert(self.server.db_session, body['user_id'],
body['club_id'])
except IntegrityError:
raise APIError('Invalid user or club ID', status=400)
return response.text('', status=201)
35 changes: 7 additions & 28 deletions bounce/server/resource/membership.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
'''Create a Membership resource in bounce/server/resource/membership.py that specifies all the information required for creating a new membership.'''
"""Resources for the /memberships endpoint."""

from . import ResourceMeta

Expand Down Expand Up @@ -47,47 +47,26 @@ class GetMembershipResponse(metaclass=ResourceMeta):
}


class PostMembershipRequest(metaclass=ResourceMeta):
"""Defines the schema for a POST /membership request."""
__body__ = {
'type': 'object',
'required': ['user_id', 'club_id'],
'additionalProperties': False,
'properties': {
'user_id': {
'type': 'integer',
'minimum': 0,
},
'club_id': {
'type': 'integer',
'minimum': 0,
},
}
}


class PutMembershipRequest(metaclass=ResourceMeta):
"""Defines the schema for a PUT /users/<username> request."""
__body__ = {
"""Defines the schema for a PUT /memberships/<club_name> request."""
__params__ = {
'type': 'object',
'additionalProperties': False,
'required': ['user_id'],
'properties': {
'user_id': {
'type': 'integer',
'minimum': 0,
},
'created_at': {
'type': 'integer',
'type': 'string',
},
}
}


class DeleteMembershipRequest(metaclass=ResourceMeta):
"""Defines the schema for a DELETE /members/<club_name> request."""
"""Defines the schema for a DELETE /memberships/<club_name> request."""
__params__ = {
'type': 'object',
'additionalProperties': False,
'required': ['user_id'],
'properties': {
'user_id': {
'type': 'string',
Expand Down
4 changes: 2 additions & 2 deletions schema/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ CREATE TABLE users (

DROP TABLE IF EXISTS memberships;
CREATE TABLE memberships (
user_id INT REFERENCES users(id) ON DELETE CASCADE,
club_id INT REFERENCES clubs(id) ON DELETE CASCADE,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE,
PRIMARY KEY (user_id, club_id),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT (now() at time zone 'utc')
);
3 changes: 1 addition & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ disable=
broad-except,
duplicate-code,
too-many-instance-attributes,
fixme,
trailing-whitespace
fixme
55 changes: 39 additions & 16 deletions tests/api/test_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,27 +308,50 @@ def test_delete_user_image__failure(server):
assert response.status == 403


def test_post_memberships__success(server):
def test_put_memberships__success(server):
_, response = server.app.test_client.post(
'/memberships',
'/users',
data=json.dumps({
'user_id': '384928',
'club_id': '2342',
'username': 'mrguy',
'full_name': 'Hello WOrld',
'email': 'something@anotherthing.com',
'password': 'Val1dPassword!'
}))
assert response.status == 201
token = util.create_jwt(2, server.config.secret)
_, response = server.app.test_client.put(
'/memberships/newtest?user_id=2', headers={'Authorization': token})
assert response.status == 201


def test_post_memberships__failure(server):
_, response = server.app.test_client.post(
'/clubs',
data=json.dumps({
'user_id': '12313',
'club_id': '212314',
}))
assert response.status == 409
assert 'error' in response.json
def test_put_memberships__failure(server):
token = util.create_jwt(2, server.config.secret)
_, response = server.app.test_client.put(
'/memberships/doesnotexist?user_id=2',
headers={'Authorization': token})
assert response.status == 400

def test_delete_membership_success(server):
_, response = server.app.test_client.delete('/memberships/test')
assert response.status == 204

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 membership['user_id'] == 2
assert membership['full_name'] == 'Test Guy'
assert membership['username'] == 'test'
assert isinstance(membership['created_at'], int)


def test_delete_membership__failure(server):
token = util.create_jwt(1, server.config.secret)
_, response = server.app.test_client.delete(
'/memberships/newtest?user_id=2', headers={'Authorization': token})
assert response.status == 403


def test_delete_membership__success(server):
token = util.create_jwt(2, server.config.secret)
_, response = server.app.test_client.delete(
'/memberships/newtest?user_id=2', headers={'Authorization': token})
assert response.status == 204
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from bounce.server.api.auth import LoginEndpoint
from bounce.server.api.clubs import (ClubEndpoint, ClubsEndpoint,
SearchClubsEndpoint)
from bounce.server.api.membership import MembershipEndpoint
from bounce.server.api.users import (UserEndpoint, UserImagesEndpoint,
UsersEndpoint)
from bounce.server.api.membership import MembershipEndpoint
from bounce.server.config import ServerConfig


Expand Down

0 comments on commit dca93a9

Please sign in to comment.