A UK buyer can now create their own account at /portal/signup/. The screen creates a Keycloak user through a least-privilege service account (ixg-provisioning: manage-users, view-users) and the matching Django row as a buyer, then sends them through the normal sign-in flow to the portal. Staff status is derived from the role in the OIDC backend rather than asserted, so a self-registered buyer is not staff and does not become staff on a later sign-in. The platform root and the RBAC middleware now route each role to its own portal, which also fixes a redirect loop that the staff fix exposed for non-staff buyers.
122 lines
5.6 KiB
Python
122 lines
5.6 KiB
Python
"""The public buyer sign-up screen.
|
|
|
|
Two things matter: the page is reachable without signing in, and the account it
|
|
creates is a plain buyer — the same kind of account everything else uses, with
|
|
no platform privileges. Keycloak is patched here; the live Admin API path is
|
|
exercised by the smoke test, not by the suite.
|
|
"""
|
|
from unittest.mock import patch
|
|
from urllib.parse import urlencode
|
|
|
|
from django.contrib.auth import get_user_model
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
|
|
from apps.users.keycloak import DuplicateEmailError, KeycloakUnavailableError
|
|
|
|
User = get_user_model()
|
|
|
|
SIGNUP_URL = "/portal/signup/"
|
|
|
|
VALID = {
|
|
"full_name": "Ada Greenfield",
|
|
"company_name": "Greenfield Foods Ltd",
|
|
"email": "signup-smoke-01@example.com",
|
|
"password": "a-long-enough-passphrase",
|
|
"confirm_password": "a-long-enough-passphrase",
|
|
}
|
|
|
|
|
|
class SignupViewTests(TestCase):
|
|
def test_the_page_is_public(self):
|
|
response = self.client.get(SIGNUP_URL)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "Create your buyer account")
|
|
|
|
@patch("apps.buyer.views.create_keycloak_user", return_value="kc-sub-0001")
|
|
def test_successful_signup_creates_both_records(self, create):
|
|
response = self.client.post(SIGNUP_URL, VALID)
|
|
|
|
expected = reverse("oidc_authentication_init") + "?" + urlencode({"next": "/portal/?welcome=1"})
|
|
self.assertRedirects(response, expected, fetch_redirect_response=False)
|
|
create.assert_called_once_with(
|
|
email=VALID["email"], full_name=VALID["full_name"], password=VALID["password"]
|
|
)
|
|
|
|
user = User.objects.get(email=VALID["email"])
|
|
self.assertEqual(user.role, "buyer")
|
|
self.assertFalse(user.is_staff)
|
|
self.assertTrue(user.is_active)
|
|
self.assertEqual(user.company_name, "Greenfield Foods Ltd")
|
|
self.assertEqual(user.keycloak_id, "kc-sub-0001")
|
|
|
|
def test_a_duplicate_email_is_refused_without_calling_keycloak(self):
|
|
User.objects.create_user(email=VALID["email"], role="buyer")
|
|
with patch("apps.buyer.views.create_keycloak_user") as create:
|
|
response = self.client.post(SIGNUP_URL, VALID)
|
|
create.assert_not_called()
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "An account with this email already exists")
|
|
|
|
@patch("apps.buyer.views.create_keycloak_user", side_effect=DuplicateEmailError("x"))
|
|
def test_a_keycloak_duplicate_is_refused_the_same_way(self, create):
|
|
response = self.client.post(SIGNUP_URL, VALID)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "An account with this email already exists")
|
|
self.assertFalse(User.objects.filter(email=VALID["email"]).exists())
|
|
|
|
@patch("apps.buyer.views.create_keycloak_user", side_effect=KeycloakUnavailableError("down"))
|
|
def test_an_unreachable_keycloak_is_reported_not_a_500(self, create):
|
|
response = self.client.post(SIGNUP_URL, VALID)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "could not create your account")
|
|
self.assertFalse(User.objects.filter(email=VALID["email"]).exists())
|
|
|
|
@patch("apps.buyer.views.create_keycloak_user", return_value="kc-sub-0002")
|
|
def test_mismatched_confirmation_is_refused(self, create):
|
|
response = self.client.post(SIGNUP_URL, {**VALID, "confirm_password": "something-else-entirely"})
|
|
create.assert_not_called()
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "The two passwords do not match")
|
|
self.assertFalse(User.objects.filter(email=VALID["email"]).exists())
|
|
|
|
@patch("apps.buyer.views.create_keycloak_user", return_value="kc-sub-0003")
|
|
def test_a_short_password_is_refused(self, create):
|
|
response = self.client.post(SIGNUP_URL, {**VALID, "password": "short", "confirm_password": "short"})
|
|
create.assert_not_called()
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "at least 12 characters")
|
|
|
|
@patch("apps.buyer.views.create_keycloak_user", return_value="kc-sub-0004")
|
|
def test_a_password_equal_to_the_email_is_refused(self, create):
|
|
password = VALID["email"]
|
|
response = self.client.post(
|
|
SIGNUP_URL, {**VALID, "password": password, "confirm_password": password}
|
|
)
|
|
create.assert_not_called()
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertContains(response, "not your email address or company name")
|
|
|
|
def test_a_signed_in_user_is_sent_to_their_portal(self):
|
|
User.objects.create_user(email="already-in@example.com", role="buyer", is_staff=False)
|
|
self.client.force_login(User.objects.get(email="already-in@example.com"))
|
|
response = self.client.get(SIGNUP_URL)
|
|
self.assertRedirects(response, "/", fetch_redirect_response=False)
|
|
|
|
|
|
class DashboardWelcomeTests(TestCase):
|
|
def test_the_welcome_message_shows_after_signup(self):
|
|
User.objects.create_user(
|
|
email="welcome-buyer@example.com", role="buyer", is_staff=False
|
|
)
|
|
self.client.force_login(User.objects.get(email="welcome-buyer@example.com"))
|
|
response = self.client.get("/portal/?welcome=1")
|
|
self.assertContains(response, "Your account is ready")
|
|
|
|
def test_no_welcome_message_on_a_normal_visit(self):
|
|
User.objects.create_user(
|
|
email="quiet-buyer@example.com", role="buyer", is_staff=False
|
|
)
|
|
self.client.force_login(User.objects.get(email="quiet-buyer@example.com"))
|
|
response = self.client.get("/portal/")
|
|
self.assertNotContains(response, "Your account is ready")
|