import itertools import json import string from datetime import timedelta import pytest from django.conf import settings from django.contrib.auth.base_user import make_password from django.test import Client, TestCase from django.urls import reverse from django.utils.timezone import now from model_bakery import baker from club.models import Membership from core.baker_recipes import board_user, subscriber_user from core.models import User from counter.baker_recipes import refill_recipe, sale_recipe from counter.models import ( BillingInfo, Counter, Customer, Refilling, Selling, StudentCard, ) @pytest.mark.django_db class TestBillingInfo: @pytest.fixture def payload(self): return { "first_name": "Subscribed", "last_name": "User", "address_1": "3, rue de Troyes", "zip_code": "34301", "city": "Sète", "country": "FR", "phone_number": "0612345678", } def test_edit_infos(self, client: Client, payload: dict): user = subscriber_user.make() baker.make(BillingInfo, customer=user.customer) client.force_login(user) response = client.put( reverse("api:put_billing_info", args=[user.id]), json.dumps(payload), content_type="application/json", ) user.refresh_from_db() infos = BillingInfo.objects.get(customer__user=user) assert response.status_code == 200 assert hasattr(user.customer, "billing_infos") assert infos.customer == user.customer for key, val in payload.items(): assert getattr(infos, key) == val @pytest.mark.parametrize( "user_maker", [subscriber_user.make, lambda: baker.make(User)] ) @pytest.mark.django_db def test_create_infos(self, client: Client, user_maker, payload): user = user_maker() client.force_login(user) assert not BillingInfo.objects.filter(customer__user=user).exists() response = client.put( reverse("api:put_billing_info", args=[user.id]), json.dumps(payload), content_type="application/json", ) assert response.status_code == 200 user.refresh_from_db() assert hasattr(user, "customer") infos = BillingInfo.objects.get(customer__user=user) assert hasattr(user.customer, "billing_infos") assert infos.customer == user.customer for key, val in payload.items(): assert getattr(infos, key) == val def test_invalid_data(self, client: Client, payload: dict[str, str]): user = subscriber_user.make() client.force_login(user) # address_1, zip_code and country are missing del payload["city"] response = client.put( reverse("api:put_billing_info", args=[user.id]), json.dumps(payload), content_type="application/json", ) assert response.status_code == 422 user.customer.refresh_from_db() assert not hasattr(user.customer, "billing_infos") @pytest.mark.parametrize( ("operator_maker", "expected_code"), [ (subscriber_user.make, 403), (lambda: baker.make(User), 403), (lambda: baker.make(User, is_superuser=True), 200), ], ) def test_edit_other_user( self, client: Client, operator_maker, expected_code: int, payload: dict ): user = subscriber_user.make() client.force_login(operator_maker()) baker.make(BillingInfo, customer=user.customer) response = client.put( reverse("api:put_billing_info", args=[user.id]), json.dumps(payload), content_type="application/json", ) assert response.status_code == expected_code @pytest.mark.parametrize( "phone_number", ["+33612345678", "0612345678", "06 12 34 56 78", "06-12-34-56-78"], ) def test_phone_number_format( self, client: Client, payload: dict, phone_number: str ): """Test that various formats of phone numbers are accepted.""" user = subscriber_user.make() client.force_login(user) payload["phone_number"] = phone_number response = client.put( reverse("api:put_billing_info", args=[user.id]), json.dumps(payload), content_type="application/json", ) assert response.status_code == 200 infos = BillingInfo.objects.get(customer__user=user) assert infos.phone_number == "0612345678" assert infos.phone_number.country_code == 33 def test_foreign_phone_number(self, client: Client, payload: dict): """Test that a foreign phone number is accepted.""" user = subscriber_user.make() client.force_login(user) payload["phone_number"] = "+49612345678" response = client.put( reverse("api:put_billing_info", args=[user.id]), json.dumps(payload), content_type="application/json", ) assert response.status_code == 200 infos = BillingInfo.objects.get(customer__user=user) assert infos.phone_number.as_national == "06123 45678" assert infos.phone_number.country_code == 49 @pytest.mark.parametrize( "phone_number", ["061234567a", "06 12 34 56", "061234567879", "azertyuiop"] ) def test_invalid_phone_number( self, client: Client, payload: dict, phone_number: str ): """Test that invalid phone numbers are rejected.""" user = subscriber_user.make() client.force_login(user) payload["phone_number"] = phone_number response = client.put( reverse("api:put_billing_info", args=[user.id]), json.dumps(payload), content_type="application/json", ) assert response.status_code == 422 assert not BillingInfo.objects.filter(customer__user=user).exists() class TestStudentCard(TestCase): """Tests for adding and deleting Stundent Cards Test that an user can be found with it's student card. """ @classmethod def setUpTestData(cls): cls.customer = subscriber_user.make() cls.barmen = subscriber_user.make(password=make_password("plop")) cls.board_admin = board_user.make() cls.club_admin = baker.make(User) cls.root = baker.make(User, is_superuser=True) cls.subscriber = subscriber_user.make() cls.counter = baker.make(Counter, type="BAR") cls.counter.sellers.add(cls.barmen) cls.club_counter = baker.make(Counter) baker.make( Membership, start_date=now() - timedelta(days=30), club=cls.club_counter.club, role=settings.SITH_CLUB_ROLES_ID["Board member"], user=cls.club_admin, ) cls.valid_card = baker.make( StudentCard, customer=cls.customer.customer, uid="8A89B82018B0A0" ) def login_in_counter(self): self.client.post( reverse("counter:login", args=[self.counter.id]), {"username": self.barmen.username, "password": "plop"}, ) def invalid_uids(self) -> list[tuple[str, str]]: """Return a list of invalid uids, with the associated error message""" return [ ("8B90734A802A8", ""), # too short ( "8B90734A802A8FA", "Assurez-vous que cette valeur comporte au plus 14 caractères (actuellement 15).", ), # too long ("8b90734a802a9f", ""), # has lowercases (" " * 14, "Ce champ est obligatoire."), # empty ( self.customer.customer.student_card.uid, "Un objet Carte étudiante avec ce champ Uid existe déjà.", ), ] def test_search_user_with_student_card(self): self.login_in_counter() response = self.client.post( reverse("counter:details", args=[self.counter.id]), {"code": self.valid_card.uid}, ) assert response.url == reverse( "counter:click", kwargs={"counter_id": self.counter.id, "user_id": self.customer.pk}, ) def test_add_student_card_from_counter(self): self.login_in_counter() for uid in ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"]: customer = subscriber_user.make().customer response = self.client.post( reverse( "counter:add_student_card", kwargs={"customer_id": customer.pk} ), {"uid": uid}, HTTP_REFERER=reverse( "counter:click", kwargs={"counter_id": self.counter.id, "user_id": customer.pk}, ), ) assert response.status_code == 302 customer.refresh_from_db() assert hasattr(customer, "student_card") assert customer.student_card.uid == uid def test_add_student_card_from_counter_fail(self): self.login_in_counter() customer = subscriber_user.make().customer for uid, error_msg in self.invalid_uids(): response = self.client.post( reverse( "counter:add_student_card", kwargs={"customer_id": customer.pk} ), {"uid": uid}, HTTP_REFERER=reverse( "counter:click", kwargs={"counter_id": self.counter.id, "user_id": customer.pk}, ), ) self.assertContains(response, text="Cet UID est invalide") self.assertContains(response, text=error_msg) customer.refresh_from_db() assert not hasattr(customer, "student_card") def test_add_student_card_from_counter_unauthorized(self): def send_valid_request(counter_id): return self.client.post( reverse( "counter:add_student_card", kwargs={"customer_id": self.customer.pk} ), {"uid": "8B90734A802A8F"}, HTTP_REFERER=reverse( "counter:click", kwargs={"counter_id": counter_id, "user_id": self.customer.pk}, ), ) # Send to a counter where you aren't logged in assert send_valid_request(self.counter.id).status_code == 403 # Send to a non bar counter self.client.force_login(self.club_admin) assert send_valid_request(self.club_counter.id).status_code == 403 def test_delete_student_card_with_owner(self): self.client.force_login(self.customer) self.client.post( reverse( "counter:delete_student_card", kwargs={"customer_id": self.customer.customer.pk}, ) ) self.customer.customer.refresh_from_db() assert not hasattr(self.customer.customer, "student_card") def test_delete_student_card_with_admin_user(self): """Test that AE board members and root users can delete student cards""" for user in self.board_admin, self.root: self.client.force_login(user) self.client.post( reverse( "counter:delete_student_card", kwargs={"customer_id": self.customer.customer.pk}, ) ) self.customer.customer.refresh_from_db() assert not hasattr(self.customer.customer, "student_card") def test_delete_student_card_from_counter(self): self.login_in_counter() self.client.post( reverse( "counter:delete_student_card", kwargs={"customer_id": self.customer.customer.pk}, ), http_referer=reverse( "counter:click", kwargs={ "counter_id": self.counter.id, "user_id": self.customer.customer.pk, }, ), ) self.customer.customer.refresh_from_db() assert not hasattr(self.customer.customer, "student_card") def test_delete_student_card_fail(self): """Test that non-admin users cannot delete student cards""" self.client.force_login(self.subscriber) response = self.client.post( reverse( "counter:delete_student_card", kwargs={"customer_id": self.customer.customer.pk}, ) ) assert response.status_code == 403 self.subscriber.customer.refresh_from_db() assert not hasattr(self.subscriber.customer, "student_card") def test_add_student_card_from_user_preferences(self): users = [self.customer, self.board_admin, self.root] uids = ["8B90734A802A8F", "ABCAAAFAAFAAAB", "15248196326518"] for user, uid in itertools.product(users, uids): self.customer.customer.student_card.delete() self.client.force_login(user) response = self.client.post( reverse( "counter:add_student_card", kwargs={"customer_id": self.customer.customer.pk}, ), {"uid": uid}, ) assert response.status_code == 302 response = self.client.get(response.url) self.customer.customer.refresh_from_db() assert self.customer.customer.student_card.uid == uid self.assertContains(response, text="Carte enregistrée") def test_add_student_card_from_user_preferences_fail(self): customer = subscriber_user.make() self.client.force_login(customer) for uid, error_msg in self.invalid_uids(): url = reverse( "counter:add_student_card", kwargs={"customer_id": customer.customer.pk} ) response = self.client.post(url, {"uid": uid}) self.assertContains(response, text="Cet UID est invalide") self.assertContains(response, text=error_msg) customer.refresh_from_db() assert not hasattr(customer.customer, "student_card") class TestCustomerAccountId(TestCase): @classmethod def setUpTestData(cls): cls.user_a = User.objects.create( username="a", password="plop", email="a.a@a.fr" ) user_b = User.objects.create(username="b", password="plop", email="b.b@b.fr") user_c = User.objects.create(username="c", password="plop", email="c.c@c.fr") Customer.objects.create(user=cls.user_a, amount=10, account_id="1111a") Customer.objects.create(user=user_b, amount=0, account_id="9999z") Customer.objects.create(user=user_c, amount=0, account_id="12345f") def test_create_customer(self): user_d = User.objects.create(username="d", password="plop") customer, created = Customer.get_or_create(user_d) account_id = customer.account_id number = account_id[:-1] assert created is True assert number == "12346" assert len(account_id) == 6 assert account_id[-1] in string.ascii_lowercase assert customer.amount == 0 def test_get_existing_account(self): account, created = Customer.get_or_create(self.user_a) assert created is False assert account.account_id == "1111a" assert account.amount == 10 @pytest.mark.django_db def test_update_balance(): customers = baker.make(Customer, _quantity=5, _bulk_create=True) refills = [ *refill_recipe.prepare( customer=iter(customers), amount=iter([30, 30, 40, 50, 50]), _quantity=len(customers), _save_related=True, ), refill_recipe.prepare(customer=customers[0], amount=30, _save_related=True), refill_recipe.prepare(customer=customers[4], amount=10, _save_related=True), ] Refilling.objects.bulk_create(refills) sales = [ *sale_recipe.prepare( customer=iter(customers), _quantity=len(customers), unit_price=10, quantity=1, _save_related=True, ), *sale_recipe.prepare( customer=iter(customers[:3]), _quantity=3, unit_price=5, quantity=2, _save_related=True, ), sale_recipe.prepare( customer=customers[4], quantity=1, unit_price=50, _save_related=True ), ] Selling.objects.bulk_create(sales) # customer 0 = 40, customer 1 = 10€, customer 2 = 20€, # customer 3 = 40€, customer 4 = 0€ customers_qs = Customer.objects.filter(pk__in={c.pk for c in customers}) # put everything at zero to be sure the amounts were wrong beforehand customers_qs.update(amount=0) customers_qs.update_amount() for customer, amount in zip(customers, [40, 10, 20, 40, 0]): customer.refresh_from_db() assert customer.amount == amount