add website_membership_signup_required: force to create user before add to cart a membership product
This commit is contained in:
parent
80f24bcc46
commit
c4d32c6add
22 changed files with 1202 additions and 0 deletions
|
|
@ -0,0 +1,202 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import re
|
||||
import time
|
||||
from urllib.parse import unquote
|
||||
|
||||
from odoo.fields import Date
|
||||
|
||||
from odoo.tests import HttpCase, tagged
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestMembershipSignupTour(HttpCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.website = cls.env.ref("website.default_website")
|
||||
cls.website.auth_signup_uninvited = "b2b"
|
||||
cls.website.membership_signup_required = True
|
||||
today = Date.to_date(Date.today())
|
||||
cls.membership_product = cls.env["product.template"].create({
|
||||
"name": "Membership Test Product",
|
||||
"membership": True,
|
||||
"list_price": 60.0,
|
||||
"is_published": True,
|
||||
"sale_ok": True,
|
||||
"website_id": cls.website.id,
|
||||
"membership_date_from": today.replace(month=1, day=1),
|
||||
"membership_date_to": today.replace(month=12, day=31),
|
||||
})
|
||||
cls.regular_product = cls.env["product.template"].create({
|
||||
"name": "Regular Test Product",
|
||||
"membership": False,
|
||||
"list_price": 5.0,
|
||||
"is_published": True,
|
||||
"sale_ok": True,
|
||||
"website_id": cls.website.id,
|
||||
})
|
||||
# Enable the custom/transfer payment provider if it is installed so the
|
||||
# cart page renders fully (no checkout/payment is exercised by the
|
||||
# tours, but it avoids warnings during cart rendering).
|
||||
provider = cls.env.ref("payment.payment_provider_transfer", raise_if_not_found=False)
|
||||
if provider and provider.state != "enabled":
|
||||
provider.write({"state": "enabled", "is_published": True})
|
||||
|
||||
def test_01_membership_signup_flow(self):
|
||||
self.start_tour(
|
||||
self.env["website"].get_client_action_url("/shop"),
|
||||
"website_membership_signup_required_tour",
|
||||
login=None,
|
||||
)
|
||||
# The tour submitted the signup form, so a new portal user must exist
|
||||
# and the cart of that user must contain the membership product.
|
||||
users = self.env["res.users"].search([
|
||||
("login", "like", "membership.tour.%@test.example.com"),
|
||||
])
|
||||
self.assertTrue(users, "Tour should have created a user via /web/signup")
|
||||
first = users.sorted("create_date")[-1]
|
||||
self.assertTrue(first.partner_id.firstname)
|
||||
self.assertTrue(first.partner_id.lastname)
|
||||
carts = self.env["sale.order"].search([
|
||||
("partner_id", "=", first.partner_id.id),
|
||||
("state", "=", "draft"),
|
||||
])
|
||||
self.assertTrue(carts)
|
||||
order_lines = carts.mapped("order_line").filtered(
|
||||
lambda l: l.product_id.product_tmpl_id == self.membership_product
|
||||
)
|
||||
self.assertTrue(order_lines, "Cart must contain the membership product")
|
||||
|
||||
def test_02_non_membership_no_intercept(self):
|
||||
self.start_tour(
|
||||
self.env["website"].get_client_action_url("/shop"),
|
||||
"website_membership_signup_required_non_intercept_tour",
|
||||
login=None,
|
||||
)
|
||||
|
||||
# -- HTTP-level tests (no Chrome) --------------------------------------
|
||||
|
||||
_CSRF_RE = re.compile(r'name="csrf_token"\s+value="([^"]+)"')
|
||||
|
||||
def _compute_csrf_token(self):
|
||||
"""Manually compute a valid csrf token bound to the opener's current
|
||||
session_id cookie. Mirrors `request.csrf_token()` so the test client
|
||||
does not need to scrape the rendered HTML (which some addons strip for
|
||||
specific authenticated users).
|
||||
"""
|
||||
sid = self.opener.cookies.get("session_id")
|
||||
if not sid:
|
||||
return None
|
||||
secret = self.env["ir.config_parameter"].sudo().get_param("database.secret")
|
||||
max_ts = int(time.time() + 31536000) # 1y, matches Odoo's default.
|
||||
msg = f"{sid}{max_ts}".encode("utf-8")
|
||||
hm = hmac.new(secret.encode("ascii"), msg, hashlib.sha1).hexdigest()
|
||||
return f"{hm}o{max_ts}"
|
||||
|
||||
def _cart_update(self, product, add_qty="1"):
|
||||
"""POST to /shop/cart/update with a valid csrf token. The csrf token
|
||||
is scraped from the rendered product page, with a fallback computed
|
||||
directly from the opener's session_id cookie (covers sessions whose
|
||||
rendered page strips the form for the logged user).
|
||||
"""
|
||||
product_url = "/shop/%s" % self.env["ir.http"]._slug(product)
|
||||
page = self.url_open(product_url, allow_redirects=True)
|
||||
self.assertEqual(
|
||||
page.status_code, 200,
|
||||
"Product page must render (got %s for %s)" % (page.status_code, product_url),
|
||||
)
|
||||
match = self._CSRF_RE.search(page.text)
|
||||
csrf_token = match.group(1) if match else self._compute_csrf_token()
|
||||
self.assertTrue(
|
||||
csrf_token,
|
||||
"Could not obtain a csrf_token (rendered page or session-based).",
|
||||
)
|
||||
return self.url_open(
|
||||
"/shop/cart/update",
|
||||
data={
|
||||
"product_id": str(product.product_variant_id.id),
|
||||
"add_qty": add_qty,
|
||||
"csrf_token": csrf_token,
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
|
||||
def _assert_redirect(self, resp):
|
||||
self.assertTrue(
|
||||
300 <= resp.status_code < 400,
|
||||
"Expected a HTTP redirect, got %s. Location: %s"
|
||||
% (resp.status_code, resp.headers.get("Location", "")),
|
||||
)
|
||||
|
||||
def test_03_anon_membership_redirect(self):
|
||||
"""RF-01 / RF-02: anonymous POST /shop/cart/update for a membership
|
||||
product must redirect to /web/signup?membership_signup=1&redirect=...
|
||||
without adding the product to the cart.
|
||||
"""
|
||||
resp = self._cart_update(self.membership_product)
|
||||
self._assert_redirect(resp)
|
||||
# The `Location` header is URL-encoded, so any path segment with
|
||||
# non-safe chars (here the product slug) arrives percent-encoded.
|
||||
# Assert on stable substrings that do not depend on the encoding of
|
||||
# each part of the URL (RF-02).
|
||||
location = resp.headers.get("Location", "")
|
||||
self.assertIn("/web/signup", location)
|
||||
self.assertIn("membership_signup=1", location)
|
||||
self.assertIn("redirect=", location)
|
||||
self.assertIn("membership-test-product", unquote(location))
|
||||
|
||||
def test_04_anon_regular_no_redirect(self):
|
||||
"""RF-05: anonymous POST /shop/cart/update for a non-membership
|
||||
product must follow the core flow (redirect to /shop/cart).
|
||||
"""
|
||||
resp = self._cart_update(self.regular_product)
|
||||
self._assert_redirect(resp)
|
||||
location = resp.headers.get("Location", "")
|
||||
self.assertIn("/shop/cart", location)
|
||||
self.assertNotIn("/web/signup", location)
|
||||
|
||||
def test_05_authenticated_membership_no_redirect(self):
|
||||
"""RF-06: an authenticated (non-public) user adding a membership
|
||||
product must NOT be redirected to /web/signup.
|
||||
"""
|
||||
self.authenticate("admin", "admin")
|
||||
resp = self._cart_update(self.membership_product)
|
||||
self._assert_redirect(resp)
|
||||
location = resp.headers.get("Location", "")
|
||||
self.assertIn("/shop/cart", location)
|
||||
self.assertNotIn("/web/signup", location)
|
||||
|
||||
def test_06_signup_marker_page_renders(self):
|
||||
"""RF-03: GET /web/signup?membership_signup=1&redirect=/shop/... must
|
||||
return 200 (not 404) on a b2b website thanks to the conditional
|
||||
override of `_get_signup_invitation_scope`.
|
||||
"""
|
||||
product_url = "/shop/%s" % self.env["ir.http"]._slug(self.membership_product)
|
||||
params = {"membership_signup": "1", "redirect": product_url}
|
||||
resp = self.url_open(
|
||||
"/web/signup?" + "&".join(f"{k}={v}" for k, v in params.items()),
|
||||
allow_redirects=False,
|
||||
)
|
||||
self.assertIn(resp.status_code, (200, 302))
|
||||
if resp.status_code == 302:
|
||||
# Could be redirected to login if a session already exists.
|
||||
self.assertNotIn("web/login", resp.headers.get("Location", ""))
|
||||
|
||||
def test_07_disabled_intercept_anon_membership(self):
|
||||
"""RF-07: with `membership_signup_required = False` on the website, an
|
||||
anonymous POST /shop/cart/update for a membership product must follow
|
||||
the core flow (no redirect to /web/signup).
|
||||
"""
|
||||
self.website.membership_signup_required = False
|
||||
try:
|
||||
resp = self._cart_update(self.membership_product)
|
||||
self._assert_redirect(resp)
|
||||
location = resp.headers.get("Location", "")
|
||||
self.assertIn("/shop/cart", location)
|
||||
self.assertNotIn("/web/signup", location)
|
||||
finally:
|
||||
self.website.membership_signup_required = True
|
||||
Loading…
Add table
Add a link
Reference in a new issue