Repository: certbot/certbot

Issue:
`revoke` doesn't output any status
If I revoke:

```
$ ./letsencrypt-auto revoke --cert-path=/path/to/cert1.pem --key-path=/path/to/privkey1.pem
```

This is the output: 

```
Checking for new version...
Requesting root privileges to run letsencrypt...
   sudo /Users/jvanasco/.local/share/letsencrypt/bin/letsencrypt revoke --cert-path=/path/to/cert1.pem --key-path=/path/to/privkey1.pem
```

Did it work?  Let's try it again.

```
$ ./letsencrypt-auto revoke --cert-path=/path/to/cert1.pem --key-path=/path/to/privkey1.pem
```

An unexpected error occurred:
The request message was malformed :: Certificate already revoked
Please see the logfiles in /var/log/letsencrypt for more details.

The log is not "human readable" when it comes to figuring out if there was a success or not.  I can parse the info, I'm pretty sure most people can't.

This command really needs to output a success message.



Discussion:
I guess 'echo $?' is not user friendly enough and should only be expected to be used when the command has no output.

Yes. `echo $?` works, however, given how the rest of the client displays messages about what it has done, we should add a message here as well for better usability and consistency.


Resolving patch:
diff --git a/certbot/display/ops.py b/certbot/display/ops.py
--- a/certbot/display/ops.py
+++ b/certbot/display/ops.py
@@ -237,6 +237,19 @@ def success_renewal(domains):
             os.linesep.join(_gen_ssl_lab_urls(domains))),
         pause=False)
 
+def success_revocation(cert_path):
+    """Display a box confirming a certificate has been revoked.
+
+    :param list cert_path: path to certificate which was revoked.
+
+    """
+    z_util(interfaces.IDisplay).notification(
+        "Congratulations! You have successfully revoked the certificate "
+        "that was located at {0}{1}{1}".format(
+            cert_path,
+            os.linesep),
+        pause=False)
+
 
 def _gen_ssl_lab_urls(domains):
     """Returns a list of urls.
diff --git a/certbot/main.py b/certbot/main.py
--- a/certbot/main.py
+++ b/certbot/main.py
@@ -12,6 +12,7 @@
 
 from acme import jose
 from acme import messages
+from acme import errors as acme_errors
 
 import certbot
 
@@ -503,7 +504,12 @@ def revoke(config, unused_plugins):  # TODO: coop with renewal config
         key = acc.key
     acme = client.acme_from_config_key(config, key)
     cert = crypto_util.pyopenssl_load_certificate(config.cert_path[1])[0]
-    acme.revoke(jose.ComparableX509(cert))
+    try:
+        acme.revoke(jose.ComparableX509(cert))
+    except acme_errors.ClientError as e:
+        return e.message
+
+    display_ops.success_revocation(config.cert_path[0])
 
 
 def run(config, plugins):  # pylint: disable=too-many-branches,too-many-locals


Test patch:
diff --git a/certbot/tests/display/ops_test.py b/certbot/tests/display/ops_test.py
--- a/certbot/tests/display/ops_test.py
+++ b/certbot/tests/display/ops_test.py
@@ -336,6 +336,21 @@ def test_success_renewal(self, mock_util):
         for name in names:
             self.assertTrue(name in arg)
 
+class SuccessRevocationTest(unittest.TestCase):
+    # pylint: disable=too-few-public-methods
+    """Test the success revocation message."""
+    @classmethod
+    def _call(cls, path):
+        from certbot.display.ops import success_revocation
+        success_revocation(path)
+
+    @mock.patch("certbot.display.ops.z_util")
+    def test_success_revocation(self, mock_util):
+        mock_util().notification.return_value = None
+        path = "/path/to/cert.pem"
+        self._call(path)
+        mock_util().notification.assert_called_once()
+        self.assertTrue(path in mock_util().notification.call_args[0][0])
 
 if __name__ == "__main__":
     unittest.main()  # pragma: no cover
diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py
--- a/certbot/tests/main_test.py
+++ b/certbot/tests/main_test.py
@@ -3,6 +3,8 @@
 import shutil
 import tempfile
 import unittest
+import datetime
+import pytz
 
 import mock
 
@@ -13,6 +15,11 @@
 from certbot import errors
 from certbot.plugins import disco as plugins_disco
 
+from certbot.tests import test_util
+from acme import jose
+
+CERT_PATH = test_util.vector_path('cert.pem')
+KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem"))
 
 class MainTest(unittest.TestCase):
     def setUp(self):
@@ -110,6 +117,61 @@ def _assert_no_pause(self, message, pause=True):
         # pylint: disable=unused-argument
         self.assertFalse(pause)
 
+class RevokeTest(unittest.TestCase):
+    """Tests for certbot.main.revoke."""
+
+    def setUp(self):
+        self.tempdir_path = tempfile.mkdtemp()
+        shutil.copy(CERT_PATH, self.tempdir_path)
+        self.tmp_cert_path = os.path.abspath(os.path.join(self.tempdir_path,
+            'cert.pem'))
+
+        self.patches = [
+            mock.patch('acme.client.Client'),
+            mock.patch('certbot.client.Client'),
+            mock.patch('certbot.main._determine_account'),
+            mock.patch('certbot.main.display_ops.success_revocation')
+        ]
+        self.mock_acme_client = self.patches[0].start()
+        self.patches[1].start()
+        self.mock_determine_account = self.patches[2].start()
+        self.mock_success_revoke = self.patches[3].start()
+
+        from certbot.account import Account
+
+        self.regr = mock.MagicMock()
+        self.meta = Account.Meta(
+            creation_host="test.certbot.org",
+            creation_dt=datetime.datetime(
+                2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC))
+        self.acc = Account(self.regr, KEY, self.meta)
+
+        self.mock_determine_account.return_value = (self.acc, None)
+
+
+    def tearDown(self):
+        shutil.rmtree(self.tempdir_path)
+        for patch in self.patches:
+            patch.stop()
+
+    def _call(self):
+        args = 'revoke --cert-path={0}'.format(self.tmp_cert_path).split()
+        plugins = plugins_disco.PluginsRegistry.find_all()
+        config = configuration.NamespaceConfig(
+            cli.prepare_and_parse_args(plugins, args))
+
+        from certbot.main import revoke
+        revoke(config, plugins)
+
+    def test_revocation_success(self):
+        self._call()
+        self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path)
+
+    def test_revocation_error(self):
+        from acme import errors as acme_errors
+        self.mock_acme_client.side_effect = acme_errors.ClientError()
+        self.assertRaises(acme_errors.ClientError, self._call)
+        self.mock_success_revoke.assert_not_called()
 
 class SetupLogFileHandlerTest(unittest.TestCase):
     """Tests for certbot.main.setup_log_file_handler."""