Achieve 100% test coverage for Cart model
This commit is contained in:
parent
66455dc45a
commit
89f8737a58
4 changed files with 1535 additions and 0 deletions
72
CLAUDE.md
Normal file
72
CLAUDE.md
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# Claude Development Notes
|
||||
|
||||
## Project Setup
|
||||
|
||||
This project uses a Makefile for most development operations. Use `make` commands instead of running tools directly.
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Testing
|
||||
- Run tests: `make test`
|
||||
- This installs development dependencies and runs the test suite with py.test
|
||||
- Tests are located in `make_post_sell/tests/`
|
||||
|
||||
### Installation & Setup
|
||||
- Install from source for development: `make install-from-source`
|
||||
- Install from PyPI: `make install-from-pypi`
|
||||
- Initialize database: `make init-db`
|
||||
|
||||
### Development Server
|
||||
- Start development server: `make serve`
|
||||
- Runs with auto-reload enabled
|
||||
- Uses `data/development.ini` configuration
|
||||
|
||||
### Environment Management
|
||||
- Create virtual environment: `make venv`
|
||||
- Clean up environment: `make clean`
|
||||
- Activate environment: `source env/bin/activate`
|
||||
|
||||
## Code Structure
|
||||
|
||||
### Key Directories
|
||||
- `make_post_sell/views/` - View controllers
|
||||
- `make_post_sell/models/` - Database models
|
||||
- `make_post_sell/tests/` - Test suite
|
||||
|
||||
### Important Files
|
||||
- `make_post_sell/views/cart.py` - Cart and checkout logic
|
||||
- `development.ini` - Configuration file
|
||||
|
||||
## Testing Notes
|
||||
|
||||
The project uses pytest with unittest framework. There are three types of tests:
|
||||
|
||||
### Test Types
|
||||
- **Unit tests** (`test_models.py`) - Test individual model methods and properties in isolation
|
||||
- **Integration tests** (`test_integration.py`) - Test interactions between models and business logic
|
||||
- **Functional tests** (`test_functional.py`) - End-to-end tests through the web interface
|
||||
|
||||
### Running Tests
|
||||
**Before running tests**: Source environment variables with `source vars.sh` to set required Stripe API keys and other configuration.
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
make test
|
||||
|
||||
# Run specific test types
|
||||
env/bin/py.test make_post_sell/tests/test_models.py # Unit tests
|
||||
env/bin/py.test make_post_sell/tests/test_integration.py # Integration tests
|
||||
env/bin/py.test make_post_sell/tests/test_functional.py # Functional tests
|
||||
|
||||
# Run with coverage
|
||||
env/bin/py.test --cov=make_post_sell.models.cart --cov-report=term-missing make_post_sell/tests/test_models.py::TestCart
|
||||
```
|
||||
|
||||
### Current Coverage
|
||||
- Cart model unit tests cover critical business logic like `requires_payment` threshold (64 cents)
|
||||
- Integration tests verify the original AttributeError bug fix for free coupon checkout
|
||||
- Functional tests provide end-to-end coverage of cart/checkout/payment flows
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
Do not include Claude Code attribution in commit messages.
|
||||
5
Makefile
5
Makefile
|
|
@ -113,6 +113,11 @@ test: install-source-dev-and-test
|
|||
@echo "Running tests..."
|
||||
$(VENV_DIR)/bin/py.test
|
||||
|
||||
# Run tests with coverage for the full repository.
|
||||
test-coverage: install-source-dev-and-test
|
||||
@echo "Running tests with coverage..."
|
||||
$(VENV_DIR)/bin/py.test --cov=make_post_sell --cov-report=term-missing --cov-report=html
|
||||
|
||||
# Start a simple HTTP server (if needed for static files).
|
||||
http: venv
|
||||
@echo "Starting simple HTTP server on port 8000..."
|
||||
|
|
|
|||
986
make_post_sell/tests/test_integration.py
Normal file
986
make_post_sell/tests/test_integration.py
Normal file
|
|
@ -0,0 +1,986 @@
|
|||
"""Integration tests for make_post_sell.
|
||||
|
||||
Integration tests verify interactions between different parts of the system,
|
||||
testing boundaries between models, views, and business logic with real database
|
||||
and ORM objects.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import transaction
|
||||
import mock
|
||||
|
||||
from pyramid.paster import get_appsettings
|
||||
from ..models import get_tm_session
|
||||
from ..models.meta import Base
|
||||
from ..models.user import get_or_create_user_by_email, User
|
||||
from ..models.shop import Shop
|
||||
from ..models.product import Product
|
||||
from ..models.cart import Cart
|
||||
from ..models.coupon import Coupon
|
||||
from ..models.cart_coupon import CartCoupon
|
||||
from ..models.stripe_user_shop import StripeUserShop
|
||||
|
||||
|
||||
class DatabaseIntegrationTests(unittest.TestCase):
|
||||
"""Base class for integration tests that need real database."""
|
||||
|
||||
def setUp(self):
|
||||
from make_post_sell import main
|
||||
|
||||
self.settings = get_appsettings("test.ini")
|
||||
self.app = main({}, **self.settings)
|
||||
|
||||
self.session_factory = self.app.registry["dbsession_factory"]
|
||||
self.engine = self.session_factory.kw["bind"]
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
|
||||
self.dbsession = get_tm_session(self.session_factory, transaction.manager)
|
||||
|
||||
def tearDown(self):
|
||||
transaction.abort()
|
||||
Base.metadata.drop_all(bind=self.engine)
|
||||
|
||||
|
||||
class TestCartOrmIntegration(DatabaseIntegrationTests):
|
||||
"""Integration tests for Cart model with real ORM objects."""
|
||||
|
||||
def test_cart_with_real_products_and_shops(self):
|
||||
"""Test cart operations with real Product and Shop ORM objects."""
|
||||
# Create real user
|
||||
user = get_or_create_user_by_email(self.dbsession, "test@example.com")
|
||||
self.dbsession.add(user)
|
||||
|
||||
# Create real shop
|
||||
shop = Shop(
|
||||
name="Test Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A test shop"
|
||||
)
|
||||
shop.stripe_public_api_key = "pk_test_123"
|
||||
shop.stripe_secret_api_key = "sk_test_123"
|
||||
shop.domain_name = "test.com"
|
||||
self.dbsession.add(shop)
|
||||
|
||||
# Create real products
|
||||
product1 = Product(
|
||||
title="Digital Product 1",
|
||||
description="Test product"
|
||||
)
|
||||
product1.shop_id = shop.id
|
||||
product1.price_in_cents = 1000 # $10.00
|
||||
product1.is_physical = False
|
||||
|
||||
product2 = Product(
|
||||
title="Digital Product 2",
|
||||
description="Another test product"
|
||||
)
|
||||
product2.shop_id = shop.id
|
||||
product2.price_in_cents = 500 # $5.00
|
||||
product2.is_physical = False
|
||||
self.dbsession.add(product1)
|
||||
self.dbsession.add(product2)
|
||||
self.dbsession.flush() # Get IDs
|
||||
|
||||
# Create real cart
|
||||
cart = Cart(user=user)
|
||||
cart.shop = shop
|
||||
self.dbsession.add(cart)
|
||||
|
||||
# Test adding products to cart
|
||||
cart.add_product(product1)
|
||||
cart.add_product(product1) # Add twice
|
||||
cart.add_product(product2)
|
||||
|
||||
# Test cart calculations with real ORM relationships
|
||||
self.assertEqual(cart.count, 3) # 2 + 1
|
||||
self.assertEqual(cart.get_product_quantity(product1), 2)
|
||||
self.assertEqual(cart.get_product_quantity(product2), 1)
|
||||
|
||||
# Test total calculation
|
||||
expected_total = (1000 * 2) + (500 * 1) # $25.00
|
||||
self.assertEqual(cart.total_price_in_cents, expected_total)
|
||||
|
||||
# Test requires_payment logic
|
||||
self.assertTrue(cart.requires_payment) # Above 64 cent threshold
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_with_real_coupon_integration(self):
|
||||
"""Test cart with real coupon objects and validation."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "test@example.com")
|
||||
shop = Shop(
|
||||
name="Test Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A test shop"
|
||||
)
|
||||
shop.stripe_public_api_key = "pk_test_123"
|
||||
shop.stripe_secret_api_key = "sk_test_123"
|
||||
shop.domain_name = "test.com"
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
|
||||
# Create real product
|
||||
product = Product(
|
||||
title="Test Product",
|
||||
description="Test product"
|
||||
)
|
||||
product.shop_id = shop.id
|
||||
product.price_in_cents = 1000 # $10.00
|
||||
product.is_physical = False
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create real coupon
|
||||
coupon = Coupon(
|
||||
shop=shop,
|
||||
code="SAVE50",
|
||||
description="Save $5",
|
||||
action_type="dollar-off",
|
||||
action_value=5, # $5.00 off
|
||||
max_redemptions=10,
|
||||
max_redemptions_per_user=1,
|
||||
cart_qualifier=5 # Minimum $5.00 cart
|
||||
)
|
||||
self.dbsession.add(coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create real cart
|
||||
cart = Cart(user=user)
|
||||
cart.shop = shop
|
||||
self.dbsession.add(cart)
|
||||
|
||||
# Add product to cart
|
||||
cart.add_product(product)
|
||||
|
||||
# Attach coupon to cart
|
||||
cart_coupon = CartCoupon(cart=cart, coupon=coupon)
|
||||
self.dbsession.add(cart_coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Test coupon validation with real ORM relationships
|
||||
errors = cart.validate_attached_coupons()
|
||||
self.assertEqual(errors, []) # Should be valid
|
||||
|
||||
# Test discounted total
|
||||
self.assertTrue(cart.is_discounted)
|
||||
self.assertEqual(cart.total_discounted_price_in_cents, 500) # $10 - $5 = $5
|
||||
|
||||
# Test that discounted cart still requires payment (above 64 cent threshold)
|
||||
self.assertTrue(cart.requires_payment)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_free_with_coupon_integration(self):
|
||||
"""Test cart that becomes free with coupon - the original bug scenario."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "test@example.com")
|
||||
shop = Shop(
|
||||
name="Test Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A test shop"
|
||||
)
|
||||
shop.stripe_public_api_key = "pk_test_123"
|
||||
shop.stripe_secret_api_key = "sk_test_123"
|
||||
shop.domain_name = "test.com"
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
|
||||
# Create real cheap product
|
||||
product = Product(
|
||||
title="Cheap Product",
|
||||
description="Test product"
|
||||
)
|
||||
product.shop_id = shop.id
|
||||
product.price_in_cents = 50 # $0.50
|
||||
product.is_physical = False
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create coupon that makes cart free
|
||||
coupon = Coupon(
|
||||
shop=shop,
|
||||
code="FREEGIFT",
|
||||
description="Free gift",
|
||||
action_type="dollar-off",
|
||||
action_value=1, # $1.00 off (more than product cost)
|
||||
max_redemptions=100,
|
||||
max_redemptions_per_user=1,
|
||||
cart_qualifier=0 # No minimum
|
||||
)
|
||||
self.dbsession.add(coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create real cart
|
||||
cart = Cart(user=user)
|
||||
cart.shop = shop
|
||||
self.dbsession.add(cart)
|
||||
|
||||
# Add product to cart
|
||||
cart.add_product(product)
|
||||
|
||||
# Attach coupon to cart
|
||||
cart_coupon = CartCoupon(cart=cart, coupon=coupon)
|
||||
self.dbsession.add(cart_coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Test the original bug scenario
|
||||
self.assertTrue(cart.is_discounted)
|
||||
self.assertEqual(cart.total_discounted_price_in_cents, 0) # Free!
|
||||
|
||||
# This is the key test - free cart should not require payment
|
||||
self.assertFalse(cart.requires_payment) # Below 64 cent threshold
|
||||
|
||||
# Test the checkout logic that was crashing
|
||||
stripe_user_shop = None # No payment method needed for free cart
|
||||
|
||||
# These are the exact conditions from cart.py that we fixed
|
||||
requires_billing_redirect = cart.requires_payment and stripe_user_shop is None
|
||||
self.assertFalse(requires_billing_redirect) # Should NOT redirect
|
||||
|
||||
# This was the line that crashed - now fixed
|
||||
requires_active_card_redirect = stripe_user_shop and stripe_user_shop.active_card is None
|
||||
self.assertFalse(requires_active_card_redirect) # Should NOT crash
|
||||
|
||||
# This was the return value that crashed - now fixed
|
||||
active_card = stripe_user_shop.active_card if stripe_user_shop else None
|
||||
self.assertIsNone(active_card) # Should be None, not crash
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_physical_products_integration(self):
|
||||
"""Test cart with physical products requiring shipping."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "test@example.com")
|
||||
shop = Shop(
|
||||
name="Physical Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A shop with physical items"
|
||||
)
|
||||
shop.stripe_public_api_key = "pk_test_123"
|
||||
shop.stripe_secret_api_key = "sk_test_123"
|
||||
shop.domain_name = "test.com"
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
|
||||
# Create physical product
|
||||
physical_product = Product(
|
||||
title="Physical Item",
|
||||
description="A physical product"
|
||||
)
|
||||
physical_product.shop_id = shop.id
|
||||
physical_product.price_in_cents = 2000 # $20.00
|
||||
physical_product.is_physical = True
|
||||
self.dbsession.add(physical_product)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create real cart
|
||||
cart = Cart(user=user)
|
||||
cart.shop = shop
|
||||
self.dbsession.add(cart)
|
||||
|
||||
# Add physical product to cart
|
||||
cart.add_product(physical_product)
|
||||
|
||||
# Test physical product detection
|
||||
self.assertTrue(len(cart.physical_products) > 0)
|
||||
self.assertIn(str(physical_product.id), cart.physical_products)
|
||||
|
||||
# Test handling options
|
||||
cart.handling_option = "shipping"
|
||||
cart.handling_cost_in_cents = 500 # $5.00 shipping
|
||||
|
||||
# Test total with handling
|
||||
expected_total = 2000 + 500 # Product + shipping
|
||||
self.assertEqual(cart.total_in_cents, expected_total)
|
||||
|
||||
# Test remove handling when no physical products
|
||||
cart.remove_product(physical_product)
|
||||
cart.remove_handling_if_no_physical_products()
|
||||
|
||||
self.assertIsNone(cart.handling_option)
|
||||
self.assertEqual(cart.handling_cost_in_cents, 0)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
|
||||
class TestStripeUserShopIntegration(DatabaseIntegrationTests):
|
||||
"""Integration tests for StripeUserShop boundaries."""
|
||||
|
||||
def test_stripe_user_shop_creation_integration(self):
|
||||
"""Test creating StripeUserShop with real User and Shop objects."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "test@example.com")
|
||||
shop = Shop(
|
||||
name="Stripe Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A shop with Stripe"
|
||||
)
|
||||
shop.stripe_public_api_key = "pk_test_123"
|
||||
shop.stripe_secret_api_key = "sk_test_123"
|
||||
shop.domain_name = "test.com"
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create StripeUserShop
|
||||
stripe_user_shop = StripeUserShop(user=user, shop=shop)
|
||||
stripe_user_shop.cus_id = "cus_test123"
|
||||
stripe_user_shop.active_card_id = "card_test123"
|
||||
self.dbsession.add(stripe_user_shop)
|
||||
|
||||
# Test ORM relationships
|
||||
self.assertEqual(stripe_user_shop.user, user)
|
||||
self.assertEqual(stripe_user_shop.shop, shop)
|
||||
self.assertEqual(stripe_user_shop.cus_id, "cus_test123")
|
||||
self.assertEqual(stripe_user_shop.active_card_id, "card_test123")
|
||||
|
||||
# Test the scenario where active_card exists
|
||||
self.assertIsNotNone(stripe_user_shop.active_card_id)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_checkout_with_stripe_user_shop_integration(self):
|
||||
"""Test the full checkout flow with real StripeUserShop object."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "test@example.com")
|
||||
shop = Shop(
|
||||
name="Payment Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A shop requiring payment"
|
||||
)
|
||||
shop.stripe_public_api_key = "pk_test_123"
|
||||
shop.stripe_secret_api_key = "sk_test_123"
|
||||
shop.domain_name = "test.com"
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
|
||||
# Create product requiring payment
|
||||
product = Product(
|
||||
title="Paid Product",
|
||||
description="A product requiring payment"
|
||||
)
|
||||
product.shop_id = shop.id
|
||||
product.price_in_cents = 1500 # $15.00
|
||||
product.is_physical = False
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create cart
|
||||
cart = Cart(user=user)
|
||||
cart.shop = shop
|
||||
self.dbsession.add(cart)
|
||||
cart.add_product(product)
|
||||
|
||||
# Test scenario 1: No StripeUserShop (should require billing)
|
||||
stripe_user_shop = None
|
||||
requires_billing = cart.requires_payment and stripe_user_shop is None
|
||||
self.assertTrue(requires_billing)
|
||||
|
||||
# Test scenario 2: StripeUserShop exists but no active card
|
||||
stripe_user_shop = StripeUserShop(user=user, shop=shop)
|
||||
stripe_user_shop.cus_id = "cus_test123"
|
||||
stripe_user_shop.active_card_id = None # No active card
|
||||
self.dbsession.add(stripe_user_shop)
|
||||
|
||||
# Test the logic directly without accessing the property that calls Stripe
|
||||
requires_active_card = stripe_user_shop and stripe_user_shop.active_card_id is None
|
||||
self.assertTrue(requires_active_card)
|
||||
|
||||
# Test scenario 3: StripeUserShop with active card (should allow checkout)
|
||||
stripe_user_shop.active_card_id = "card_test123"
|
||||
|
||||
requires_billing = cart.requires_payment and stripe_user_shop is None
|
||||
requires_active_card = stripe_user_shop and stripe_user_shop.active_card_id is None
|
||||
|
||||
self.assertFalse(requires_billing)
|
||||
self.assertFalse(requires_active_card)
|
||||
|
||||
# Test the return value for template (using active_card_id since active_card would call Stripe)
|
||||
active_card_id = stripe_user_shop.active_card_id if stripe_user_shop else None
|
||||
self.assertEqual(active_card_id, "card_test123")
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_detailed_properties_integration(self):
|
||||
"""Test cart properties that require real database access for 100% coverage."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "test@example.com")
|
||||
shop = Shop(
|
||||
name="Detail Test Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A test shop"
|
||||
)
|
||||
shop.stripe_public_api_key = "pk_test_123"
|
||||
shop.stripe_secret_api_key = "sk_test_123"
|
||||
shop.domain_name = "test.com"
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
|
||||
# Create multiple products for different shops
|
||||
product1 = Product(title="Product 1", description="Test product 1")
|
||||
product1.shop_id = shop.id
|
||||
product1.price_in_cents = 1000 # $10.00
|
||||
product1.is_physical = False
|
||||
|
||||
product2 = Product(title="Product 2", description="Test product 2")
|
||||
product2.shop_id = shop.id
|
||||
product2.price_in_cents = 1500 # $15.00
|
||||
product2.is_physical = False
|
||||
|
||||
self.dbsession.add(product1)
|
||||
self.dbsession.add(product2)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create real cart
|
||||
cart = Cart(user=user)
|
||||
cart.shop = shop
|
||||
self.dbsession.add(cart)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Add products to cart
|
||||
cart.add_product(product1)
|
||||
cart.add_product(product1) # Add twice for quantity 2
|
||||
cart.add_product(product2)
|
||||
|
||||
# Test detailed properties that require database access
|
||||
# These should exercise the missing lines in the coverage report
|
||||
|
||||
# Test shops property (aggregates unique shops from products)
|
||||
shops = cart.shops
|
||||
self.assertIn(str(shop.id), shops)
|
||||
self.assertEqual(shops[str(shop.id)], shop)
|
||||
|
||||
# Test shop_product_dict property
|
||||
shop_product_dict = cart.shop_product_dict
|
||||
self.assertIn(str(shop.id), shop_product_dict)
|
||||
shop_products = shop_product_dict[str(shop.id)]
|
||||
self.assertEqual(len(shop_products), 2) # Two different products
|
||||
|
||||
# Test shop_totals_in_cents property
|
||||
shop_totals_cents = cart.shop_totals_in_cents
|
||||
self.assertIn(str(shop.id), shop_totals_cents)
|
||||
# Just verify the calculation is working, don't hardcode expected values
|
||||
self.assertGreater(shop_totals_cents[str(shop.id)], 0)
|
||||
|
||||
# Test shop_totals property (dollars)
|
||||
shop_totals = cart.shop_totals
|
||||
# Verify conversion from cents to dollars is working
|
||||
self.assertGreater(shop_totals[str(shop.id)], 0)
|
||||
|
||||
# Test line_totals property
|
||||
line_totals = cart.line_totals
|
||||
# Verify line totals exist for both products
|
||||
self.assertIn(str(product1.id), line_totals)
|
||||
self.assertIn(str(product2.id), line_totals)
|
||||
self.assertGreater(line_totals[str(product1.id)], 0)
|
||||
self.assertGreater(line_totals[str(product2.id)], 0)
|
||||
|
||||
# Test human timestamp properties
|
||||
created_timestamp = cart.human_created_timestamp
|
||||
updated_timestamp = cart.human_updated_timestamp
|
||||
self.assertIsInstance(created_timestamp, str)
|
||||
self.assertIsInstance(updated_timestamp, str)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_coupon_validation_complete_integration(self):
|
||||
"""Test all coupon validation scenarios with real objects for 100% coverage."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "coupon_test@example.com")
|
||||
shop = Shop(
|
||||
name="Coupon Test Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A test shop"
|
||||
)
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create a product
|
||||
product = Product(title="Test Product", description="Test product")
|
||||
product.shop_id = shop.id
|
||||
product.price_in_cents = 2000 # $20.00
|
||||
product.is_physical = False
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Test 1: Multiple coupons (coupon stacking)
|
||||
cart1 = Cart(user=user)
|
||||
cart1.shop = shop
|
||||
self.dbsession.add(cart1)
|
||||
cart1.add_product(product)
|
||||
|
||||
# Create two coupons
|
||||
coupon1 = Coupon(
|
||||
shop=shop,
|
||||
code="COUPON1",
|
||||
description="First coupon",
|
||||
action_type="dollar-off",
|
||||
action_value=5 # $5.00 off
|
||||
)
|
||||
coupon2 = Coupon(
|
||||
shop=shop,
|
||||
code="COUPON2",
|
||||
description="Second coupon",
|
||||
action_type="dollar-off",
|
||||
action_value=3 # $3.00 off
|
||||
)
|
||||
self.dbsession.add(coupon1)
|
||||
self.dbsession.add(coupon2)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Add both coupons to cart to trigger stacking error
|
||||
from make_post_sell.models.cart_coupon import CartCoupon
|
||||
cart_coupon1 = CartCoupon(cart=cart1, coupon=coupon1)
|
||||
cart_coupon2 = CartCoupon(cart=cart1, coupon=coupon2)
|
||||
self.dbsession.add(cart_coupon1)
|
||||
self.dbsession.add(cart_coupon2)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Test coupon stacking validation
|
||||
errors = cart1.validate_attached_coupons()
|
||||
self.assertIn("We don't support coupon stacking. Please choose one coupon.", errors)
|
||||
|
||||
# Test 2: Invalid coupon (expired/disabled)
|
||||
cart2 = Cart(user=user)
|
||||
cart2.shop = shop
|
||||
self.dbsession.add(cart2)
|
||||
cart2.add_product(product)
|
||||
|
||||
expired_coupon = Coupon(
|
||||
shop=shop,
|
||||
code="EXPIRED",
|
||||
description="Expired coupon",
|
||||
action_type="dollar-off",
|
||||
action_value=5,
|
||||
expiration_date="2020-01-01" # Past date
|
||||
)
|
||||
self.dbsession.add(expired_coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
cart_coupon3 = CartCoupon(cart=cart2, coupon=expired_coupon)
|
||||
self.dbsession.add(cart_coupon3)
|
||||
self.dbsession.flush()
|
||||
|
||||
errors = cart2.validate_attached_coupons()
|
||||
self.assertIn("Sorry, the coupon 'EXPIRED' is not valid (expired or disabled).", errors)
|
||||
|
||||
# Test 3: Cart total doesn't meet qualifier
|
||||
cart3 = Cart(user=user)
|
||||
cart3.shop = shop
|
||||
self.dbsession.add(cart3)
|
||||
|
||||
# Create cheap product
|
||||
cheap_product = Product(title="Cheap Product", description="Cheap")
|
||||
cheap_product.shop_id = shop.id
|
||||
cheap_product.price_in_cents = 300 # $3.00
|
||||
cheap_product.is_physical = False
|
||||
self.dbsession.add(cheap_product)
|
||||
self.dbsession.flush()
|
||||
|
||||
cart3.add_product(cheap_product)
|
||||
|
||||
# Coupon requires minimum $10 but cart only has $3
|
||||
min_coupon = Coupon(
|
||||
shop=shop,
|
||||
code="MIN10",
|
||||
description="Minimum $10 coupon",
|
||||
action_type="dollar-off",
|
||||
action_value=2,
|
||||
cart_qualifier=10 # Requires $10.00 minimum
|
||||
)
|
||||
self.dbsession.add(min_coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
cart_coupon4 = CartCoupon(cart=cart3, coupon=min_coupon)
|
||||
self.dbsession.add(cart_coupon4)
|
||||
self.dbsession.flush()
|
||||
|
||||
errors = cart3.validate_attached_coupons()
|
||||
self.assertIn("Please review the terms for coupon 'MIN10': shop total not met.", errors)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_merge_with_coupon_integration(self):
|
||||
"""Test cart merging with coupons to cover coupon append logic."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "merge_test@example.com")
|
||||
shop = Shop(
|
||||
name="Merge Test Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A test shop"
|
||||
)
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create products
|
||||
product1 = Product(title="Product 1", description="Test product 1")
|
||||
product1.shop_id = shop.id
|
||||
product1.price_in_cents = 1000
|
||||
product1.is_physical = False
|
||||
|
||||
product2 = Product(title="Product 2", description="Test product 2")
|
||||
product2.shop_id = shop.id
|
||||
product2.price_in_cents = 1500
|
||||
product2.is_physical = False
|
||||
|
||||
self.dbsession.add(product1)
|
||||
self.dbsession.add(product2)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create carts
|
||||
cart1 = Cart(user=user)
|
||||
cart1.shop = shop
|
||||
cart2 = Cart(user=user)
|
||||
cart2.shop = shop
|
||||
self.dbsession.add(cart1)
|
||||
self.dbsession.add(cart2)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Add products to carts
|
||||
cart1.add_product(product1)
|
||||
cart2.add_product(product2)
|
||||
|
||||
# Create coupons
|
||||
coupon1 = Coupon(
|
||||
shop=shop,
|
||||
code="CART1COUPON",
|
||||
description="Cart 1 coupon",
|
||||
action_type="dollar-off",
|
||||
action_value=2
|
||||
)
|
||||
coupon2 = Coupon(
|
||||
shop=shop,
|
||||
code="CART2COUPON",
|
||||
description="Cart 2 coupon",
|
||||
action_type="dollar-off",
|
||||
action_value=3
|
||||
)
|
||||
coupon_shared = Coupon(
|
||||
shop=shop,
|
||||
code="SHARED",
|
||||
description="Shared coupon",
|
||||
action_type="dollar-off",
|
||||
action_value=1
|
||||
)
|
||||
self.dbsession.add(coupon1)
|
||||
self.dbsession.add(coupon2)
|
||||
self.dbsession.add(coupon_shared)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Add coupons to carts
|
||||
from make_post_sell.models.cart_coupon import CartCoupon
|
||||
|
||||
# Cart1 has coupon1 and shared coupon
|
||||
cart_coupon1 = CartCoupon(cart=cart1, coupon=coupon1)
|
||||
cart_coupon_shared1 = CartCoupon(cart=cart1, coupon=coupon_shared)
|
||||
self.dbsession.add(cart_coupon1)
|
||||
self.dbsession.add(cart_coupon_shared1)
|
||||
|
||||
# Cart2 has coupon2 and the same shared coupon
|
||||
cart_coupon2 = CartCoupon(cart=cart2, coupon=coupon2)
|
||||
cart_coupon_shared2 = CartCoupon(cart=cart2, coupon=coupon_shared)
|
||||
self.dbsession.add(cart_coupon2)
|
||||
self.dbsession.add(cart_coupon_shared2)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Count original coupons
|
||||
original_cart1_coupons = len(cart1.coupons)
|
||||
original_cart2_coupons = len(cart2.coupons)
|
||||
|
||||
# Merge cart2 into cart1 - this should trigger the coupon merge logic
|
||||
cart1.merge_in_cart(cart2)
|
||||
|
||||
# Verify products merged
|
||||
cart_data = cart1.get_cart()
|
||||
self.assertIn(str(product1.id), cart_data)
|
||||
self.assertIn(str(product2.id), cart_data)
|
||||
|
||||
# Verify coupons merged (should not duplicate shared coupon)
|
||||
# This exercises lines 152-153 in merge_in_cart
|
||||
merged_coupon_codes = [c.code for c in cart1.coupons]
|
||||
self.assertIn("CART1COUPON", merged_coupon_codes)
|
||||
self.assertIn("CART2COUPON", merged_coupon_codes)
|
||||
self.assertIn("SHARED", merged_coupon_codes)
|
||||
|
||||
# Should not have duplicated the shared coupon
|
||||
shared_count = merged_coupon_codes.count("SHARED")
|
||||
self.assertEqual(shared_count, 1)
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_final_coverage_lines_integration(self):
|
||||
"""Test remaining lines to achieve 100% coverage."""
|
||||
# Create real user and shop
|
||||
user = get_or_create_user_by_email(self.dbsession, "inventory_test@example.com")
|
||||
shop = Shop(
|
||||
name="Inventory Test Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A test shop"
|
||||
)
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
|
||||
# Create shop location for inventory management
|
||||
from make_post_sell.models.shop_location import ShopLocation
|
||||
shop_location = ShopLocation(
|
||||
shop=shop,
|
||||
name="Main Location",
|
||||
address="123 Main St",
|
||||
city="Test City",
|
||||
state="Test State",
|
||||
country="USA",
|
||||
postal_code="12345"
|
||||
)
|
||||
self.dbsession.add(shop_location)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create physical products for inventory testing
|
||||
physical_product = Product(title="Physical Product", description="Physical item")
|
||||
physical_product.shop_id = shop.id
|
||||
physical_product.price_in_cents = 1000 # $10.00
|
||||
physical_product.is_physical = True
|
||||
|
||||
self.dbsession.add(physical_product)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create inventory for the physical product
|
||||
from make_post_sell.models.inventory import Inventory
|
||||
inventory = Inventory(
|
||||
product=physical_product,
|
||||
shop_location=shop_location,
|
||||
quantity=5 # 5 items in stock
|
||||
)
|
||||
self.dbsession.add(inventory)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create cart with physical products
|
||||
cart = Cart(user=user)
|
||||
cart.shop = shop
|
||||
self.dbsession.add(cart)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Add 3 physical products to cart
|
||||
cart.add_product(physical_product)
|
||||
cart.add_product(physical_product)
|
||||
cart.add_product(physical_product)
|
||||
|
||||
# Test inventory check - should pass (3 needed, 5 available)
|
||||
errors = cart.check_inventory(shop_location)
|
||||
self.assertEqual(len(errors), 0) # No errors
|
||||
|
||||
# Add more products to exceed inventory
|
||||
cart.add_product(physical_product) # 4 total
|
||||
cart.add_product(physical_product) # 5 total
|
||||
cart.add_product(physical_product) # 6 total - exceeds inventory
|
||||
|
||||
# Test inventory check - should fail (6 needed, 5 available)
|
||||
errors = cart.check_inventory(shop_location)
|
||||
self.assertEqual(len(errors), 1)
|
||||
self.assertIn("Not enough stock for Physical Product", errors[0])
|
||||
self.assertIn("Needed: 6, Available: 5", errors[0])
|
||||
|
||||
# Test inventory update (reduce quantity back to 3 for successful update)
|
||||
cart.set_product_quantity(physical_product, 3)
|
||||
|
||||
# Update inventory - should reduce quantity from 5 to 2
|
||||
cart.update_inventory(shop_location)
|
||||
|
||||
# Verify inventory was reduced
|
||||
self.dbsession.refresh(inventory)
|
||||
self.assertEqual(inventory.quantity, 2) # 5 - 3 = 2
|
||||
|
||||
# Test physical product identification
|
||||
physical_products = cart.physical_products
|
||||
self.assertIn(str(physical_product.id), physical_products)
|
||||
self.assertEqual(physical_products[str(physical_product.id)], physical_product)
|
||||
|
||||
# Test handling cost updates for all methods
|
||||
cart.handling_option = "local_pickup"
|
||||
cart.update_handling_cost(shop_location)
|
||||
self.assertEqual(cart.handling_cost_in_cents, 0) # Pickup is free
|
||||
|
||||
# Set rates on shop location for testing
|
||||
shop_location.local_delivery_rate_in_cents = 500
|
||||
shop_location.local_shipping_rate_in_cents = 750
|
||||
shop_location.international_shipping_rate_in_cents = 1500
|
||||
self.dbsession.add(shop_location)
|
||||
self.dbsession.flush()
|
||||
|
||||
cart.handling_option = "local_delivery"
|
||||
cart.update_handling_cost(shop_location)
|
||||
self.assertEqual(cart.handling_cost_in_cents, 500)
|
||||
|
||||
cart.handling_option = "local_shipping"
|
||||
cart.update_handling_cost(shop_location)
|
||||
self.assertEqual(cart.handling_cost_in_cents, 750)
|
||||
|
||||
cart.handling_option = "international_shipping"
|
||||
cart.update_handling_cost(shop_location)
|
||||
self.assertEqual(cart.handling_cost_in_cents, 1500)
|
||||
|
||||
# Test remove handling if no physical products
|
||||
# First add a digital product and remove the physical one
|
||||
digital_product = Product(title="Digital Product", description="Digital item")
|
||||
digital_product.shop_id = shop.id
|
||||
digital_product.price_in_cents = 500
|
||||
digital_product.is_physical = False
|
||||
self.dbsession.add(digital_product)
|
||||
self.dbsession.flush()
|
||||
|
||||
cart.add_product(digital_product)
|
||||
cart.remove_product(physical_product) # Remove physical product
|
||||
|
||||
# Now cart has only digital products
|
||||
cart.remove_handling_if_no_physical_products()
|
||||
self.assertIsNone(cart.handling_option)
|
||||
self.assertEqual(cart.handling_cost_in_cents, 0)
|
||||
|
||||
# Test line 345-347: Cart.total property when discounted
|
||||
coupon = Coupon(
|
||||
shop=shop,
|
||||
code="DISCOUNT",
|
||||
description="Test discount",
|
||||
action_type="dollar-off",
|
||||
action_value=5
|
||||
)
|
||||
self.dbsession.add(coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
cart_with_discount = Cart(user=user)
|
||||
cart_with_discount.shop = shop
|
||||
self.dbsession.add(cart_with_discount)
|
||||
cart_with_discount.add_product(physical_product)
|
||||
|
||||
from make_post_sell.models.cart_coupon import CartCoupon
|
||||
cart_coupon = CartCoupon(cart=cart_with_discount, coupon=coupon)
|
||||
self.dbsession.add(cart_coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
# This should exercise line 345-347: if self.is_discounted return discounted_price
|
||||
discounted_total = cart_with_discount.total
|
||||
self.assertTrue(cart_with_discount.is_discounted)
|
||||
self.assertEqual(discounted_total, cart_with_discount.total_discounted_price)
|
||||
|
||||
# Test line 374: Cart.is_empty property
|
||||
empty_cart = Cart(user=user)
|
||||
empty_cart.shop = shop
|
||||
self.dbsession.add(empty_cart)
|
||||
self.assertTrue(empty_cart.is_empty) # This tests line 374: return self.count <= 0
|
||||
|
||||
# Test lines 408 and 416: Coupon validation max redemptions
|
||||
# Create coupon with max redemptions
|
||||
max_coupon = Coupon(
|
||||
shop=shop,
|
||||
code="MAXED",
|
||||
description="Max redemptions coupon",
|
||||
action_type="dollar-off",
|
||||
action_value=1,
|
||||
max_redemptions=1,
|
||||
max_redemptions_per_user=1
|
||||
)
|
||||
self.dbsession.add(max_coupon)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Create a redemption to test max_redemptions logic
|
||||
from make_post_sell.models.coupon_redemption import CouponRedemption
|
||||
from make_post_sell.models.invoice import Invoice
|
||||
|
||||
# Create invoice for redemption
|
||||
invoice = Invoice(user)
|
||||
invoice.shop = shop
|
||||
self.dbsession.add(invoice)
|
||||
self.dbsession.flush()
|
||||
|
||||
redemption = CouponRedemption(coupon=max_coupon, invoice=invoice, shop=shop, user=user)
|
||||
self.dbsession.add(redemption)
|
||||
self.dbsession.flush()
|
||||
|
||||
cart_max_test = Cart(user=user)
|
||||
cart_max_test.shop = shop
|
||||
self.dbsession.add(cart_max_test)
|
||||
cart_max_test.add_product(physical_product)
|
||||
|
||||
cart_coupon_max = CartCoupon(cart=cart_max_test, coupon=max_coupon)
|
||||
self.dbsession.add(cart_coupon_max)
|
||||
self.dbsession.flush()
|
||||
|
||||
# This should exercise lines 408 and 416
|
||||
errors = cart_max_test.validate_attached_coupons()
|
||||
self.assertTrue(len(errors) >= 1) # Should have errors for max redemptions
|
||||
|
||||
# Test lines 426-436: Inventory check with no inventory record
|
||||
# Create a product without inventory
|
||||
product_no_inventory = Product(title="No Inventory Product", description="No inventory")
|
||||
product_no_inventory.shop_id = shop.id
|
||||
product_no_inventory.price_in_cents = 500
|
||||
product_no_inventory.is_physical = True
|
||||
self.dbsession.add(product_no_inventory)
|
||||
self.dbsession.flush()
|
||||
|
||||
cart_no_inventory = Cart(user=user)
|
||||
cart_no_inventory.shop = shop
|
||||
self.dbsession.add(cart_no_inventory)
|
||||
cart_no_inventory.add_product(product_no_inventory)
|
||||
|
||||
# This should exercise lines 432-435: if inventory is None or insufficient
|
||||
inventory_errors = cart_no_inventory.check_inventory(shop_location)
|
||||
self.assertEqual(len(inventory_errors), 1)
|
||||
self.assertIn("Not enough stock", inventory_errors[0])
|
||||
self.assertIn("Available: 0", inventory_errors[0])
|
||||
|
||||
# Test lines 466-478: update_inventory with no inventory records
|
||||
# This should exercise the case where inventory is None (lines 472-475 won't run)
|
||||
cart_no_inventory.update_inventory(shop_location)
|
||||
# No inventory record means no changes made
|
||||
|
||||
transaction.commit()
|
||||
|
||||
def test_cart_functions_and_remaining_lines(self):
|
||||
"""Test the module-level functions to complete 100% coverage."""
|
||||
# Create test data
|
||||
user = get_or_create_user_by_email(self.dbsession, "functions_test@example.com")
|
||||
shop = Shop(
|
||||
name="Functions Test Shop",
|
||||
phone_number="555-555-5555",
|
||||
billing_address="123 Test St",
|
||||
description="A test shop"
|
||||
)
|
||||
self.dbsession.add(user)
|
||||
self.dbsession.add(shop)
|
||||
|
||||
cart1 = Cart(user=user)
|
||||
cart1.shop = shop
|
||||
cart2 = Cart(user=user)
|
||||
cart2.shop = shop
|
||||
self.dbsession.add(cart1)
|
||||
self.dbsession.add(cart2)
|
||||
self.dbsession.flush()
|
||||
|
||||
# Test line 486: get_all_carts function
|
||||
from make_post_sell.models.cart import get_all_carts
|
||||
all_carts = get_all_carts(self.dbsession)
|
||||
cart_ids = [str(c.id) for c in all_carts]
|
||||
self.assertIn(str(cart1.id), cart_ids)
|
||||
self.assertIn(str(cart2.id), cart_ids)
|
||||
|
||||
# Test line 491: get_cart_by_id function
|
||||
from make_post_sell.models.cart import get_cart_by_id
|
||||
retrieved_cart = get_cart_by_id(self.dbsession, cart1.id)
|
||||
self.assertEqual(retrieved_cart.id, cart1.id)
|
||||
|
||||
# Function tests completed
|
||||
|
||||
transaction.commit()
|
||||
|
|
@ -71,6 +71,478 @@ class TestCart(unittest.TestCase):
|
|||
def validate_attached_coupon_codes(self):
|
||||
"""Make sure all coupons attached to the cart met the terms."""
|
||||
pass
|
||||
|
||||
def test_cart_requires_payment_above_threshold(self):
|
||||
"""Test that cart requires payment when total is above $0.64 (64 cents)."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
# Mock the total_in_cents property to return value above threshold
|
||||
with mock.patch.object(type(cart), 'total_in_cents', new_callable=mock.PropertyMock) as mock_total:
|
||||
mock_total.return_value = 65 # Above 64 cent threshold
|
||||
self.assertTrue(cart.requires_payment)
|
||||
|
||||
def test_cart_requires_payment_at_threshold(self):
|
||||
"""Test that cart requires payment at exactly $0.64 (64 cents)."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
with mock.patch.object(type(cart), 'total_in_cents', new_callable=mock.PropertyMock) as mock_total:
|
||||
mock_total.return_value = 64 # At threshold
|
||||
self.assertFalse(cart.requires_payment)
|
||||
|
||||
def test_cart_requires_payment_below_threshold(self):
|
||||
"""Test that cart does not require payment when total is below $0.64."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
with mock.patch.object(type(cart), 'total_in_cents', new_callable=mock.PropertyMock) as mock_total:
|
||||
mock_total.return_value = 30 # Below 64 cent threshold
|
||||
self.assertFalse(cart.requires_payment)
|
||||
|
||||
def test_cart_requires_payment_zero_total(self):
|
||||
"""Test that cart does not require payment when total is zero (free cart)."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
with mock.patch.object(type(cart), 'total_in_cents', new_callable=mock.PropertyMock) as mock_total:
|
||||
mock_total.return_value = 0 # Free cart
|
||||
self.assertFalse(cart.requires_payment)
|
||||
|
||||
def test_cart_is_not_public_property(self):
|
||||
"""Test cart.is_not_public property logic."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Test the actual logic: is_not_public should return opposite of public
|
||||
cart.public = True
|
||||
self.assertFalse(cart.is_not_public)
|
||||
|
||||
cart.public = False
|
||||
self.assertTrue(cart.is_not_public)
|
||||
|
||||
def test_cart_empty_method(self):
|
||||
"""Test cart.empty() method clears cart contents."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.set_cart({"product1": 2, "product2": 3})
|
||||
|
||||
# Cart should have items
|
||||
self.assertNotEqual(cart.get_cart(), {})
|
||||
|
||||
# Empty should clear everything
|
||||
cart.empty()
|
||||
self.assertEqual(cart.get_cart(), {})
|
||||
|
||||
def test_cart_set_and_get_cart(self):
|
||||
"""Test cart.set_cart() and get_cart() methods."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
test_cart_data = {"product1": 2, "product2": 3}
|
||||
|
||||
cart.set_cart(test_cart_data)
|
||||
retrieved_cart = cart.get_cart()
|
||||
|
||||
self.assertEqual(retrieved_cart, test_cart_data)
|
||||
|
||||
def test_cart_add_product(self):
|
||||
"""Test cart.add_product() method."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock product with proper uuid_str
|
||||
mock_product = mock.Mock()
|
||||
mock_product.uuid_str = "product1" # This is what cart uses as key
|
||||
|
||||
# Add product
|
||||
cart.add_product(mock_product)
|
||||
|
||||
# Should have quantity 1
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 1)
|
||||
|
||||
# Add same product again
|
||||
cart.add_product(mock_product)
|
||||
|
||||
# Should have quantity 2
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 2)
|
||||
|
||||
def test_cart_remove_product(self):
|
||||
"""Test cart.remove_product() method."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock product
|
||||
mock_product = mock.Mock()
|
||||
mock_product.uuid_str = "product1"
|
||||
|
||||
# Add product first
|
||||
cart.add_product(mock_product)
|
||||
cart.add_product(mock_product) # quantity = 2
|
||||
|
||||
# Remove product completely (removes all quantity)
|
||||
cart.remove_product(mock_product)
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 0)
|
||||
|
||||
def test_cart_set_product_quantity(self):
|
||||
"""Test cart.set_product_quantity() method."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock product
|
||||
mock_product = mock.Mock()
|
||||
mock_product.uuid_str = "product1"
|
||||
|
||||
# Set quantity directly
|
||||
cart.set_product_quantity(mock_product, 5)
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 5)
|
||||
|
||||
# Set to zero should remove product
|
||||
cart.set_product_quantity(mock_product, 0)
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 0)
|
||||
|
||||
def test_cart_get_product_quantity(self):
|
||||
"""Test cart.get_product_quantity() method."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock product
|
||||
mock_product = mock.Mock()
|
||||
mock_product.uuid_str = "product1"
|
||||
|
||||
# Should return 0 for product not in cart
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 0)
|
||||
|
||||
# Add product and test
|
||||
cart.set_product_quantity(mock_product, 3)
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 3)
|
||||
|
||||
def test_cart_merge_in_cart(self):
|
||||
"""Test cart.merge_in_cart() method."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart1 = Cart()
|
||||
cart2 = Cart()
|
||||
|
||||
# Set up cart1
|
||||
cart1.set_cart({"product1": 2, "product2": 1})
|
||||
|
||||
# Set up cart2
|
||||
cart2.set_cart({"product2": 1, "product3": 3})
|
||||
|
||||
# Merge cart2 into cart1
|
||||
cart1.merge_in_cart(cart2)
|
||||
|
||||
result = cart1.get_cart()
|
||||
|
||||
# Should have product1: 2, product2: 2 (1+1), product3: 3
|
||||
self.assertEqual(result["product1"], 2)
|
||||
self.assertEqual(result["product2"], 2) # Merged
|
||||
self.assertEqual(result["product3"], 3)
|
||||
|
||||
def test_cart_validate_attached_coupons_no_coupons(self):
|
||||
"""Test coupon validation when no coupons attached."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock the coupons property to return empty list
|
||||
with mock.patch.object(type(cart), 'coupons', new_callable=mock.PropertyMock) as mock_coupons:
|
||||
mock_coupons.return_value = []
|
||||
|
||||
errors = cart.validate_attached_coupons()
|
||||
self.assertEqual(errors, [])
|
||||
|
||||
def test_cart_remove_handling_if_no_physical_products(self):
|
||||
"""Test remove_handling_if_no_physical_products method."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.handling_option = "pickup"
|
||||
cart.handling_cost_in_cents = 500
|
||||
|
||||
# Mock no physical products by mocking the property
|
||||
with mock.patch.object(type(cart), 'physical_products', new_callable=mock.PropertyMock) as mock_physical:
|
||||
mock_physical.return_value = {}
|
||||
|
||||
cart.remove_handling_if_no_physical_products()
|
||||
|
||||
# Should clear handling
|
||||
self.assertIsNone(cart.handling_option)
|
||||
self.assertEqual(cart.handling_cost_in_cents, 0)
|
||||
|
||||
def test_cart_bust_memoized_attributes(self):
|
||||
"""Test that _bust_memoized_attributes clears all cached properties."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Set some memoized attributes
|
||||
cart._count = 5
|
||||
cart._line_totals_in_cents = {"test": 100}
|
||||
cart._products = {"test": "product"}
|
||||
cart._physical_products = {"test": "physical"}
|
||||
cart._shops = {"test": "shop"}
|
||||
cart._shop_product_dict = {"test": []}
|
||||
cart._shop_totals_in_cents = {"test": 100}
|
||||
cart._shop_totals = {"test": 1.0}
|
||||
cart._discounted_shop_totals_in_cents = {"test": 90}
|
||||
cart._discounted_shop_totals = {"test": 0.9}
|
||||
cart._line_totals = {"test": 1.0}
|
||||
|
||||
# Call the bust method
|
||||
cart._bust_memoized_attributes()
|
||||
|
||||
# Verify all attributes are cleared
|
||||
self.assertFalse(hasattr(cart, "_count"))
|
||||
self.assertFalse(hasattr(cart, "_line_totals_in_cents"))
|
||||
self.assertFalse(hasattr(cart, "_products"))
|
||||
self.assertFalse(hasattr(cart, "_physical_products"))
|
||||
self.assertFalse(hasattr(cart, "_shops"))
|
||||
self.assertFalse(hasattr(cart, "_shop_product_dict"))
|
||||
self.assertFalse(hasattr(cart, "_shop_totals_in_cents"))
|
||||
self.assertFalse(hasattr(cart, "_shop_totals"))
|
||||
self.assertFalse(hasattr(cart, "_discounted_shop_totals_in_cents"))
|
||||
self.assertFalse(hasattr(cart, "_discounted_shop_totals"))
|
||||
self.assertFalse(hasattr(cart, "_line_totals"))
|
||||
|
||||
def test_cart_set_product_quantity_max_limit(self):
|
||||
"""Test that set_product_quantity enforces 999 max limit."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
mock_product = mock.Mock()
|
||||
mock_product.uuid_str = "product1"
|
||||
|
||||
# Test setting quantity above 999
|
||||
cart.set_product_quantity(mock_product, 1500)
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 999)
|
||||
|
||||
# Test setting quantity at 999
|
||||
cart.set_product_quantity(mock_product, 999)
|
||||
self.assertEqual(cart.get_product_quantity(mock_product), 999)
|
||||
|
||||
def test_cart_merge_in_cart_with_handling_and_coupons(self):
|
||||
"""Test merge_in_cart copies handling options."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart1 = Cart()
|
||||
cart2 = Cart()
|
||||
|
||||
# Set up cart1
|
||||
cart1.set_cart({"product1": 2})
|
||||
cart1.handling_cost_in_cents = 100
|
||||
|
||||
# Set up cart2 with handling
|
||||
cart2.set_cart({"product2": 3})
|
||||
cart2.handling_option = "shipping"
|
||||
cart2.handling_cost_in_cents = 200
|
||||
|
||||
# Initialize coupons as empty lists to avoid SQLAlchemy issues
|
||||
cart1.coupons = []
|
||||
cart2.coupons = []
|
||||
|
||||
# Merge cart2 into cart1
|
||||
cart1.merge_in_cart(cart2)
|
||||
|
||||
# Check products merged
|
||||
result = cart1.get_cart()
|
||||
self.assertEqual(result["product1"], 2)
|
||||
self.assertEqual(result["product2"], 3)
|
||||
|
||||
# Check handling option copied
|
||||
self.assertEqual(cart1.handling_option, "shipping")
|
||||
|
||||
def test_cart_line_totals_property(self):
|
||||
"""Test line_totals property converts cents to dollars."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock line_totals_in_cents to return test data
|
||||
with mock.patch.object(type(cart), 'line_totals_in_cents', new_callable=mock.PropertyMock) as mock_line_totals_cents:
|
||||
mock_line_totals_cents.return_value = {"product1": 1500, "product2": 2000}
|
||||
|
||||
line_totals = cart.line_totals
|
||||
|
||||
# Should convert cents to dollars
|
||||
self.assertEqual(line_totals["product1"], 15.00)
|
||||
self.assertEqual(line_totals["product2"], 20.00)
|
||||
|
||||
def test_cart_shop_totals_property(self):
|
||||
"""Test shop_totals property converts shop totals from cents to dollars."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock shop_totals_in_cents to return test data
|
||||
with mock.patch.object(type(cart), 'shop_totals_in_cents', new_callable=mock.PropertyMock) as mock_shop_totals_cents:
|
||||
mock_shop_totals_cents.return_value = {"shop1": 2500, "shop2": 3000}
|
||||
|
||||
shop_totals = cart.shop_totals
|
||||
|
||||
# Should convert cents to dollars
|
||||
self.assertEqual(shop_totals["shop1"], 25.00)
|
||||
self.assertEqual(shop_totals["shop2"], 30.00)
|
||||
|
||||
def test_cart_discounted_shop_totals_property(self):
|
||||
"""Test discounted_shop_totals property converts cents to dollars."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock discounted_shop_totals_in_cents to return test data
|
||||
with mock.patch.object(type(cart), 'discounted_shop_totals_in_cents', new_callable=mock.PropertyMock) as mock_discounted_cents:
|
||||
mock_discounted_cents.return_value = {"shop1": 2000, "shop2": 2500}
|
||||
|
||||
discounted_totals = cart.discounted_shop_totals
|
||||
|
||||
# Should convert cents to dollars
|
||||
self.assertEqual(discounted_totals["shop1"], 20.00)
|
||||
self.assertEqual(discounted_totals["shop2"], 25.00)
|
||||
|
||||
def test_cart_human_timestamp_properties(self):
|
||||
"""Test human_updated_timestamp and human_created_timestamp properties."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
from make_post_sell.lib.time_funcs import timestamp_to_ago_string
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock the timestamp_to_ago_string function
|
||||
with mock.patch('make_post_sell.models.cart.timestamp_to_ago_string') as mock_ago_string:
|
||||
mock_ago_string.return_value = "2 hours ago"
|
||||
|
||||
# Test human_updated_timestamp
|
||||
result = cart.human_updated_timestamp
|
||||
mock_ago_string.assert_called_with(cart.updated_timestamp)
|
||||
self.assertEqual(result, "2 hours ago")
|
||||
|
||||
# Reset mock for second test
|
||||
mock_ago_string.reset_mock()
|
||||
mock_ago_string.return_value = "3 hours ago"
|
||||
|
||||
# Test human_created_timestamp
|
||||
result = cart.human_created_timestamp
|
||||
mock_ago_string.assert_called_with(cart.created_timestamp)
|
||||
self.assertEqual(result, "3 hours ago")
|
||||
|
||||
def test_cart_total_price_in_cents_with_handling(self):
|
||||
"""Test total_price_in_cents includes handling cost."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.handling_cost_in_cents = 500 # $5.00 handling
|
||||
|
||||
# Mock line_totals_in_cents to return some total
|
||||
with mock.patch.object(type(cart), 'line_totals_in_cents', new_callable=mock.PropertyMock) as mock_line_totals:
|
||||
mock_line_totals.return_value = {"product1": 1000, "product2": 1500} # $25.00
|
||||
|
||||
total = cart.total_price_in_cents
|
||||
|
||||
# Should include handling cost: 1000 + 1500 + 500 = 3000
|
||||
self.assertEqual(total, 3000)
|
||||
|
||||
def test_cart_total_price_in_cents_no_handling(self):
|
||||
"""Test total_price_in_cents without handling cost."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.handling_cost_in_cents = 0 # No handling
|
||||
|
||||
# Mock line_totals_in_cents to return some total
|
||||
with mock.patch.object(type(cart), 'line_totals_in_cents', new_callable=mock.PropertyMock) as mock_line_totals:
|
||||
mock_line_totals.return_value = {"product1": 1000} # $10.00
|
||||
|
||||
total = cart.total_price_in_cents
|
||||
|
||||
# Should just be line totals: 1000
|
||||
self.assertEqual(total, 1000)
|
||||
|
||||
|
||||
|
||||
|
||||
def test_cart_update_handling_cost_local_pickup(self):
|
||||
"""Test update_handling_cost for local pickup (free)."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.handling_option = "local_pickup"
|
||||
|
||||
mock_shop_location = mock.Mock()
|
||||
|
||||
cart.update_handling_cost(mock_shop_location)
|
||||
|
||||
# Local pickup should be free
|
||||
self.assertEqual(cart.handling_cost_in_cents, 0)
|
||||
|
||||
def test_cart_update_handling_cost_local_delivery(self):
|
||||
"""Test update_handling_cost for local delivery."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.handling_option = "local_delivery"
|
||||
|
||||
mock_shop_location = mock.Mock()
|
||||
mock_shop_location.local_delivery_rate_in_cents = 500 # $5.00
|
||||
|
||||
cart.update_handling_cost(mock_shop_location)
|
||||
|
||||
# Should use local delivery rate
|
||||
self.assertEqual(cart.handling_cost_in_cents, 500)
|
||||
|
||||
def test_cart_update_handling_cost_local_shipping(self):
|
||||
"""Test update_handling_cost for local shipping."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.handling_option = "local_shipping"
|
||||
|
||||
mock_shop_location = mock.Mock()
|
||||
mock_shop_location.local_shipping_rate_in_cents = 750 # $7.50
|
||||
|
||||
cart.update_handling_cost(mock_shop_location)
|
||||
|
||||
# Should use local shipping rate
|
||||
self.assertEqual(cart.handling_cost_in_cents, 750)
|
||||
|
||||
def test_cart_update_handling_cost_international_shipping(self):
|
||||
"""Test update_handling_cost for international shipping."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
cart.handling_option = "international_shipping"
|
||||
|
||||
mock_shop_location = mock.Mock()
|
||||
mock_shop_location.international_shipping_rate_in_cents = 1500 # $15.00
|
||||
|
||||
cart.update_handling_cost(mock_shop_location)
|
||||
|
||||
# Should use international shipping rate
|
||||
self.assertEqual(cart.handling_cost_in_cents, 1500)
|
||||
|
||||
def test_cart_total_property_not_discounted(self):
|
||||
"""Test Cart.total property when cart is not discounted (line 347)."""
|
||||
from make_post_sell.models.cart import Cart
|
||||
|
||||
cart = Cart()
|
||||
|
||||
# Mock properties to ensure cart is not discounted
|
||||
with mock.patch.object(type(cart), 'is_discounted', new_callable=mock.PropertyMock) as mock_is_discounted:
|
||||
with mock.patch.object(type(cart), 'total_price', new_callable=mock.PropertyMock) as mock_total_price:
|
||||
mock_is_discounted.return_value = False # Not discounted
|
||||
mock_total_price.return_value = 25.00 # Regular price
|
||||
|
||||
# This should hit line 347: return self.total_price
|
||||
total = cart.total
|
||||
self.assertEqual(total, 25.00)
|
||||
mock_total_price.assert_called_once()
|
||||
|
||||
|
||||
class TestCoupon(unittest.TestCase):
|
||||
@mock.patch("make_post_sell.models.user.is_user_name_available", mock_always_true)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue