Repository: certbot/certbot

Issue:
[Bug]: acme.errors.ValidationError provides no information
### OS

Fedora 41

### Installation method

DNF

### Certbot Version

python3-acme-3.1.0-1.fc41.noarch

### What happened?

I am using `python3-acme` in a Let's Encrypt client of my own creation — https://github.com/ipilcher/acg — and I have recently been receiving this error.

```
Traceback (most recent call last):
  File "/usr/local/bin/acg", line 303, in <module>
    main()
    ~~~~^^
  File "/usr/local/bin/acg", line 295, in main
    cert = execute_challenge(client, csr, args.hostname)
  File "/usr/local/bin/acg", line 242, in execute_challenge
    order = client.poll_and_finalize(order)
  File "/usr/lib/python3.13/site-packages/acme/client.py", line 189, in poll_and_finalize
    orderr = self.poll_authorizations(orderr, deadline)
  File "/usr/lib/python3.13/site-packages/acme/client.py", line 214, in poll_authorizations
    raise errors.ValidationError(failed)
acme.errors.ValidationError
```

### Expected behavior

The error message in the traceback should include some indication of the nature of the error.

### Relevant log output

```shell

```


Discussion:

Patch:
diff --git a/acme/src/acme/errors.py b/acme/src/acme/errors.py
index b8407da7e65..5e0a5379d12 100644
--- a/acme/src/acme/errors.py
+++ b/acme/src/acme/errors.py
@@ -107,6 +107,16 @@ def __init__(self, failed_authzrs: List['messages.AuthorizationResource']) -> No
         self.failed_authzrs = failed_authzrs
         super().__init__()
 
+    def __str__(self) -> str:
+        msg = []
+        for authzr in self.failed_authzrs:
+            msg.append(f'Authorization for {authzr.body.identifier.value} ' \
+                'failed due to one or more failed challenges:')
+            for challenge in authzr.body.challenges:
+                msg.append(f'  Challenge {challenge.chall.typ} failed ' \
+                    f'with error {str(challenge.error)}')
+        return '\n'.join(msg)
+
 
 class TimeoutError(Error):  # pylint: disable=redefined-builtin
     """Error for when polling an authorization or an order times out."""


Test patch:
diff --git a/acme/src/acme/_internal/tests/errors_test.py b/acme/src/acme/_internal/tests/errors_test.py
index 97f739d4047..531f16a2fa7 100644
--- a/acme/src/acme/_internal/tests/errors_test.py
+++ b/acme/src/acme/_internal/tests/errors_test.py
@@ -51,5 +51,36 @@ def test_repr(self):
                          'sentinel.AR2})' % repr(set()) == repr(self.invalid)
 
 
+class ValidationErrorTest(unittest.TestCase):
+    """Tests for acme.errors.ValidationError"""
+
+    def setUp(self):
+        from acme.errors import ValidationError
+        from acme.challenges import DNS01
+        from acme.messages import Error
+        from acme.messages import Authorization
+        from acme.messages import AuthorizationResource
+        from acme.messages import IDENTIFIER_FQDN
+        from acme.messages import ChallengeBody
+        from acme.messages import Identifier
+        self.challenge_error = Error(typ='custom', detail='bar')
+        failed_authzr = AuthorizationResource(
+            body=Authorization(
+                identifier=Identifier(typ=IDENTIFIER_FQDN, value="example.com"),
+                challenges=[ChallengeBody(
+                    chall=DNS01(),
+                    error=self.challenge_error,
+                )]
+            )
+        )
+        self.error = ValidationError([failed_authzr])
+
+    def test_repr(self):
+        err_message = str(self.error)
+        assert 'Authorization for example.com failed' in err_message
+        assert 'Challenge dns-01 failed' in err_message
+        assert str(self.challenge_error) in err_message
+
+
 if __name__ == "__main__":
     sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover