Skip to content

Commit

Permalink
Merge pull request #6 from awaisdar001/aj/address-feedback
Browse files Browse the repository at this point in the history
test: add more tests and code refactoring
  • Loading branch information
awaisdar001 authored Jan 17, 2022
2 parents 2d87524 + 98fa6e8 commit 1cfcec1
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 61 deletions.
24 changes: 17 additions & 7 deletions ReadMe.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
# Django Graphene (GraphQL) API
# Schedule Booking GraphQL API

Schedule Booking app is powered by a GraphQL API. GraphQL is a query language that allows clients to talk to an API server. Unlike REST, it gives the client control over how much or how little data they want to request about each object and allows relations within the object graph to be traversed easily.

To learn more about GraphQL language and its concepts, see the official [GraphQL website](https://graphql.org/).

The API endpoint is available at `/graphql/` and requires queries to be submitted using HTTP `POST` method and the `application/json` content type.

The API provides simple CURD operation. The application has simple data flow where
authenticated users can create their availabilities slots and other users can
book these slots. Other uses can also see all the bookings of a user.
CURD operations are provided on authenticated availability endpoint using `JWT` authentication
mechanism. API provides both types of operations:

* Public (search & book slots in availability.)
* Private (create/update/delete availabilities)

This is simple application which provided CURD operation. The application has simple
data flow where authenticated users can create their availabilities slots and
other users can book these slots. Other uses can also see all the bookings of a user.
CURD ops are provided on authenticated availability endpoint using `JWT` authentication
mechanism.

**Project Requirements:**

Expand All @@ -22,7 +32,7 @@ testing GraphQL queries. Follow the step by step guide below to run & test the G

<img width="1371" alt="Screen Shot 2021-12-24 at 3 25 55 AM" src="https://user-images.githubusercontent.com/4252738/147296260-1f2f256b-3cb7-4fe7-88b3-bc6121cfe7f5.png">

### Getting Started
### Development Setup
This project is created and tested with `Python 3.8.10`

#### Create & activate virtual environment.
Expand Down
17 changes: 17 additions & 0 deletions scheduler/api/schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import graphene
from graphql_auth.schema import UserQuery

from scheduler.meeting_scheduler.schema import (
AvailabilityQuery, BookingQuery, AvailabilityMutation, BookingMutation, UserMutation
)


class Query(BookingQuery, AvailabilityQuery, UserQuery):
pass


class Mutation(AvailabilityMutation, BookingMutation, UserMutation):
pass


schema = graphene.Schema(query=Query, mutation=Mutation)
78 changes: 66 additions & 12 deletions scheduler/api/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
"""
from datetime import time

from scheduler.meeting_scheduler.schema import schema
from .schema import schema
from scheduler.meeting_scheduler.tests import BaseTests


class BookingAPITests(BaseTests):
"""
Booking api tests.
"""

def setUp(self) -> None:
self.user = self.create_user(username="api-user")
self.availability = self.create_availability(self.user)
Expand All @@ -20,27 +21,55 @@ def setUp(self) -> None:
start_time=time(hour=11, minute=0, second=0),
total_time=15
)

self.booking_by_user_query = '''
query getUserBookings {
bookingsByUser(username: "api-user"){
query getUserBookings($username: String!) {
bookingsByUser(username: $username){
id user {
id username email
}
}
}
'''

@classmethod
def execute_and_assert_success(cls, query, **kwargs):
"""
Run the query and assert there were no errors.
"""
result = schema.execute(query, **kwargs)

assert result.errors is None, result.errors
return result.data

@classmethod
def execute_and_assert_error(cls, query, error, **kwargs):
"""
Run the query and assert there the expected error is raised.
"""
result = schema.execute(query, **kwargs)
assert result.errors is not None, "No errors while executing query!"
assert any(
[error in err.message for err in result.errors]
) is True, f'No error {error} instead {result.errors}'
return result.errors

def test_user_has_one_booking(self):
"""Test that get user booking api returns data."""
result = schema.execute(self.booking_by_user_query)
data = result.data
data = self.execute_and_assert_success(
self.booking_by_user_query,
variables={"username": "api-user"}
)

assert data is not None
assert len(data['bookingsByUser']) == 1

def test_user_booking_fields(self):
"""Test that get user booking api returns expected data."""
result = schema.execute(self.booking_by_user_query)
booking = result.data['bookingsByUser'][0]
booking = self.execute_and_assert_success(
self.booking_by_user_query,
variables={"username": "api-user"}
)['bookingsByUser'][0]

assert booking['id'] == f'{self.user_booking.id}'
assert booking['user'] == {'id': f'{self.user.id}', 'username': 'api-user', 'email': self.user.email}
Expand All @@ -50,17 +79,42 @@ def test_user_booking_additional_fields(self):
Tests that you can provide additional api key fields and the api
returns those additional fields.
"""
self.booking_by_user_query = '''
query getUserBookings {
bookingsByUser(username: "api-user"){
query = '''
query getUserBookings($username: String!) {
bookingsByUser(username: $username){
id fullName email date startTime endTime totalTime updatedAt
user {
id username email
}
}
}
'''
result = schema.execute(self.booking_by_user_query)
booking = result.data['bookingsByUser'][0]
booking = self.execute_and_assert_success(
query,
variables={"username": "api-user"}
)['bookingsByUser'][0]

for field in "id fullName email date startTime endTime totalTime updatedAt".split():
assert field in booking

def test_variable_error(self):
""""""
query = '''
query getUserBookings($username: String) {
bookingsByUser(username: $username){
id fullName email date startTime endTime totalTime updatedAt
user {
id username email
}
}
}
'''
expected_error = 'Variable "username" of type "String" used in position expecting type "String!".'
self.execute_and_assert_error(query=query, variables={"username": "api-user"}, error=expected_error)

def test_missing_variable(self):
"""
Test that missing variable error is raised when variable is not provided.
"""
expected_error = 'Variable "$username" of required type "String!" was not provided.'
self.execute_and_assert_error(self.booking_by_user_query, error=expected_error)
3 changes: 1 addition & 2 deletions scheduler/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
from django.views.decorators.csrf import csrf_exempt
from graphene_django.views import GraphQLView

from scheduler.meeting_scheduler.schema import schema
from .schema import schema

urlpatterns = [
path("graphql", csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema))),

]
3 changes: 2 additions & 1 deletion scheduler/meeting_scheduler/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from django.contrib import admin
from django.contrib.sessions.models import Session

from scheduler.meeting_scheduler.models import Booking, Availability, UserModel as User
from .models import Booking, Availability, UserModel as User


class SessionAdmin(admin.ModelAdmin):
"""Django session model admin """

def _session_data(self, obj):
"""Return decoded session data."""
return obj.get_decoded()
Expand Down
14 changes: 7 additions & 7 deletions scheduler/meeting_scheduler/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
import graphene
from graphql import GraphQLError

from scheduler.meeting_scheduler.decorators import user_required
from scheduler.meeting_scheduler.enums import Description
from scheduler.meeting_scheduler.models import Booking, UserModel as User, Availability
from scheduler.meeting_scheduler.nodes import BookingNode, AvailabilityNode
from .decorators import user_required
from .enums import Description
from .models import Booking, UserModel as User, Availability
from .types import BookingType, AvailabilityType


class CreateBooking(graphene.Mutation):
"""
OTD mutation class for creating bookings with users.
"""
booking = graphene.Field(BookingNode)
booking = graphene.Field(BookingType)
success = graphene.Boolean()

class Arguments:
Expand Down Expand Up @@ -50,7 +50,7 @@ class CreateAvailability(graphene.Mutation):
"""
OTD mutation class for creating user availabilities.
"""
availability = graphene.Field(AvailabilityNode)
availability = graphene.Field(AvailabilityType)
success = graphene.Boolean()
error = graphene.String()

Expand Down Expand Up @@ -78,7 +78,7 @@ class UpdateAvailability(graphene.Mutation):
"""
OTD mutation class for updating user availabilities.
"""
availability = graphene.Field(AvailabilityNode)
availability = graphene.Field(AvailabilityType)
success = graphene.Boolean()
error = graphene.String()

Expand Down
63 changes: 36 additions & 27 deletions scheduler/meeting_scheduler/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,39 @@
from graphql_auth.schema import UserQuery
from graphql_jwt.decorators import user_passes_test

from scheduler.meeting_scheduler.models import Booking, Availability
from scheduler.meeting_scheduler.mutations import (
from .models import Booking, Availability
from .mutations import (
CreateBooking, CreateAvailability, DeleteAvailability, UpdateAvailability,
)
from scheduler.meeting_scheduler.nodes import AvailabilityNode, BookingNode
from .types import AvailabilityType, BookingType


class Query(UserQuery, graphene.ObjectType):
class BookingQuery(graphene.ObjectType):
"""
Describes entry point for fields to *read* data in the booking Schema.
Describes entry point for fields to *read* data in the booking schema.
"""
availabilities = graphene.List(AvailabilityNode)
availability = graphene.Field(AvailabilityNode, id=graphene.Int(
required=True, description="ID of a availability to view"
))

bookings_by_user = graphene.List(
BookingNode,
username=graphene.Argument(
graphene.String, description="Pass username of the user.", required=True
),
BookingType,
username=graphene.String(required=True),
# Alternative
# username=graphene.Argument(graphene.String, description="Pass username of the user.", required=True),
)

@classmethod
def resolve_bookings_by_user(cls, root, info, username):
"""Resolve bookings by user"""
return Booking.objects.filter(user__username=username).prefetch_related('user')


class AvailabilityQuery(UserQuery, graphene.ObjectType):
"""
Describes entry point for fields to *read* data in the availability schema.
"""
availabilities = graphene.List(AvailabilityType)
availability = graphene.Field(AvailabilityType, id=graphene.Int(
required=True, description="ID of a availability to view"
))

@classmethod
@user_passes_test(lambda user: user and not user.is_anonymous)
def resolve_availabilities(cls, root, info):
Expand All @@ -38,27 +48,26 @@ def resolve_availability(cls, root, info, id):
"""Resolve the user availability field"""
return Availability.objects.get(id=id, user=info.context.user)

@classmethod
def resolve_bookings_by_user(cls, root, info, username):
"""Resolve bookings by user"""
return Booking.objects.filter(user__username=username).prefetch_related('user')

class BookingMutation(graphene.ObjectType):
"""
Describes entry point for fields to *create* data in bookings API.
"""
create_booking = CreateBooking.Field()

class Mutation(graphene.ObjectType):

class AvailabilityMutation(graphene.ObjectType):
"""
Describes entry point for fields to *create, update or delete* data in bookings API.
Describes entry point for fields to *create, update or delete* data in availability API.
"""
# Availability mutations
create_availability = CreateAvailability.Field()
update_availability = UpdateAvailability.Field()
delete_availability = DeleteAvailability.Field()

# Booking mutations
create_booking = CreateBooking.Field()

# User mutations
class UserMutation(graphene.ObjectType):
"""
Describes entry point for fields to *login, verify token* data in user API.
"""
login = mutations.ObtainJSONWebToken.Field(description="Login and obtain token for the user")
verify_token = mutations.VerifyToken.Field(description="Verify if the token is valid.")


schema = graphene.Schema(query=Query, mutation=Mutation)
2 changes: 1 addition & 1 deletion scheduler/meeting_scheduler/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from django.test import TestCase

from scheduler.meeting_scheduler.models import Booking, UserModel, Availability
from .models import Booking, UserModel, Availability


class BaseTests(TestCase):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""
Custom scheduler app nodes
"""

import graphene
from graphene_django import DjangoObjectType

from scheduler.meeting_scheduler.models import Booking, Availability, UserModel
from .models import Booking, Availability, UserModel


class UserType(DjangoObjectType):
Expand All @@ -16,7 +15,7 @@ class Meta:
fields = ("id", "username", "email")


class AvailabilityNode(DjangoObjectType):
class AvailabilityType(DjangoObjectType):
"""Availability Object Type Definition"""
id = graphene.ID()
interval_mints = graphene.String()
Expand All @@ -31,7 +30,7 @@ def resolve_interval_mints(cls, availability, info):
return availability.get_interval_mints_display()


class BookingNode(DjangoObjectType):
class BookingType(DjangoObjectType):
"""Booking Object Type Definition"""
id = graphene.ID()
user = graphene.Field(UserType)
Expand Down
Loading

0 comments on commit 1cfcec1

Please sign in to comment.