Skip to content

Commit

Permalink
Add app layer tests for checkouts
Browse files Browse the repository at this point in the history
  • Loading branch information
subhashb committed Aug 19, 2024
1 parent 1314566 commit 492b104
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"module": "pytest",
"justMyCode": false,
"args": [
"tests/lending/model/bdd/holds/test_holds.py::test_regular_patron_tries_to_place_an_openended_hold",
"tests/lending/app/bdd/checkout_handlers/test_checkouts.py::test_patron_returns_a_book_after_the_due_date",
]
},
]
Expand Down
3 changes: 2 additions & 1 deletion src/lending/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from lending.app.dailysheet import DailySheet # isort:skip
from lending.app.patron.hold import CancelHold, PlaceHold # isort:skip
from lending.app.patron.checkout import CheckoutBook # isort:skip
from lending.app.patron.checkout import CheckoutBook, ReturnBook # isort:skip


__all__ = [
Expand All @@ -31,6 +31,7 @@
"HoldPlaced",
"Checkout",
"CheckoutBook",
"ReturnBook",
"CheckoutStatus",
"Book",
"BookStatus",
Expand Down
13 changes: 13 additions & 0 deletions src/lending/app/patron/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ class CheckoutBook:
branch_id = Identifier(required=True)


@lending.command(part_of="Patron")
class ReturnBook:
patron_id = Identifier(required=True, identifier=True)
book_id = Identifier(required=True)


@lending.command_handler(part_of="Patron")
class CheckoutCommandHandler:
@handle(CheckoutBook)
Expand All @@ -21,3 +27,10 @@ def handle_checkout_book(self, command: CheckoutBook) -> None:

checkout(patron, book, command.branch_id)()
current_domain.repository_for(Patron).add(patron)

@handle(ReturnBook)
def handle_return_book(self, command: ReturnBook) -> None:
patron = current_domain.repository_for(Patron).get(command.patron_id)

patron.return_book(command.book_id)
current_domain.repository_for(Patron).add(patron)
178 changes: 175 additions & 3 deletions tests/lending/app/bdd/checkout_handlers/step_defs/checkout_steps.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
from datetime import date, timedelta

import pytest
from protean import current_domain, g
from protean import UnitOfWork, current_domain, g
from pytest_bdd import given, then, when
from pytest_bdd.parsers import cfparse

from lending import CheckoutBook, HoldStatus, HoldType, Patron, PlaceHold
from lending import (
Book,
Checkout,
CheckoutBook,
CheckoutStatus,
DailySheetService,
HoldStatus,
HoldType,
Patron,
PlaceHold,
ReturnBook,
)


@pytest.fixture(autouse=True)
Expand All @@ -25,7 +36,13 @@ def circulating_book_available(book):
g.current_book = book


@given("a restricted book is available")
def restricted_book(restricted_book):
g.current_book = restricted_book


@given("a patron is logged in")
@given("a regular patron is logged in")
def patron_logged_in(regular_patron):
g.current_user = regular_patron

Expand All @@ -41,14 +58,116 @@ def patron_has_hold_on_book():
current_domain.process(command)


@given("a patron has checked out a book")
def patron_with_checkout(regular_patron, book):
g.current_user = regular_patron
g.current_book = book

command = CheckoutBook(
patron_id=g.current_user.id,
book_id=g.current_book.id,
branch_id="1",
)
try:
current_domain.process(command)
except Exception as e:
g.current_exception = e


@given("the system has overdue checkouts")
def system_has_overdue_checkouts():
patron1 = Patron()
current_domain.repository_for(Patron).add(patron1)
patron2 = Patron()
current_domain.repository_for(Patron).add(patron2)

book1 = Book(isbn="1234567890123")
current_domain.repository_for(Book).add(book1)
book2 = Book(isbn="1234567890124")
current_domain.repository_for(Book).add(book2)
book3 = Book(isbn="1234567890125")
current_domain.repository_for(Book).add(book3)

patron1 = current_domain.repository_for(Patron).get(patron1.id)
patron2 = current_domain.repository_for(Patron).get(patron2.id)

patron1.add_checkouts(
[
Checkout(
book_id=book1.id,
branch_id="1",
),
Checkout(
book_id=book2.id,
branch_id="1",
),
]
)
# Manually expire a checkout
patron1.checkouts[0].due_on = date.today() - timedelta(days=1)

patron2.add_checkouts(
Checkout(
book_id=book3.id,
branch_id="1",
)
)
# Manually exipre a checkout
patron2.checkouts[0].due_on = date.today() - timedelta(days=1)

current_domain.repository_for(Patron).add(patron1)
current_domain.repository_for(Patron).add(patron2)

g.current_patrons = [patron1, patron2]
g.patron1_checkout_overdue_id = patron1.checkouts[0].id
g.patron2_checkout_overdue_id = patron2.checkouts[0].id


@given("the book is overdue")
def mark_checkout_overdue():
patron = current_domain.repository_for(Patron).get(g.current_user.id)

patron.checkouts[0].due_on = date.today() - timedelta(days=1)
patron.checkouts[0].status = CheckoutStatus.OVERDUE.value

current_domain.repository_for(Patron).add(patron)


@when("the patron checks out the book")
@when("the patron tries to check out the book")
def patron_checks_out_book():
command = CheckoutBook(
patron_id=g.current_user.id,
book_id=g.current_book.id,
branch_id="1",
)
current_domain.process(command)
try:
current_domain.process(command)
except Exception as e:
g.current_exception = e


@when("the system processes the overdue checkouts")
def process_overdue_checkouts():
with UnitOfWork():
patron1 = current_domain.repository_for(Patron).get(g.current_patrons[0].id)
patron2 = current_domain.repository_for(Patron).get(g.current_patrons[1].id)
DailySheetService(patrons=[patron1, patron2]).run()

current_domain.repository_for(Patron).add(patron1)
current_domain.repository_for(Patron).add(patron2)


@when("the patron returns the book")
def patron_returns_book():
command = ReturnBook(
patron_id=g.current_user.id,
book_id=g.current_book.id,
)
try:
current_domain.process(command)
except Exception as e:
g.current_exception = e


@then("the checkout is successfully completed")
Expand All @@ -73,3 +192,56 @@ def hold_marked_checked_out():
patron = current_domain.repository_for(Patron).get(g.current_user.id)
hold = patron.holds[0]
assert hold.status == HoldStatus.CHECKED_OUT.value


@then("the checkout is rejected")
def checkout_rejected():
assert isinstance(g.current_exception, Exception)
assert (
str(g.current_exception)
== "{'restricted': ['Regular patron cannot place a hold on a restricted book']}"
)


@then("the checkouts are marked overdue")
def confirm_overdue_marking():
patron1 = current_domain.repository_for(Patron).get(g.current_patrons[0].id)
patron2 = current_domain.repository_for(Patron).get(g.current_patrons[1].id)

patron1_checkout = next(
(
checkout
for checkout in patron1.checkouts
if checkout.id == g.patron1_checkout_overdue_id
),
None,
)
patron2_checkout = next(
(
checkout
for checkout in patron2.checkouts
if checkout.id == g.patron2_checkout_overdue_id
),
None,
)
assert patron1_checkout.status == "OVERDUE"
assert patron2_checkout.status == "OVERDUE"

if hasattr(g, "current_exception"):
print(g.current_exception.messages)
assert hasattr(g, "current_exception") is False


@then("the book is successfully returned")
def book_successfully_returned():
book = current_domain.repository_for(Book).get(g.current_book.id)
assert book.status == "AVAILABLE"

assert hasattr(g, "current_exception") is False


@then("the overdue status is cleared")
def overdue_status_cleared():
patron = current_domain.repository_for(Patron).get(g.current_user.id)
checkout = patron.checkouts[0]
assert checkout.status != "OVERDUE"
14 changes: 13 additions & 1 deletion tests/lending/features/checkouts/check_out_a_book.feature
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,16 @@ Feature: Check Out a Book
When the patron checks out the book
Then the checkout is successfully completed
And the checkout has a validity of CHECKOUT_PERIOD
And the hold is marked as checked out
And the hold is marked as checked out

Scenario: Patron checks out an available circulating book
Given a circulating book is available
And a patron is logged in
When the patron checks out the book
Then the checkout is successfully completed

Scenario: Regular Patron tries to check out a restricted book without holding
Given a restricted book is available
And a regular patron is logged in
When the patron tries to check out the book
Then the checkout is rejected

This file was deleted.

2 changes: 1 addition & 1 deletion tests/lending/model/bdd/checkouts/test_checkouts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

from .step_defs.checkout_steps import * # noqa: F403

scenarios("./features")
scenarios("../../../features/checkouts")

0 comments on commit 492b104

Please sign in to comment.