Repository: packit/packit-service
Issue: Cannot create Copr project when identifier contains not allowed characters

Sentry Issue: [PACKIT-SERVICE-4N2](https://sentry.io/organizations/red-hat-0p/issues/3545446876/?referrer=github_integration)

```
Cannot create a new Copr project (owner=packit-stg project=packit-specfile-91-fedora+epel chroots=['fedora-rawhide-x86_64', 'epel-9-x86_64', 'fedora-36-x86_64', 'fedora-35-x86_64']): name: Name must contain only letters, digits, underscores, dashes and dots.
```

@lbarcziova: we pass the job identifier directly as a suffix to the Copr project name, so in case the identifier contains not allowed characters, the Copr project cannot be created. The reporting in this case is also [confusing](https://github.com/packit/specfile/pull/91#issuecomment-1231939314). When creating the default Copr project name, for commit trigger we include the branch name, so that may cause the same issues.

TODO:
- [ ] check the Copr project name to be created and remove/modify the unwanted characters
- [ ] improve the reporting

Changed files:
tests/unit/test_copr_build.py, packit_service/worker/helpers/build/copr_build.py

diff --git a/packit_service/worker/helpers/build/copr_build.py b/packit_service/worker/helpers/build/copr_build.py
index 5b33ddb0..f9b1cf79 100644
--- a/packit_service/worker/helpers/build/copr_build.py
+++ b/packit_service/worker/helpers/build/copr_build.py
@@ -2,6 +2,7 @@
 # SPDX-License-Identifier: MIT
 
 import logging
+import re
 from datetime import datetime, timezone
 from typing import Iterable, List, Optional, Set, Tuple
 
@@ -126,11 +127,21 @@ class CoprBuildJobHelper(BaseBuildJobHelper):
             f"-{self.job_config.identifier}" if self.job_config.identifier else ""
         )
 
-        return (
+        copr_project_name = (
             f"{service_prefix}{namespace}-{self.project.repo}-{ref_identifier}"
             f"{configured_identifier}"
         )
 
+        return self.normalise_copr_project_name(copr_project_name)
+
+    @staticmethod
+    def normalise_copr_project_name(copr_project_name: str) -> str:
+        """
+        Transform not allowed characters for Copr project name to '-'
+        (name must contain only letters, digits, underscores, dashes and dots).
+        """
+        return re.sub(r"[^\\w.-]", "-", copr_project_name)
+
     @property
     def job_project(self) -> Optional[str]:
         """
@@ -912,16 +923,18 @@ class CoprBuildJobHelper(BaseBuildJobHelper):
             raise ex
         except PackitCoprProjectException as ex:
             msg = (
-                "We were not able to find Copr project"
-                f"({owner}/{self.job_project}) "
+                "We were not able to find or create Copr project"
+                f" `{owner}/{self.job_project}` "
                 "specified in the config with the following error:\\n"
-                f"```\\n{str(ex.__cause__)}\\n```\\n---\\n"
+                f"```\\n{str(ex)}\\n```\\n---\\n"
                 "Please check your configuration for:\\n\\n"
                 "1. typos in owner and project name (groups need to be prefixed with `@`)\\n"
-                "2. whether the project itself exists (Packit creates projects"
+                "2. whether the project name doesn't contain not allowed characters (only letters, "
+                "digits, underscores, dashes and dots must be used)\\n"
+                "3. whether the project itself exists (Packit creates projects"
                 " only in its own namespace)\\n"
-                "3. whether Packit is allowed to build in your Copr project\\n"
-                "4. whether your Copr project/group is not private"
+                "4. whether Packit is allowed to build in your Copr project\\n"
+                "5. whether your Copr project/group is not private"
             )
             self.status_reporter.comment(body=msg)
             raise ex
diff --git a/tests/unit/test_copr_build.py b/tests/unit/test_copr_build.py
index 6cd0d845..af135cc8 100644
--- a/tests/unit/test_copr_build.py
+++ b/tests/unit/test_copr_build.py
@@ -14,9 +14,6 @@ from copr.v3 import CoprRequestException, CoprAuthException
 from copr.v3.proxies.build import BuildProxy
 from flexmock import flexmock
 from munch import Munch
-
-import packit
-import packit_service
 from ogr.abstract import CommitStatus, GitProject
 from ogr.exceptions import GitForgeInternalError, GitlabAPIException, OgrNetworkError
 from ogr.services.github import GithubProject
@@ -26,11 +23,18 @@ from ogr.services.github.check_run import (
     create_github_check_run_output,
 )
 from ogr.services.gitlab import GitlabProject
+
+import packit
+import packit_service
 from packit.actions import ActionName
 from packit.api import PackitAPI
 from packit.config import JobConfig, JobConfigTriggerType, JobType, PackageConfig
 from packit.copr_helper import CoprHelper
-from packit.exceptions import FailedCreateSRPM, PackitCoprSettingsException
+from packit.exceptions import (
+    FailedCreateSRPM,
+    PackitCoprSettingsException,
+    PackitCoprProjectException,
+)
 from packit_service import sentry_integration
 from packit_service.config import ServiceConfig
 from packit_service.constants import (
@@ -2610,3 +2614,72 @@ def test_submit_copr_build(
             ownername="", projectname="", path="", buildopts=buildopts
         ).and_return(flexmock(id=0))
         helper.submit_copr_build()
+
+
+@pytest.mark.parametrize(
+    "raw_name,expected_name",
+    [
+        ("packit-specfile-91-fedora-epel", "packit-specfile-91-fedora-epel"),
+        ("packit-specfile-91-fedora+epel", "packit-specfile-91-fedora-epel"),
+        ("packit-specfile-my@fancy@branch", "packit-specfile-my-fancy-branch"),
+        ("packit-specfile-v23:1", "packit-specfile-v23-1"),
+    ],
+)
+def test_normalise_copr_project_name(raw_name, expected_name):
+    assert CoprBuildJobHelper.normalise_copr_project_name(raw_name) == expected_name
+
+
+def test_copr_build_invalid_copr_project_name(github_pr_event):
+    """Verify that comment we post when we fail to update chroots on our projects
+    is correct and not the one about permissions"""
+    helper = build_helper(
+        event=github_pr_event,
+        db_trigger=flexmock(
+            job_config_trigger_type=JobConfigTriggerType.pull_request,
+            id=123,
+            job_trigger_model_type=JobTriggerModelType.pull_request,
+        ),
+    )
+    # enforce that we are reporting on our own Copr project
+    helper.job_build.owner = "packit"
+    flexmock(copr_build).should_receive("get_valid_build_targets").and_return(
+        {"f31", "f32"}
+    )
+    flexmock(CoprHelper).should_receive("create_copr_project_if_not_exists").and_raise(
+        PackitCoprProjectException(
+            "Cannot create a new Copr project (owner=packit-stg project="
+            "packit-specfile-91-fedora+epel chroots=['fedora-rawhide-x86_64', "
+            "'epel-9-x86_64', 'fedora-36-x86_64', 'fedora-35-x86_64']): name: "
+            "Name must contain only letters, digits, underscores, dashes and dots.",
+        )
+    )
+    expected_body = (
+        "We were not able to find or create Copr project "
+        "`packit/the-example-namespace-the-example-repo-342` "
+        "specified in the config with the following error:\\n"
+        "```\\nCannot create a new Copr project (owner=packit-stg project="
+        "packit-specfile-91-fedora+epel chroots=['fedora-rawhide-x86_64', "
+        "'epel-9-x86_64', 'fedora-36-x86_64', 'fedora-35-x86_64']): name: "
+        "Name must contain only letters, digits, underscores, dashes and dots.\\n```\\n---\\n"
+        "Please check your configuration for:\\n\\n"
+        "1. typos in owner and project name (groups need to be prefixed with `@`)\\n"
+        "2. whether the project name doesn't contain not allowed characters (only letters, "
+        "digits, underscores, dashes and dots must be used)\\n"
+        "3. whether the project itself exists (Packit creates projects"
+        " only in its own namespace)\\n"
+        "4. whether Packit is allowed to build in your Copr project\\n"
+        "5. whether your Copr project/group is not private"
+    )
+    status_reporter = (
+        flexmock()
+        .should_receive("comment")
+        .with_args(body=expected_body)
+        .and_return()
+        .mock()
+    )
+
+    flexmock(CoprBuildJobHelper).should_receive("status_reporter").and_return(
+        status_reporter
+    )
+    with pytest.raises(PackitCoprProjectException):
+        helper.create_copr_project_if_not_exists()