Alexander Hess
419c7e6bf6
- so far, the check constraints were untested + the tests mirror the tests for the `ReplayedOrder` model with some variations - shorten some import names - fix some typos
1068 lines
36 KiB
Python
1068 lines
36 KiB
Python
"""Test the ORM's `Order` model."""
|
|
|
|
import datetime as dt
|
|
import random
|
|
|
|
import pytest
|
|
import sqlalchemy as sqla
|
|
from sqlalchemy import exc as sa_exc
|
|
|
|
from tests import config as test_config
|
|
from urban_meal_delivery import db
|
|
|
|
|
|
class TestSpecialMethods:
|
|
"""Test special methods in `Order`."""
|
|
|
|
def test_create_order(self, order):
|
|
"""Test instantiation of a new `Order` object."""
|
|
assert order is not None
|
|
|
|
def test_text_representation(self, order):
|
|
"""`Order` has a non-literal text representation."""
|
|
result = repr(order)
|
|
|
|
assert result == f'<Order(#{order.id})>'
|
|
|
|
|
|
@pytest.mark.db
|
|
@pytest.mark.no_cover
|
|
class TestConstraints:
|
|
"""Test the database constraints defined in `Order`."""
|
|
|
|
def test_insert_ad_hoc_order_into_into_database(self, db_session, order):
|
|
"""Insert an instance into the (empty) database."""
|
|
assert order.ad_hoc is True
|
|
|
|
assert db_session.query(db.Order).count() == 0
|
|
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
assert db_session.query(db.Order).count() == 1
|
|
|
|
def test_insert_scheduled_order_into_into_database(self, db_session, pre_order):
|
|
"""Insert an instance into the (empty) database."""
|
|
assert pre_order.ad_hoc is False
|
|
|
|
assert db_session.query(db.Order).count() == 0
|
|
|
|
db_session.add(pre_order)
|
|
db_session.commit()
|
|
|
|
assert db_session.query(db.Order).count() == 1
|
|
|
|
def test_delete_a_referenced_customer(self, db_session, order):
|
|
"""Remove a record that is referenced with a FK."""
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
# Must delete without ORM as otherwise an UPDATE statement is emitted.
|
|
stmt = sqla.delete(db.Customer).where(db.Customer.id == order.customer.id)
|
|
|
|
with pytest.raises(sa_exc.IntegrityError, match='fk_orders_to_customers'):
|
|
db_session.execute(stmt)
|
|
|
|
def test_delete_a_referenced_courier(self, db_session, order):
|
|
"""Remove a record that is referenced with a FK."""
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
# Must delete without ORM as otherwise an UPDATE statement is emitted.
|
|
stmt = sqla.delete(db.Courier).where(db.Courier.id == order.courier.id)
|
|
|
|
with pytest.raises(sa_exc.IntegrityError, match='fk_orders_to_couriers'):
|
|
db_session.execute(stmt)
|
|
|
|
def test_delete_a_referenced_restaurant(self, db_session, order):
|
|
"""Remove a record that is referenced with a FK."""
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
# Must delete without ORM as otherwise an UPDATE statement is emitted.
|
|
stmt = sqla.delete(db.Restaurant).where(db.Restaurant.id == order.restaurant.id)
|
|
|
|
with pytest.raises(sa_exc.IntegrityError, match='fk_orders_to_restaurants'):
|
|
db_session.execute(stmt)
|
|
|
|
def test_change_a_referenced_pickup_address(self, db_session, order, make_address):
|
|
"""Remove a record that is referenced with a FK.
|
|
|
|
Each `Restaurant` may only have one `Address` in the dataset.
|
|
"""
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
# Give the `restaurant` another `address`.
|
|
order.restaurant.address = make_address()
|
|
db_session.add(order.restaurant.address)
|
|
|
|
with pytest.raises(sa_exc.IntegrityError, match='fk_orders_to_restaurants'):
|
|
db_session.commit()
|
|
|
|
# Here should be a test to check deletion of a referenced pickup address, so
|
|
# `test_delete_a_referenced_pickup_address(self, db_session, order)`.
|
|
# The corresponding "fk_orders_to_addresses_on_pickup_address_id" constraint
|
|
# is very hard to test in isolation as the above "fk_orders_to_restaurants_..."
|
|
# constraint ensures its integrity already.
|
|
|
|
def test_delete_a_referenced_delivery_address(self, db_session, order):
|
|
"""Remove a record that is referenced with a FK."""
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
# Must delete without ORM as otherwise an UPDATE statement is emitted.
|
|
stmt = sqla.delete(db.Address).where(db.Address.id == order.delivery_address.id)
|
|
|
|
with pytest.raises(sa_exc.IntegrityError, match='fk_orders_to_addresses'):
|
|
db_session.execute(stmt)
|
|
|
|
def test_ad_hoc_order_with_scheduled_delivery_at(self, db_session, order):
|
|
"""Insert an instance with invalid data."""
|
|
assert order.ad_hoc is True
|
|
|
|
order.scheduled_delivery_at = dt.datetime(*test_config.DATE, 18, 0)
|
|
order.scheduled_delivery_at_corrected = False
|
|
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='either_ad_hoc_or_scheduled_order',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_scheduled_order_without_scheduled_delivery_at(self, db_session, pre_order):
|
|
"""Insert an instance with invalid data."""
|
|
assert pre_order.ad_hoc is False
|
|
|
|
pre_order.scheduled_delivery_at = None
|
|
pre_order.scheduled_delivery_at_corrected = None
|
|
|
|
db_session.add(pre_order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='either_ad_hoc_or_scheduled_order',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_ad_hoc_order_too_early(self, db_session, make_order):
|
|
"""Insert an instance with invalid data."""
|
|
order = make_order(placed_at=dt.datetime(*test_config.DATE, 10, 0))
|
|
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='ad_hoc_orders_within_business_hours',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_ad_hoc_order_too_late(self, db_session, make_order):
|
|
"""Insert an instance with invalid data."""
|
|
order = make_order(placed_at=dt.datetime(*test_config.DATE, 23, 0))
|
|
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='ad_hoc_orders_within_business_hours',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_scheduled_order_way_too_early(self, db_session, make_order):
|
|
"""Insert an instance with invalid data."""
|
|
pre_order = make_order(
|
|
scheduled=True, scheduled_delivery_at=dt.datetime(*test_config.DATE, 10, 0),
|
|
)
|
|
|
|
db_session.add(pre_order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='scheduled_orders_within_business_hours',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_scheduled_order_a_bit_too_early(self, db_session, make_order):
|
|
"""Insert an instance with invalid data."""
|
|
pre_order = make_order(
|
|
scheduled=True,
|
|
scheduled_delivery_at=dt.datetime(*test_config.DATE, 11, 30),
|
|
)
|
|
|
|
db_session.add(pre_order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='scheduled_orders_within_business_hours',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_scheduled_order_not_too_early(self, db_session, make_order):
|
|
"""Insert an instance with invalid data.
|
|
|
|
11.45 is the only time outside noon to 11 pm when a scheduled order is allowed.
|
|
"""
|
|
pre_order = make_order(
|
|
scheduled=True,
|
|
scheduled_delivery_at=dt.datetime(*test_config.DATE, 11, 45),
|
|
)
|
|
|
|
assert db_session.query(db.Order).count() == 0
|
|
|
|
db_session.add(pre_order)
|
|
db_session.commit()
|
|
|
|
assert db_session.query(db.Order).count() == 1
|
|
|
|
def test_scheduled_order_too_late(self, db_session, make_order):
|
|
"""Insert an instance with invalid data."""
|
|
pre_order = make_order(
|
|
scheduled=True, scheduled_delivery_at=dt.datetime(*test_config.DATE, 23, 0),
|
|
)
|
|
|
|
db_session.add(pre_order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='scheduled_orders_within_business_hours',
|
|
):
|
|
db_session.commit()
|
|
|
|
@pytest.mark.parametrize('minute', [min_ for min_ in range(60) if min_ % 15 != 0])
|
|
def test_scheduled_order_at_non_quarter_of_an_hour(
|
|
self, db_session, make_order, minute,
|
|
):
|
|
"""Insert an instance with invalid data."""
|
|
pre_order = make_order(
|
|
scheduled=True,
|
|
scheduled_delivery_at=dt.datetime( # `minute` is not 0, 15, 30, or 45
|
|
*test_config.DATE, test_config.NOON, minute,
|
|
),
|
|
)
|
|
|
|
db_session.add(pre_order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError,
|
|
match='scheduled_orders_must_be_at_quarters_of_an_hour',
|
|
):
|
|
db_session.commit()
|
|
|
|
@pytest.mark.parametrize('second', list(range(1, 60)))
|
|
def test_scheduled_order_at_non_quarter_of_an_hour_by_seconds(
|
|
self, db_session, make_order, second,
|
|
):
|
|
"""Insert an instance with invalid data."""
|
|
pre_order = make_order(
|
|
scheduled=True,
|
|
scheduled_delivery_at=dt.datetime(
|
|
*test_config.DATE, test_config.NOON, 0, second,
|
|
),
|
|
)
|
|
|
|
db_session.add(pre_order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError,
|
|
match='scheduled_orders_must_be_at_quarters_of_an_hour',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_scheduled_order_too_soon(self, db_session, make_order):
|
|
"""Insert an instance with invalid data.
|
|
|
|
Scheduled orders must be at least 30 minutes into the future.
|
|
"""
|
|
# Create an ad-hoc order first and then make that a scheduled order.
|
|
# This way, it is less work to keep the timestamps consistent.
|
|
pre_order = make_order(scheduled=False)
|
|
|
|
# Make the `scheduled_delivery_at` the quarter of an hour
|
|
# following the next quarter of an hour (i.e., the timestamp
|
|
# is between 15 and 30 minutes into the future).
|
|
pre_order.ad_hoc = False
|
|
minutes_to_next_quarter = 15 - (pre_order.placed_at.minute % 15)
|
|
pre_order.scheduled_delivery_at = (
|
|
# `.placed_at` may have non-0 seconds.
|
|
pre_order.placed_at.replace(second=0)
|
|
+ dt.timedelta(minutes=(minutes_to_next_quarter + 15))
|
|
)
|
|
pre_order.scheduled_delivery_at_corrected = False
|
|
|
|
db_session.add(pre_order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='scheduled_orders_not_within_30_minutes',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_uncancelled_order_has_cancelled_at(self, db_session, order):
|
|
"""Insert an instance with invalid data."""
|
|
order.cancelled_at = order.delivery_at
|
|
order.cancelled_at_corrected = False
|
|
order.delivery_at = None
|
|
order.delivery_at_corrected = None
|
|
order.delivery_not_confirmed = None
|
|
order._courier_waited_at_delivery = None
|
|
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='only_cancelled_orders_may_have_cancelled_at',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_cancelled_order_is_delivered(self, db_session, order):
|
|
"""Insert an instance with invalid data."""
|
|
order.cancelled = True
|
|
order.cancelled_at = order.delivery_at + dt.timedelta(seconds=1)
|
|
order.cancelled_at_corrected = False
|
|
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='cancelled_orders_must_not_be_delivered',
|
|
):
|
|
db_session.commit()
|
|
|
|
@pytest.mark.parametrize('duration', [-1, 2701])
|
|
def test_estimated_prep_duration_out_of_range(self, db_session, order, duration):
|
|
"""Insert an instance with invalid data."""
|
|
order.estimated_prep_duration = duration
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='between_0_and_2700',
|
|
):
|
|
db_session.commit()
|
|
|
|
@pytest.mark.parametrize('duration', [1, 59, 119, 2699])
|
|
def test_estimated_prep_duration_not_whole_minute(
|
|
self, db_session, order, duration,
|
|
):
|
|
"""Insert an instance with invalid data."""
|
|
order.estimated_prep_duration = duration
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='must_be_whole_minutes',
|
|
):
|
|
db_session.commit()
|
|
|
|
@pytest.mark.parametrize('duration', [-1, 901])
|
|
def test_estimated_prep_buffer_out_of_range(self, db_session, order, duration):
|
|
"""Insert an instance with invalid data."""
|
|
order.estimated_prep_buffer = duration
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='between_0_and_900',
|
|
):
|
|
db_session.commit()
|
|
|
|
@pytest.mark.parametrize('duration', [1, 59, 119, 899])
|
|
def test_estimated_prep_buffer_not_whole_minute(self, db_session, order, duration):
|
|
"""Insert an instance with invalid data."""
|
|
order.estimated_prep_buffer = duration
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='must_be_whole_minutes',
|
|
):
|
|
db_session.commit()
|
|
|
|
@pytest.mark.parametrize('utilization', [-1, 101])
|
|
def test_utilization_out_of_range(self, db_session, order, utilization):
|
|
"""Insert an instance with invalid data."""
|
|
order.utilization = utilization
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='between_0_and_100',
|
|
):
|
|
db_session.commit()
|
|
|
|
@pytest.mark.parametrize(
|
|
'column',
|
|
[
|
|
'scheduled_delivery_at',
|
|
'cancelled_at',
|
|
'restaurant_notified_at',
|
|
'restaurant_confirmed_at',
|
|
'estimated_prep_duration',
|
|
'dispatch_at',
|
|
'courier_notified_at',
|
|
'courier_accepted_at',
|
|
'left_pickup_at',
|
|
],
|
|
)
|
|
def test_unset_timestamp_column_not_marked_as_uncorrected( # noqa:WPS213
|
|
self, db_session, order, column,
|
|
):
|
|
"""Insert an instance with invalid data.
|
|
|
|
There are two special cases for this test case below,
|
|
where other attributes on `order` must be unset.
|
|
"""
|
|
# Set the actual timestamp to NULL.
|
|
setattr(order, column, None)
|
|
|
|
# Setting both the timestamp and its correction column to NULL is allowed.
|
|
setattr(order, f'{column}_corrected', None)
|
|
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
# Also, an unset timestamp column may always be marked as corrected.
|
|
setattr(order, f'{column}_corrected', True)
|
|
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
# Without a timestamp set, a column may not be marked as uncorrected.
|
|
setattr(order, f'{column}_corrected', False)
|
|
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='corrections_only_for_set_value',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_unset_timestamp_column_not_marked_as_uncorrected_special_case1(
|
|
self, db_session, order,
|
|
):
|
|
"""Insert an instance with invalid data.
|
|
|
|
This is one of two special cases. See the generic case above.
|
|
"""
|
|
order.pickup_not_confirmed = None
|
|
self.test_unset_timestamp_column_not_marked_as_uncorrected(
|
|
db_session, order, 'pickup_at',
|
|
)
|
|
|
|
def test_unset_timestamp_column_not_marked_as_uncorrected_special_case2(
|
|
self, db_session, order,
|
|
):
|
|
"""Insert an instance with invalid data.
|
|
|
|
This is one of two special cases. See the generic case above.
|
|
"""
|
|
order.delivery_not_confirmed = None
|
|
order._courier_waited_at_delivery = None
|
|
self.test_unset_timestamp_column_not_marked_as_uncorrected(
|
|
db_session, order, 'delivery_at',
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
'column',
|
|
[
|
|
'restaurant_notified_at',
|
|
'restaurant_confirmed_at',
|
|
'estimated_prep_duration',
|
|
'dispatch_at',
|
|
'courier_notified_at',
|
|
'courier_accepted_at',
|
|
'pickup_at',
|
|
'left_pickup_at',
|
|
'delivery_at',
|
|
],
|
|
)
|
|
def test_set_timestamp_column_is_not_unmarked( # noqa:WPS213
|
|
self, db_session, order, column,
|
|
):
|
|
"""Insert an instance with invalid data.
|
|
|
|
There are two special cases for this test case below,
|
|
where other attributes on `order` must be unset.
|
|
"""
|
|
# Ensure the timestamp is set.
|
|
assert getattr(order, column) is not None
|
|
|
|
# A set timestamp may be marked as either corrected or uncorrected.
|
|
|
|
setattr(order, f'{column}_corrected', True)
|
|
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
setattr(order, f'{column}_corrected', False)
|
|
|
|
db_session.add(order)
|
|
db_session.commit()
|
|
|
|
# A set timestamp may not be left unmarked.
|
|
setattr(order, f'{column}_corrected', None)
|
|
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(
|
|
sa_exc.IntegrityError, match='corrections_only_for_set_value',
|
|
):
|
|
db_session.commit()
|
|
|
|
def test_set_timestamp_column_is_not_unmarked_special_case1(
|
|
self, db_session, pre_order,
|
|
):
|
|
"""Insert an instance with invalid data.
|
|
|
|
This is one of two special cases. See the generic case above.
|
|
"""
|
|
self.test_set_timestamp_column_is_not_unmarked(
|
|
db_session, pre_order, 'scheduled_delivery_at',
|
|
)
|
|
|
|
def test_set_timestamp_column_is_not_unmarked_special_case2(
|
|
self, db_session, order,
|
|
):
|
|
"""Insert an instance with invalid data.
|
|
|
|
This is one of two special cases. See the generic case above.
|
|
"""
|
|
order.cancelled = True
|
|
order.cancelled_at = order.delivery_at
|
|
|
|
order.delivery_at = None
|
|
order.delivery_at_corrected = None
|
|
order.delivery_not_confirmed = None
|
|
order._courier_waited_at_delivery = None
|
|
|
|
self.test_set_timestamp_column_is_not_unmarked(
|
|
db_session, order, 'cancelled_at',
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
'comparison',
|
|
[
|
|
'placed_at < first_estimated_delivery_at',
|
|
'placed_at < restaurant_notified_at',
|
|
'placed_at < restaurant_confirmed_at',
|
|
'placed_at < dispatch_at',
|
|
'placed_at < courier_notified_at',
|
|
'placed_at < courier_accepted_at',
|
|
'placed_at < reached_pickup_at',
|
|
'placed_at < pickup_at',
|
|
'placed_at < left_pickup_at',
|
|
'placed_at < reached_delivery_at',
|
|
'placed_at < delivery_at',
|
|
'restaurant_notified_at < restaurant_confirmed_at',
|
|
'restaurant_notified_at < pickup_at',
|
|
'restaurant_confirmed_at < pickup_at',
|
|
'dispatch_at < courier_notified_at',
|
|
'dispatch_at < courier_accepted_at',
|
|
'dispatch_at < reached_pickup_at',
|
|
'dispatch_at < pickup_at',
|
|
'dispatch_at < left_pickup_at',
|
|
'dispatch_at < reached_delivery_at',
|
|
'dispatch_at < delivery_at',
|
|
'courier_notified_at < courier_accepted_at',
|
|
'courier_notified_at < reached_pickup_at',
|
|
'courier_notified_at < pickup_at',
|
|
'courier_notified_at < left_pickup_at',
|
|
'courier_notified_at < reached_delivery_at',
|
|
'courier_notified_at < delivery_at',
|
|
'courier_accepted_at < reached_pickup_at',
|
|
'courier_accepted_at < pickup_at',
|
|
'courier_accepted_at < left_pickup_at',
|
|
'courier_accepted_at < reached_delivery_at',
|
|
'courier_accepted_at < delivery_at',
|
|
'reached_pickup_at < pickup_at',
|
|
'reached_pickup_at < left_pickup_at',
|
|
'reached_pickup_at < reached_delivery_at',
|
|
'reached_pickup_at < delivery_at',
|
|
'pickup_at < left_pickup_at',
|
|
'pickup_at < reached_delivery_at',
|
|
'pickup_at < delivery_at',
|
|
'left_pickup_at < reached_delivery_at',
|
|
'left_pickup_at < delivery_at',
|
|
'reached_delivery_at < delivery_at',
|
|
],
|
|
)
|
|
def test_timestamps_unordered(
|
|
self, db_session, order, comparison,
|
|
):
|
|
"""Insert an instance with invalid data.
|
|
|
|
There are two special cases for this test case below,
|
|
where other attributes on `order` must be unset.
|
|
"""
|
|
smaller, bigger = comparison.split(' < ')
|
|
|
|
assert smaller is not None
|
|
|
|
violating_timestamp = getattr(order, smaller) - dt.timedelta(seconds=1)
|
|
setattr(order, bigger, violating_timestamp)
|
|
setattr(order, f'{bigger}_corrected', False)
|
|
|
|
db_session.add(order)
|
|
|
|
with pytest.raises(sa_exc.IntegrityError, match='ordered_timestamps'):
|
|
db_session.commit()
|
|
|
|
def test_timestamps_unordered_scheduled(self, db_session, pre_order):
|
|
"""Insert an instance with invalid data.
|
|
|
|
This is one of two special cases. See the generic case above.
|
|
"""
|
|
# As we subtract 1 second in the generic case,
|
|
# choose one second after a quarter of an hour.
|
|
pre_order.placed_at = dt.datetime(*test_config.DATE, 11, 45, 1)
|
|
self.test_timestamps_unordered(
|
|
db_session, pre_order, 'placed_at < scheduled_delivery_at',
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
'comparison',
|
|
[
|
|
'placed_at < cancelled_at',
|
|
'restaurant_notified_at < cancelled_at',
|
|
'restaurant_confirmed_at < cancelled_at',
|
|
'dispatch_at < cancelled_at',
|
|
'courier_notified_at < cancelled_at',
|
|
'courier_accepted_at < cancelled_at',
|
|
'reached_pickup_at < cancelled_at',
|
|
'pickup_at < cancelled_at',
|
|
'left_pickup_at < cancelled_at',
|
|
'reached_delivery_at < cancelled_at',
|
|
],
|
|
)
|
|
def test_timestamps_unordered_cancelled(
|
|
self, db_session, order, comparison,
|
|
):
|
|
"""Insert an instance with invalid data.
|
|
|
|
This is one of two special cases. See the generic case above.
|
|
"""
|
|
order.cancelled = True
|
|
|
|
order.delivery_at = None
|
|
order.delivery_at_corrected = None
|
|
order.delivery_not_confirmed = None
|
|
order._courier_waited_at_delivery = None
|
|
|
|
self.test_timestamps_unordered(db_session, order, comparison)
|
|
|
|
|
|
class TestProperties:
|
|
"""Test properties in `Order`.
|
|
|
|
The `order` fixture uses the defaults specified in `factories.OrderFactory`
|
|
and provided by the `make_order` fixture.
|
|
"""
|
|
|
|
def test_is_ad_hoc(self, order):
|
|
"""Test `Order.scheduled` property."""
|
|
assert order.ad_hoc is True
|
|
|
|
result = order.scheduled
|
|
|
|
assert result is False
|
|
|
|
def test_is_scheduled(self, make_order):
|
|
"""Test `Order.scheduled` property."""
|
|
order = make_order(scheduled=True)
|
|
assert order.ad_hoc is False
|
|
|
|
result = order.scheduled
|
|
|
|
assert result is True
|
|
|
|
def test_is_completed(self, order):
|
|
"""Test `Order.completed` property."""
|
|
result = order.completed
|
|
|
|
assert result is True
|
|
|
|
def test_is_not_completed1(self, make_order):
|
|
"""Test `Order.completed` property."""
|
|
order = make_order(cancel_before_pickup=True)
|
|
assert order.cancelled is True
|
|
|
|
result = order.completed
|
|
|
|
assert result is False
|
|
|
|
def test_is_not_completed2(self, make_order):
|
|
"""Test `Order.completed` property."""
|
|
order = make_order(cancel_after_pickup=True)
|
|
assert order.cancelled is True
|
|
|
|
result = order.completed
|
|
|
|
assert result is False
|
|
|
|
def test_is_not_corrected(self, order):
|
|
"""Test `Order.corrected` property."""
|
|
# By default, the `OrderFactory` sets all `.*_corrected` attributes to `False`.
|
|
result = order.corrected
|
|
|
|
assert result is False
|
|
|
|
@pytest.mark.parametrize(
|
|
'column',
|
|
[
|
|
'scheduled_delivery_at',
|
|
'cancelled_at',
|
|
'restaurant_notified_at',
|
|
'restaurant_confirmed_at',
|
|
'dispatch_at',
|
|
'courier_notified_at',
|
|
'courier_accepted_at',
|
|
'pickup_at',
|
|
'left_pickup_at',
|
|
'delivery_at',
|
|
],
|
|
)
|
|
def test_is_corrected(self, order, column):
|
|
"""Test `Order.corrected` property."""
|
|
setattr(order, f'{column}_corrected', True)
|
|
|
|
result = order.corrected
|
|
|
|
assert result is True
|
|
|
|
def test_time_to_accept_no_dispatch_at(self, order):
|
|
"""Test `Order.time_to_accept` property."""
|
|
order.dispatch_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_to_accept)
|
|
|
|
def test_time_to_accept_no_courier_accepted(self, order):
|
|
"""Test `Order.time_to_accept` property."""
|
|
order.courier_accepted_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_to_accept)
|
|
|
|
def test_time_to_accept_success(self, order):
|
|
"""Test `Order.time_to_accept` property."""
|
|
result = order.time_to_accept
|
|
|
|
assert result > dt.timedelta(0)
|
|
|
|
def test_time_to_react_no_courier_notified(self, order):
|
|
"""Test `Order.time_to_react` property."""
|
|
order.courier_notified_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_to_react)
|
|
|
|
def test_time_to_react_no_courier_accepted(self, order):
|
|
"""Test `Order.time_to_react` property."""
|
|
order.courier_accepted_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_to_react)
|
|
|
|
def test_time_to_react_success(self, order):
|
|
"""Test `Order.time_to_react` property."""
|
|
result = order.time_to_react
|
|
|
|
assert result > dt.timedelta(0)
|
|
|
|
def test_time_to_pickup_no_reached_pickup_at(self, order):
|
|
"""Test `Order.time_to_pickup` property."""
|
|
order.reached_pickup_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_to_pickup)
|
|
|
|
def test_time_to_pickup_no_courier_accepted(self, order):
|
|
"""Test `Order.time_to_pickup` property."""
|
|
order.courier_accepted_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_to_pickup)
|
|
|
|
def test_time_to_pickup_success(self, order):
|
|
"""Test `Order.time_to_pickup` property."""
|
|
result = order.time_to_pickup
|
|
|
|
assert result > dt.timedelta(0)
|
|
|
|
def test_time_at_pickup_no_reached_pickup_at(self, order):
|
|
"""Test `Order.time_at_pickup` property."""
|
|
order.reached_pickup_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_at_pickup)
|
|
|
|
def test_time_at_pickup_no_pickup_at(self, order):
|
|
"""Test `Order.time_at_pickup` property."""
|
|
order.pickup_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_at_pickup)
|
|
|
|
def test_time_at_pickup_success(self, order):
|
|
"""Test `Order.time_at_pickup` property."""
|
|
result = order.time_at_pickup
|
|
|
|
assert result > dt.timedelta(0)
|
|
|
|
def test_scheduled_pickup_at_no_restaurant_notified(self, order): # noqa:WPS118
|
|
"""Test `Order.scheduled_pickup_at` property."""
|
|
order.restaurant_notified_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.scheduled_pickup_at)
|
|
|
|
def test_scheduled_pickup_at_no_est_prep_duration(self, order): # noqa:WPS118
|
|
"""Test `Order.scheduled_pickup_at` property."""
|
|
order.estimated_prep_duration = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.scheduled_pickup_at)
|
|
|
|
def test_scheduled_pickup_at_success(self, order):
|
|
"""Test `Order.scheduled_pickup_at` property."""
|
|
result = order.scheduled_pickup_at
|
|
|
|
assert order.placed_at < result < order.delivery_at
|
|
|
|
def test_courier_is_early_at_pickup(self, order):
|
|
"""Test `Order.courier_early` property."""
|
|
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
|
|
order.estimated_prep_duration = 999_999
|
|
|
|
result = order.courier_early
|
|
|
|
assert bool(result) is True
|
|
|
|
def test_courier_is_not_early_at_pickup(self, order):
|
|
"""Test `Order.courier_early` property."""
|
|
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
|
|
order.estimated_prep_duration = 1
|
|
|
|
result = order.courier_early
|
|
|
|
assert bool(result) is False
|
|
|
|
def test_courier_is_late_at_pickup(self, order):
|
|
"""Test `Order.courier_late` property."""
|
|
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
|
|
order.estimated_prep_duration = 1
|
|
|
|
result = order.courier_late
|
|
|
|
assert bool(result) is True
|
|
|
|
def test_courier_is_not_late_at_pickup(self, order):
|
|
"""Test `Order.courier_late` property."""
|
|
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
|
|
order.estimated_prep_duration = 999_999
|
|
|
|
result = order.courier_late
|
|
|
|
assert bool(result) is False
|
|
|
|
def test_restaurant_early_at_pickup(self, order):
|
|
"""Test `Order.restaurant_early` property."""
|
|
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
|
|
order.estimated_prep_duration = 999_999
|
|
|
|
result = order.restaurant_early
|
|
|
|
assert bool(result) is True
|
|
|
|
def test_restaurant_is_not_early_at_pickup(self, order):
|
|
"""Test `Order.restaurant_early` property."""
|
|
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
|
|
order.estimated_prep_duration = 1
|
|
|
|
result = order.restaurant_early
|
|
|
|
assert bool(result) is False
|
|
|
|
def test_restaurant_is_late_at_pickup(self, order):
|
|
"""Test `Order.restaurant_late` property."""
|
|
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
|
|
order.estimated_prep_duration = 1
|
|
|
|
result = order.restaurant_late
|
|
|
|
assert bool(result) is True
|
|
|
|
def test_restaurant_is_not_late_at_pickup(self, order):
|
|
"""Test `Order.restaurant_late` property."""
|
|
# Manipulate the attribute that determines `Order.scheduled_pickup_at`.
|
|
order.estimated_prep_duration = 999_999
|
|
|
|
result = order.restaurant_late
|
|
|
|
assert bool(result) is False
|
|
|
|
def test_time_to_delivery_no_reached_delivery_at(self, order): # noqa:WPS118
|
|
"""Test `Order.time_to_delivery` property."""
|
|
order.reached_delivery_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_to_delivery)
|
|
|
|
def test_time_to_delivery_no_pickup_at(self, order):
|
|
"""Test `Order.time_to_delivery` property."""
|
|
order.pickup_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_to_delivery)
|
|
|
|
def test_time_to_delivery_success(self, order):
|
|
"""Test `Order.time_to_delivery` property."""
|
|
result = order.time_to_delivery
|
|
|
|
assert result > dt.timedelta(0)
|
|
|
|
def test_time_at_delivery_no_reached_delivery_at(self, order): # noqa:WPS118
|
|
"""Test `Order.time_at_delivery` property."""
|
|
order.reached_delivery_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_at_delivery)
|
|
|
|
def test_time_at_delivery_no_delivery_at(self, order):
|
|
"""Test `Order.time_at_delivery` property."""
|
|
order.delivery_at = None
|
|
|
|
with pytest.raises(RuntimeError, match='not set'):
|
|
int(order.time_at_delivery)
|
|
|
|
def test_time_at_delivery_success(self, order):
|
|
"""Test `Order.time_at_delivery` property."""
|
|
result = order.time_at_delivery
|
|
|
|
assert result > dt.timedelta(0)
|
|
|
|
def test_courier_waited_at_delivery(self, order):
|
|
"""Test `Order.courier_waited_at_delivery` property."""
|
|
order._courier_waited_at_delivery = True
|
|
|
|
result = order.courier_waited_at_delivery.total_seconds()
|
|
|
|
assert result > 0
|
|
|
|
def test_courier_did_not_wait_at_delivery(self, order):
|
|
"""Test `Order.courier_waited_at_delivery` property."""
|
|
order._courier_waited_at_delivery = False
|
|
|
|
result = order.courier_waited_at_delivery.total_seconds()
|
|
|
|
assert result == 0
|
|
|
|
def test_ad_hoc_order_cannot_be_early(self, order):
|
|
"""Test `Order.delivery_early` property."""
|
|
# By default, the `OrderFactory` creates ad-hoc orders.
|
|
with pytest.raises(AttributeError, match='scheduled'):
|
|
int(order.delivery_early)
|
|
|
|
def test_scheduled_order_delivered_early(self, make_order):
|
|
"""Test `Order.delivery_early` property."""
|
|
order = make_order(scheduled=True)
|
|
# Schedule the order to a lot later.
|
|
order.scheduled_delivery_at += dt.timedelta(hours=2)
|
|
|
|
result = order.delivery_early
|
|
|
|
assert bool(result) is True
|
|
|
|
def test_scheduled_order_not_delivered_early(self, make_order):
|
|
"""Test `Order.delivery_early` property."""
|
|
order = make_order(scheduled=True)
|
|
# Schedule the order to a lot earlier.
|
|
order.scheduled_delivery_at -= dt.timedelta(hours=2)
|
|
|
|
result = order.delivery_early
|
|
|
|
assert bool(result) is False
|
|
|
|
def test_ad_hoc_order_cannot_be_late(self, order):
|
|
"""Test Order.delivery_late property."""
|
|
# By default, the `OrderFactory` creates ad-hoc orders.
|
|
with pytest.raises(AttributeError, match='scheduled'):
|
|
int(order.delivery_late)
|
|
|
|
def test_scheduled_order_delivered_late(self, make_order):
|
|
"""Test `Order.delivery_early` property."""
|
|
order = make_order(scheduled=True)
|
|
# Schedule the order to a lot earlier.
|
|
order.scheduled_delivery_at -= dt.timedelta(hours=2)
|
|
|
|
result = order.delivery_late
|
|
|
|
assert bool(result) is True
|
|
|
|
def test_scheduled_order_not_delivered_late(self, make_order):
|
|
"""Test `Order.delivery_early` property."""
|
|
order = make_order(scheduled=True)
|
|
# Schedule the order to a lot later.
|
|
order.scheduled_delivery_at += dt.timedelta(hours=2)
|
|
|
|
result = order.delivery_late
|
|
|
|
assert bool(result) is False
|
|
|
|
def test_no_total_time_for_scheduled_order(self, make_order):
|
|
"""Test `Order.total_time` property."""
|
|
order = make_order(scheduled=True)
|
|
|
|
with pytest.raises(AttributeError, match='Scheduled'):
|
|
int(order.total_time)
|
|
|
|
def test_no_total_time_for_cancelled_order(self, make_order):
|
|
"""Test `Order.total_time` property."""
|
|
order = make_order(cancel_before_pickup=True)
|
|
|
|
with pytest.raises(RuntimeError, match='Cancelled'):
|
|
int(order.total_time)
|
|
|
|
def test_total_time_success(self, order):
|
|
"""Test `Order.total_time` property."""
|
|
result = order.total_time
|
|
|
|
assert result > dt.timedelta(0)
|
|
|
|
|
|
@pytest.mark.db
|
|
@pytest.mark.no_cover
|
|
def test_make_random_orders( # noqa:C901,WPS211,WPS213
|
|
db_session, make_address, make_courier, make_restaurant, make_order,
|
|
):
|
|
"""Sanity check the all the `make_*` fixtures.
|
|
|
|
Ensure that all generated `Address`, `Courier`, `Customer`, `Restaurant`,
|
|
and `Order` objects adhere to the database constraints.
|
|
""" # noqa:D202
|
|
# Generate a large number of `Order`s to obtain a large variance of data.
|
|
for _ in range(1_000): # noqa:WPS122
|
|
|
|
# Ad-hoc `Order`s are far more common than pre-orders.
|
|
scheduled = random.choice([True, False, False, False, False])
|
|
|
|
# Randomly pass a `address` argument to `make_restaurant()` and
|
|
# a `restaurant` argument to `make_order()`.
|
|
if random.random() < 0.5:
|
|
address = random.choice([None, make_address()])
|
|
restaurant = make_restaurant(address=address)
|
|
else:
|
|
restaurant = None
|
|
|
|
# Randomly pass a `courier` argument to `make_order()`.
|
|
courier = random.choice([None, make_courier()])
|
|
|
|
# A tiny fraction of `Order`s get cancelled.
|
|
if random.random() < 0.05:
|
|
if random.random() < 0.5:
|
|
cancel_before_pickup, cancel_after_pickup = True, False
|
|
else:
|
|
cancel_before_pickup, cancel_after_pickup = False, True
|
|
else:
|
|
cancel_before_pickup, cancel_after_pickup = False, False
|
|
|
|
# Write all the generated objects to the database.
|
|
# This should already trigger an `IntegrityError` if the data are flawed.
|
|
order = make_order(
|
|
scheduled=scheduled,
|
|
restaurant=restaurant,
|
|
courier=courier,
|
|
cancel_before_pickup=cancel_before_pickup,
|
|
cancel_after_pickup=cancel_after_pickup,
|
|
)
|
|
db_session.add(order)
|
|
|
|
db_session.commit()
|