Skip to content

Commit

Permalink
(PC-34314)[API] feat: add check_offer_name_does_not_contain_ean in of…
Browse files Browse the repository at this point in the history
…fer.api methods

Check added in `create_offer`, `update_offer`, `create_draft_offer`, `update_draft_offer`
  • Loading branch information
tcoudray-pass committed Jan 31, 2025
1 parent 902f025 commit a0ebec5
Show file tree
Hide file tree
Showing 11 changed files with 208 additions and 0 deletions.
9 changes: 9 additions & 0 deletions api/src/pcapi/core/offers/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ def create_draft_offer(
is_from_private_api: bool = True,
) -> models.Offer:
validation.check_offer_subcategory_is_valid(body.subcategory_id)
validation.check_offer_name_does_not_contain_ean(body.name)
if feature.FeatureToggle.WIP_SUGGESTED_SUBCATEGORIES.is_active():
venue = _get_coherent_venue_with_subcategory(venue, body.subcategory_id)

Expand Down Expand Up @@ -271,6 +272,9 @@ def update_draft_offer(offer: models.Offer, body: offers_schemas.PatchDraftOffer
if not updates:
return offer

if body.name:
validation.check_offer_name_does_not_contain_ean(body.name)

if "extraData" in updates:
formatted_extra_data = _format_extra_data(offer.subcategoryId, body.extra_data) or {}
validation.check_offer_extra_data(
Expand Down Expand Up @@ -309,6 +313,7 @@ def create_offer(
validation.check_is_duo_compliance(body.is_duo, subcategory)
validation.check_can_input_id_at_provider(provider, body.id_at_provider)
validation.check_can_input_id_at_provider_for_this_venue(venue.id, body.id_at_provider)
validation.check_offer_name_does_not_contain_ean(body.name)

fields = body.dict(by_alias=True)

Expand Down Expand Up @@ -399,6 +404,10 @@ def update_offer(
validation.check_can_input_id_at_provider(offer.lastProvider, id_at_provider)
validation.check_can_input_id_at_provider_for_this_venue(offer.venueId, id_at_provider, offer.id)

if "name" in updates:
name = get_field(offer, updates, "name", aliases=aliases)
validation.check_offer_name_does_not_contain_ean(name)

if (
"withdrawalType" in updates
or "withdrawalDelay" in updates
Expand Down
8 changes: 8 additions & 0 deletions api/src/pcapi/core/offers/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ class EanFormatException(OfferCreationBaseException):
pass


class EanInOfferNameException(OfferCreationBaseException):
def __init__(self) -> None:
super().__init__(
"name",
"Le titre d'une offre ne peut contenir l'EAN",
)


class ProductNotFoundForOfferCreation(OfferCreationBaseException):
pass

Expand Down
6 changes: 6 additions & 0 deletions api/src/pcapi/core/offers/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import decimal
from io import BytesIO
import logging
import re
import typing
import warnings

Expand Down Expand Up @@ -663,6 +664,11 @@ def check_ean_does_not_exist(ean: str | None, venue: offerers_models.Venue) -> N
raise exceptions.OfferAlreadyExists("ean")


def check_offer_name_does_not_contain_ean(offer_name: str) -> None:
if re.search(r"\d{13}", offer_name):
raise exceptions.EanInOfferNameException()


def _check_offer_has_product(offer: models.Offer | None) -> None:
if FeatureToggle.WIP_EAN_CREATION.is_active() and offer and offer.product is not None:
raise exceptions.OfferWithProductShouldNotUpdateExtraData()
Expand Down
53 changes: 53 additions & 0 deletions api/tests/core/offers/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1275,6 +1275,16 @@ def test_create_draft_offer_from_scratch(self):
assert not offer.product
assert models.Offer.query.count() == 1

def test_cannot_create_draft_offer_with_ean_in_name(self):
venue = offerers_factories.VenueFactory()
body = offers_schemas.PostDraftOfferBodyModel(
name="A pretty good offer 4759217254634",
subcategoryId=subcategories.SEANCE_CINE.id,
venueId=venue.id,
)
with pytest.raises(exceptions.EanInOfferNameException):
api.create_draft_offer(body, venue=venue)

@pytest.mark.features(WIP_SUGGESTED_SUBCATEGORIES=False)
def test_create_draft_digitaloffer_with_virtual_venue(self):
venue = offerers_factories.VirtualVenueFactory()
Expand Down Expand Up @@ -1458,6 +1468,25 @@ def test_basics(self):
assert offer.name == "New name"
assert offer.description == "New description"

def test_cannot_update_if_ean_in_name(self):
offer = factories.OfferFactory(
name="Name",
subcategoryId=subcategories.ESCAPE_GAME.id,
description="description",
)
body = offers_schemas.PatchDraftOfferBodyModel(
name="New name 4759217254634",
description="New description",
)

with pytest.raises(exceptions.EanInOfferNameException):
api.update_draft_offer(offer, body)

db.session.flush()

assert offer.name == "Name"
assert offer.description == "description"


@pytest.mark.usefixtures("db_session")
class CreateOfferTest:
Expand Down Expand Up @@ -1597,6 +1626,21 @@ def test_cannot_create_offer_when_invalid_subcategory(self):

assert error.value.errors["subcategory"] == ["La sous-catégorie de cette offre est inconnue"]

def test_cannot_create_offer_with_ean_in_name(self):
venue = offerers_factories.VenueFactory()
body = offers_schemas.CreateOffer(
name="An offer he can't refuse - 4759217254634",
subcategoryId=subcategories.SEANCE_CINE.id,
audioDisabilityCompliant=True,
mentalDisabilityCompliant=True,
motorDisabilityCompliant=True,
visualDisabilityCompliant=True,
)
with pytest.raises(exceptions.EanInOfferNameException) as error:
api.create_offer(body, venue=venue)

assert error.value.errors["name"] == ["Le titre d'une offre ne peut contenir l'EAN"]

def test_raise_error_if_extra_data_mandatory_fields_not_provided(self):
venue = offerers_factories.VenueFactory()
body = offers_schemas.CreateOffer(
Expand Down Expand Up @@ -1713,6 +1757,15 @@ def test_cannot_update_with_name_too_long(self):
assert error.value.errors == {"name": ["Vous devez saisir moins de 140 caractères"]}
assert models.Offer.query.one().name == "Old name"

def test_cannot_update_with_name_containing_ean(self):
offer = factories.OfferFactory(name="Old name", extraData={"ean": "1234567890124"})
body = offers_schemas.UpdateOffer(name="Luftballons 1234567890124")
with pytest.raises(exceptions.EanInOfferNameException) as error:
api.update_offer(offer, body)

assert error.value.errors == {"name": ["Le titre d'une offre ne peut contenir l'EAN"]}
assert models.Offer.query.one().name == "Old name"

def test_success_on_allocine_offer(self):
provider = providers_factories.AllocineProviderFactory(localClass="AllocineStocks")
offer = factories.OfferFactory(
Expand Down
14 changes: 14 additions & 0 deletions api/tests/core/offers/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,3 +959,17 @@ def test_check_offer_is_eligible_to_be_headline(self):
validation.check_offer_is_eligible_to_be_headline(offer_without_image)
msg = "Offers without images can not be set to the headline"
assert exc.value.errors["headlineOffer"] == [msg]


class CheckOfferNameDoesNotContainEanTest:
@pytest.mark.parametrize(
"offer_name",
[
"Mon offre de filou - 3759217254634",
"4759217254634",
"[3759217254634] J'essaye de mettre mon offre en avant",
],
)
def test_check_offer_name_does_not_contain_ean_should_raise(self, offer_name):
with pytest.raises(exceptions.EanInOfferNameException):
validation.check_offer_name_does_not_contain_ean(offer_name)
20 changes: 20 additions & 0 deletions api/tests/routes/pro/patch_draft_offer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,26 @@ def when_trying_to_patch_forbidden_attributes(self, client):
for key in forbidden_keys:
assert key in response.json

def when_trying_to_set_offer_name_with_ean(self, client):
offer = offers_factories.OfferFactory(
subcategoryId=subcategories.CARTE_MUSEE.id,
name="New name",
url="http://example.com/offer",
description="description",
)
offerers_factories.UserOffererFactory(
user__email="user@example.com",
offerer=offer.venue.managingOfferer,
)

data = {
"name": "Le Visible et l'invisible - Suivi de notes de travail - 9782070286256",
}
response = client.with_session_auth("user@example.com").patch(f"offers/draft/{offer.id}", json=data)

assert response.status_code == 400
assert response.json["name"] == ["Le titre d'une offre ne peut contenir l'EAN"]

@pytest.mark.features(WIP_EAN_CREATION=True)
def when_trying_to_patch_offer_with_product_with_new_ean(self, client):
user_offerer = offerers_factories.UserOffererFactory(user__email="user@example.com")
Expand Down
25 changes: 25 additions & 0 deletions api/tests/routes/pro/patch_offer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,31 @@ def should_fail_when_url_has_no_scheme(self, app, client):
assert response.status_code == 400
assert response.json["url"] == ['L\'URL doit commencer par "http://" ou "https://"']

def should_fail_when_name_contains_ean(self, app, client):
# Given
virtual_venue = offerers_factories.VirtualVenueFactory()
offer = offers_factories.OfferFactory(
venue=virtual_venue,
subcategoryId=subcategories.CARTE_MUSEE.id,
name="New name",
url="test@test.com",
description="description",
)
offerers_factories.UserOffererFactory(
user__email="user@example.com",
offerer=offer.venue.managingOfferer,
)

# When
data = {
"name": "Le Visible et l'invisible - Suivi de notes de travail - 9782070286256",
}
response = client.with_session_auth("user@example.com").patch(f"offers/{offer.id}", json=data)

# Then
assert response.status_code == 400
assert response.json["name"] == ["Le titre d'une offre ne peut contenir l'EAN"]

def should_fail_when_externalTicketOfficeUrl_has_no_scheme(self, app, client):
# Given
virtual_venue = offerers_factories.VirtualVenueFactory()
Expand Down
15 changes: 15 additions & 0 deletions api/tests/routes/pro/post_draft_offer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,21 @@ def test_fail_if_name_too_long(self, client):
assert response.status_code == 400
assert response.json["name"] == ["Le titre de l’offre doit faire au maximum 90 caractères."]

def test_fail_if_name_contains_ean(self, client):
venue = offerers_factories.VenueFactory()
offerer = venue.managingOfferer
offerers_factories.UserOffererFactory(offerer=offerer, user__email="user@example.com")

data = {
"name": "Le Visible et l'invisible - Suivi de notes de travail - 9782070286256",
"subcategoryId": subcategories.LIVRE_PAPIER.id,
"venueId": venue.id,
}
response = client.with_session_auth("user@example.com").post("/offers/draft", json=data)

assert response.status_code == 400
assert response.json["name"] == ["Le titre d'une offre ne peut contenir l'EAN"]

def test_fail_if_unknown_subcategory(self, client):
venue = offerers_factories.VenueFactory()
offerer = venue.managingOfferer
Expand Down
23 changes: 23 additions & 0 deletions api/tests/routes/pro/post_offer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,29 @@ def test_fail_if_name_too_long(self, client):
assert response.status_code == 400
assert response.json["name"] == ["Le titre de l’offre doit faire au maximum 90 caractères."]

def test_fail_if_name_contains_ean(self, client):
# Given
venue = offerers_factories.VenueFactory()
offerer = venue.managingOfferer
offerers_factories.UserOffererFactory(offerer=offerer, user__email="user@example.com")

# When
data = {
"venueId": venue.id,
"name": "Le Visible et l'invisible - Suivi de notes de travail - 9782070286256",
"subcategoryId": subcategories.LIVRE_PAPIER.id,
"withdrawalType": "no_ticket",
"mentalDisabilityCompliant": False,
"audioDisabilityCompliant": False,
"visualDisabilityCompliant": False,
"motorDisabilityCompliant": False,
}
response = client.with_session_auth("user@example.com").post("/offers", json=data)

# Then
assert response.status_code == 400
assert response.json["name"] == ["Le titre d'une offre ne peut contenir l'EAN"]

def test_fail_if_unknown_subcategory(self, client):
# Given
venue = offerers_factories.VenueFactory()
Expand Down
18 changes: 18 additions & 0 deletions api/tests/routes/public/individual_offers/v1/patch_product_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,24 @@ def test_update_subcategory_raises_error(self, client):
]
}

def test_update_with_ean_in_name_raises(self, client):
plain_api_key, venue_provider = self.setup_active_venue_provider()
product_offer = offers_factories.ThingOfferFactory(
venue=venue_provider.venue,
subcategoryId=subcategories.ABO_BIBLIOTHEQUE.id,
lastProvider=venue_provider.provider,
)

response = client.with_explicit_token(plain_api_key).patch(
self.endpoint_url,
json={
"offerId": product_offer.id,
"name": "Le Visible et l'invisible - Suivi de notes de travail - 9782070286256",
},
)
assert response.status_code == 400
assert response.json == {"name": ["Le titre d'une offre ne peut contenir l'EAN"]}

def test_update_unallowed_subcategory_product_raises_error(self, client):
venue, api_key = utils.create_offerer_provider_linked_to_venue()
product_offer = offers_factories.ThingOfferFactory(
Expand Down
17 changes: 17 additions & 0 deletions api/tests/routes/public/individual_offers/v1/post_product_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,23 @@ def test_event_category_not_accepted(self, client):
assert "categoryRelatedFields.category" in response.json
assert offers_models.Offer.query.first() is None

@pytest.mark.usefixtures("db_session")
def test_offer_with_ean_in_name_is_not_accepted(self, client):
plain_api_key, venue_provider = self.setup_active_venue_provider()

response = client.with_explicit_token(plain_api_key).post(
self.endpoint_url,
json={
"location": {"type": "physical", "venueId": venue_provider.venue.id},
"categoryRelatedFields": {"category": "LIVRE_NUMERIQUE"},
"accessibility": utils.ACCESSIBILITY_FIELDS,
"name": "Le Visible et l'invisible - Suivi de notes de travail - 9782070286256",
},
)

assert response.status_code == 400
assert response.json["name"] == ["Le titre d'une offre ne peut contenir l'EAN"]

@pytest.mark.usefixtures("db_session")
def test_venue_allowed(self, client):
utils.create_offerer_provider_linked_to_venue()
Expand Down

0 comments on commit a0ebec5

Please sign in to comment.