Repository: pulp/pulpcore

Issue:
Better exception handling
I believe it is a good practice to not send the exception/stack trace itself to the final user when dealing with errors.
Currently we're doing it in two places:
1. https://github.com/pulp/pulpcore/blob/2c573c4d03514e743b57357165d001c43acd90c2/pulpcore/app/viewsets/user.py#L221-L221

and 

2. https://github.com/pulp/pulpcore/blob/2c573c4d03514e743b57357165d001c43acd90c2/pulpcore/app/serializers/importer.py#L83-L83

Also, the second place deals directly using the `Exception` object, where it could use a more specific exception.


Discussion:

Resolving patch:
diff --git a/pulpcore/app/models/importer.py b/pulpcore/app/models/importer.py
--- a/pulpcore/app/models/importer.py
+++ b/pulpcore/app/models/importer.py
@@ -1,3 +1,4 @@
+from django.core.exceptions import ObjectDoesNotExist
 from django.db import models
 
 from pulpcore.app.models import (
@@ -51,8 +52,24 @@ def repo_mapping(self):
     @repo_mapping.setter
     def repo_mapping(self, mapping):
         self.repo_map.all().delete()
+        failed_repos = []
+        # Process a mapping to find the destinations that map to the sources
+        # Record all failing destinations to report to the user.
+        # Only create the mappings if *everything* worked - otherweise, fail with error.
+        the_map = {}
         for source, repo_name in mapping.items():
-            repo = Repository.objects.get(name=repo_name)
+            try:
+                repo = Repository.objects.get(name=repo_name)
+                the_map[source] = repo
+            except ObjectDoesNotExist:
+                failed_repos.append(repo_name)
+                continue
+
+        if failed_repos:  # We had a failure - report and leave
+            raise ObjectDoesNotExist(f"names: {str(failed_repos)}")
+
+        # Everything worked - create the mapping
+        for source, repo in the_map.items():
             self.repo_map.create(source_repo=source, repository=repo)
 
     class Meta:
diff --git a/pulpcore/app/serializers/importer.py b/pulpcore/app/serializers/importer.py
--- a/pulpcore/app/serializers/importer.py
+++ b/pulpcore/app/serializers/importer.py
@@ -1,6 +1,7 @@
 import os
 from gettext import gettext as _
 
+from django.core.exceptions import ObjectDoesNotExist
 from rest_framework import serializers
 from rest_framework.validators import UniqueValidator
 
@@ -78,9 +79,11 @@ def create(self, validated_data):
         importer = super().create(validated_data)
         try:
             importer.repo_mapping = repo_mapping
-        except Exception as err:
+        except ObjectDoesNotExist as err:
             importer.delete()
-            raise serializers.ValidationError(_("Bad repo mapping: {}").format(err))
+            raise serializers.ValidationError(
+                _("Failed to find repositories from repo_mapping: {}").format(err)
+            )
         else:
             return importer
 
diff --git a/pulpcore/app/viewsets/user.py b/pulpcore/app/viewsets/user.py
--- a/pulpcore/app/viewsets/user.py
+++ b/pulpcore/app/viewsets/user.py
@@ -217,8 +217,10 @@ def create(self, request, group_pk):
             )
         try:
             user = User.objects.get(**request.data)
-        except (User.DoesNotExist, FieldError) as exc:
-            raise ValidationError(str(exc))
+        except (User.DoesNotExist, FieldError):
+            raise ValidationError(
+                _("Could not find user {}").format(request.data.get("username", None))
+            )
         group.user_set.add(user)
         group.save()
         serializer = GroupUserSerializer(user, context={"request": request})


Test patch:
diff --git a/pulpcore/tests/functional/api/test_users_groups.py b/pulpcore/tests/functional/api/test_users_groups.py
--- a/pulpcore/tests/functional/api/test_users_groups.py
+++ b/pulpcore/tests/functional/api/test_users_groups.py
@@ -1,5 +1,6 @@
 import pytest
 import uuid
+from pulpcore.client.pulpcore.exceptions import ApiException
 
 
 @pytest.mark.parallel
@@ -58,3 +59,13 @@ def test_filter_groups(groups_api_client, gen_object_with_cleanup):
     assert len(groups.results) == 1
     groups = groups_api_client.list(name=f"{prefix}_newbees")
     assert len(groups.results) == 1
+
+
+@pytest.mark.parallel
+def test_groups_add_bad_user(groups_api_client, groups_users_api_client, gen_object_with_cleanup):
+    """Test that adding a nonexistent user to a group fails."""
+
+    prefix = str(uuid.uuid4())
+    group_href = gen_object_with_cleanup(groups_api_client, {"name": f"{prefix}_newbees"}).pulp_href
+    with pytest.raises(ApiException, match="foo"):
+        groups_users_api_client.create(group_href, group_user={"username": "foo"})