diff --git a/make_post_sell/tests/test_physical_product_handling.py b/make_post_sell/tests/test_physical_product_handling.py new file mode 100644 index 0000000..0561ebd --- /dev/null +++ b/make_post_sell/tests/test_physical_product_handling.py @@ -0,0 +1,206 @@ +# Functional test for physical product handling option selection +# Tests the cart handling option form that requires CSRF token + +from os import environ +import transaction +import unittest +import webtest +import re + +from ..models import get_tm_session +from ..models.meta import Base +from ..models.shop import Shop, get_shop_by_name +from ..models.user import get_or_create_user_by_email +from ..models.cart import get_all_carts +from ..models.product import get_all_products +from ..models.shop_location import ShopLocation +from ..models.price import Price + +from pyramid.paster import get_appsettings + + +class PhysicalProductHandlingTests(unittest.TestCase): + """Functional tests for physical product handling options with CSRF protection.""" + + def setUp(self): + from make_post_sell import main + + self.settings = get_appsettings("test.ini") + self.app = main({}, **self.settings) + self.testapp = webtest.TestApp(self.app) + 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) + + # Create test user + self.user = get_or_create_user_by_email(self.dbsession, "test@example.com") + self.user_creds = ("test@example.com", self.user.new_password()) + self.dbsession.add(self.user) + self.dbsession.flush() + transaction.manager.commit() + self.user = get_or_create_user_by_email(self.dbsession, "test@example.com") + + def tearDown(self): + self.testapp.get("/log-out") + transaction.abort() + Base.metadata.drop_all(bind=self.engine) + + def log_in_user(self): + self.testapp.post("/join-or-log-in", {"email": self.user_creds[0]}) + self.testapp.post( + "/verification-challenge", {"raw-otp": self.user_creds[1], "submit": True} + ) + + def get_csrf_token(self, shop_id): + """Extract CSRF token from search page.""" + url = f"/search?shop_id={shop_id}&keywords=test" + res = self.testapp.get(url, status=[200, 302]) + if res.status_int == 302: + res = res.follow() + html = res.body.decode("utf-8") + m = re.search( + r'name=["\']csrf_token["\'].*?value=["\']([^"\']+)["\']', + html, + flags=re.IGNORECASE | re.DOTALL, + ) + if not m: + self.fail(f"CSRF token not found on {url}") + return m.group(1) + + def test_physical_product_handling_option_selection(self): + """Test selecting handling options for physical products with CSRF token. + + This is a regression test to ensure the handling option form includes + CSRF token protection. Previously, the form was missing the CSRF token + which caused "400 Bad CSRF Token" errors when users tried to select + shipping options. + """ + from ..models.cart import Cart + from ..models.product import Product + + # 1. Create shop with user logged in + self.log_in_user() + + shop = Shop( + name="Test Physical Shop", + phone_number="555-555-5555", + billing_address="123 Test St\nTest City\n12345\n", + description="Shop selling physical goods" + ) + shop.stripe_public_api_key = environ["MPS_TEST_STRIPE_PUBLIC_API_KEY"] + shop.stripe_secret_api_key = environ["MPS_TEST_STRIPE_SECRET_API_KEY"] + shop.domain_name = "localhost.localhost" + self.dbsession.add(shop) + self.dbsession.flush() + + # 2. Create shop location with handling options enabled + shop_location = ShopLocation( + shop=shop, + name="Main Location", + address="123 Main St", + city="Test City", + state="TS", + country="US", + postal_code="12345", + ) + # Enable all handling options with costs + shop_location.local_pickup = True + shop_location.local_delivery = True + shop_location.local_delivery_rate_in_cents = 500 # $5 + shop_location.local_shipping = True + shop_location.local_shipping_rate_in_cents = 750 # $7.50 + shop_location.international_shipping = True + shop_location.international_shipping_rate_in_cents = 1500 # $15 + + self.dbsession.add(shop_location) + self.dbsession.flush() + + # 3. Create physical product + product = Product( + title="Physical Widget", + description="A physical product that needs shipping" + ) + product.shop_id = shop.id + product.price_in_cents = 2000 # $20 + product.is_physical = True + product.is_sellable = True + self.dbsession.add(product) + + # Create price history for the product + price = Price(product, 2000) + self.dbsession.add(price) + self.dbsession.flush() + + # 4. Create a cart with the physical product + cart = Cart(user=self.user) + cart.shop_id = shop.id + cart.set_product_quantity(product, 1) + self.dbsession.add(cart) + self.dbsession.flush() + + # Commit transaction + transaction.manager.commit() + + # Verify cart has physical products + self.assertEqual(cart.count, 1) + self.assertTrue(len(cart.physical_products) > 0, "Cart should have physical products") + + # 5. Get cart page and verify handling options section appears + cart_res = self.testapp.get(f"/cart/{cart.id}") + cart_body = cart_res.body.decode() + + # Verify handling options form is present + self.assertIn("Physical Handling Options", cart_body) + self.assertIn("Local Pickup", cart_body) + + # 6. Submit handling option form with CSRF token + # This is the critical test - the form MUST include CSRF token + csrf_token = self.get_csrf_token(shop.uuid_str) + + handling_res = self.testapp.post( + f"/cart/{cart.uuid_str}/handling-option", + { + "handling_option": "local_pickup", + "csrf_token": csrf_token, + }, + ) + + # Should redirect back to cart page + self.assertEqual(handling_res.status_int, 302) + final_res = handling_res.follow() + final_body = final_res.body.decode() + + # Verify success flash message appears + self.assertIn("Handling option set successfully", final_body) + + # 7. Verify handling option was actually set in database + self.dbsession.refresh(cart) + self.assertEqual(cart.handling_option, "local_pickup") + self.assertEqual(cart.handling_cost_in_cents, 0) # Pickup is free + + print("✓ CSRF TOKEN TEST PASSED: Handling option form includes CSRF protection") + print("✓ FORM SUBMISSION SUCCESSFUL: Handling option set without 400 error") + + # 8. Test other handling options to ensure they all work + for option, expected_cost in [ + ("local_delivery", 500), + ("local_shipping", 750), + ("international_shipping", 1500), + ]: + csrf_token = self.get_csrf_token(shop.uuid_str) + res = self.testapp.post( + f"/cart/{cart.uuid_str}/handling-option", + { + "handling_option": option, + "csrf_token": csrf_token, + }, + ) + self.assertEqual(res.status_int, 302) + self.assertIn("Handling option set successfully", res.follow().body.decode()) + + self.dbsession.refresh(cart) + self.assertEqual(cart.handling_option, option) + self.assertEqual(cart.handling_cost_in_cents, expected_cost) + + print(f"✓ ALL HANDLING OPTIONS TESTED: local_pickup, local_delivery, local_shipping, international_shipping")