diff --git a/mealie/routes/groups/controller_labels.py b/mealie/routes/groups/controller_labels.py index 4a156eb94..0aa2b4ca3 100644 --- a/mealie/routes/groups/controller_labels.py +++ b/mealie/routes/groups/controller_labels.py @@ -54,7 +54,11 @@ class MultiPurposeLabelsController(BaseCrudController): @router.post("", response_model=MultiPurposeLabelOut) def create_one(self, data: MultiPurposeLabelCreate): - new_label = self.service.create_one(data) + try: + new_label = self.service.create_one(data) + except Exception as ex: + self.mixins.handle_exception(ex) + raise # handle_exception always raises; this satisfies static analysis self.publish_event( event_type=EventTypes.label_created, document_data=EventLabelData(operation=EventOperation.create, label_id=new_label.id), diff --git a/mealie/routes/organizers/controller_tags.py b/mealie/routes/organizers/controller_tags.py index e8ca20e07..09ab3e486 100644 --- a/mealie/routes/organizers/controller_tags.py +++ b/mealie/routes/organizers/controller_tags.py @@ -52,7 +52,7 @@ class TagController(BaseCrudController): def create_one(self, tag: TagIn): """Creates a Tag in the database""" save_data = mapper.cast(tag, TagSave, group_id=self.group_id) - new_tag = self.repo.create(save_data) + new_tag = self.mixins.create_one(save_data) if new_tag: self.publish_event( diff --git a/tests/integration_tests/category_tag_tool_tests/test_organizers_common.py b/tests/integration_tests/category_tag_tool_tests/test_organizers_common.py index 733bdebd2..462aba3d8 100644 --- a/tests/integration_tests/category_tag_tool_tests/test_organizers_common.py +++ b/tests/integration_tests/category_tag_tool_tests/test_organizers_common.py @@ -191,6 +191,24 @@ def test_organizer_association( assert response.status_code == 200 +@pytest.mark.parametrize("route", organizer_routes, ids=test_ids) +def test_organizer_create_duplicate_name_returns_400( + api_client: TestClient, + unique_user: TestUser, + route: RoutesBase, +): + # Regression test for #7582: POSTing a duplicate name to organizer endpoints + # leaked the sqlalchemy IntegrityError as an HTTP 500. The expected behavior, + # matching other organizer endpoints (foods, units, tools), is HTTP 400. + data = {"name": random_string(10)} + + response = api_client.post(route.base, json=data, headers=unique_user.token) + assert response.status_code == 201 + + response = api_client.post(route.base, json=data, headers=unique_user.token) + assert response.status_code == 400 + + @pytest.mark.parametrize("route, recipe_key", association_data, ids=test_ids) def test_organizer_get_by_slug( api_client: TestClient, diff --git a/tests/integration_tests/user_household_tests/test_shopping_list_labels.py b/tests/integration_tests/user_household_tests/test_shopping_list_labels.py index 8cba16666..791df417c 100644 --- a/tests/integration_tests/user_household_tests/test_shopping_list_labels.py +++ b/tests/integration_tests/user_household_tests/test_shopping_list_labels.py @@ -20,6 +20,21 @@ def create_labels(api_client: TestClient, unique_user: TestUser, count: int = 10 return labels +def test_label_create_duplicate_name_returns_400(api_client: TestClient, unique_user_fn_scoped: TestUser): + # Regression test for #7582: POSTing a duplicate label name leaked the + # sqlalchemy IntegrityError as an HTTP 500. The expected behavior, matching + # the other organizer endpoints (foods, units, tools, tags, categories), + # is HTTP 400. The function-scoped fixture avoids leaking the created label + # into the module-scoped `unique_user` group state used by sibling tests. + payload = {"name": random_string(), "color": "#ff0000"} + + response = api_client.post(api_routes.groups_labels, json=payload, headers=unique_user_fn_scoped.token) + assert response.status_code == 200 + + response = api_client.post(api_routes.groups_labels, json=payload, headers=unique_user_fn_scoped.token) + assert response.status_code == 400 + + def test_new_list_creates_list_labels(api_client: TestClient, unique_user: TestUser): labels = create_labels(api_client, unique_user) response = api_client.post(