Repository: certbot/certbot

Issue:
Revamp register subcommand
The cli subcommand `register` is currently used for account management commands. This causes weird spellings like `certbot register --deactivate`. So `register` should probably be changed to `account`. I did some preliminary work on this [here](https://github.com/cowlicks/letsencrypt/commit/368c49c38e34c560fe9cb470656a02609f4ea066). But missed things like cli flags (`--update-registration`) and documentation.


Discussion:
Just wanted to mention that providing the "account" verb in the CLI going forward also has good synergy with the decision to [rename the "registration" in ACME to "account"](https://github.com/ietf-wg-acme/acme/pull/210). When the spec is finalized it will be using "account".
Go ahead and make a PR!  The old `"auth"` verb is now `"certonly"`, so you can use that as a model.
We had a meeting about the Certbot command line awhile ago and decided we should make some changes here. In general, we decided that we should favor add more verbs/subcommands (which can be nicely grouped in our help as shown in #3883) rather than more flags to change behaviors of existing subcommands.

What this means here is that:

* `certbot register --update-registration` should become its own subcommand. I'm open to suggestions on the name here. To implement this, we can probably change the verb in `certbot.cli.HelpfulArgumentParser.parse_args` if `certbot register --update-registration` is used to preserve compatibility.
* `certbot register --deactivate` as implemented in #3571 should become something like `certbot unregister`.
* `certbot register` should only be used to create a new account with the ACME CA. We can potentially change the name of this subcommand but I personally like it given its new, more specific purpose.
I'm taking a look at this one at PyCon.
Here's a way to run a single problematic test with `pytest`:

```
pytest certbot/tests/main_test.py::MainTest::test_update_registration_unsafely -s
```
`-s` stops `pytest` from capturing terminal output.

On top of that, our tests were capturing terminal output. This breaks the test, but so you can step through it to see what's going on, you can make the following changes:
```
diff --git a/certbot/tests/main_test.py b/certbot/tests/main_test.py
index 22653ca3..c0a5b83f 100644
--- a/certbot/tests/main_test.py
+++ b/certbot/tests/main_test.py
@@ -624,10 +624,10 @@ class MainTest(test_util.ConfigTestCase):  # pylint: disable=too-many-public-met
         args = self.standard_args + args
 
         toy_stdout = stdout if stdout else six.StringIO()
-        with mock.patch('certbot.main.sys.stdout', new=toy_stdout):
-            with mock.patch('certbot.main.sys.stderr') as stderr:
-                ret = main.main(args[:])  # NOTE: parser can alter its args!
-        return ret, toy_stdout, stderr
+        #with mock.patch('certbot.main.sys.stdout', new=toy_stdout):
+        #    with mock.patch('certbot.main.sys.stderr') as stderr:
+        main.main(args[:])  # NOTE: parser can alter its args!
+        return None, toy_stdout, stderr
 
     def test_no_flags(self):
         with mock.patch('certbot.main.run') as mock_run:
```
Use https://stackoverflow.com/questions/12320863/how-do-you-take-a-git-diff-file-and-apply-it-to-a-local-branch-that-is-a-copy-o if you want to apply this diff with git.

To set a breakpoint, use `import ipdb; ipdb.set_trace()`.

Resolving patch:
diff --git a/certbot/cli.py b/certbot/cli.py
--- a/certbot/cli.py
+++ b/certbot/cli.py
@@ -101,6 +101,7 @@
 
 manage your account with Let's Encrypt:
     register        Create a Let's Encrypt ACME account
+    update_account  Update a Let's Encrypt ACME account
   --agree-tos       Agree to the ACME server's Subscriber Agreement
    -m EMAIL         Email address for important account notifications
 """
@@ -397,9 +398,14 @@ def _get_help_string(self, action):
     }),
     ("register", {
         "short": "Register for account with Let's Encrypt / other ACME server",
-        "opts": "Options for account registration & modification",
+        "opts": "Options for account registration",
         "usage": "\n\n  certbot register --email user@example.com [options]\n\n"
     }),
+    ("update_account", {
+        "short": "Update existing account with Let's Encrypt / other ACME server",
+        "opts": "Options for account modification",
+        "usage": "\n\n  certbot update_account --email updated_email@example.com [options]\n\n"
+    }),
     ("unregister", {
         "short": "Irrevocably deactivate your account",
         "opts": "Options for account deactivation.",
@@ -465,6 +471,7 @@ def __init__(self, args, plugins, detect_defaults=False):
             "install": main.install,
             "plugins": main.plugins_cmd,
             "register": main.register,
+            "update_account": main.update_account,
             "unregister": main.unregister,
             "renew": main.renew,
             "revoke": main.revoke,
@@ -993,21 +1000,21 @@ def prepare_and_parse_args(plugins, args, detect_defaults=False):  # pylint: dis
              "certificates. Updates to the Subscriber Agreement will still "
              "affect you, and will be effective 14 days after posting an "
              "update to the web site.")
+    # TODO: When `certbot register --update-registration` is fully deprecated,
+    # delete following helpful.add
     helpful.add(
         "register", "--update-registration", action="store_true",
-        default=flag_default("update_registration"),
-        help="With the register verb, indicates that details associated "
-             "with an existing registration, such as the e-mail address, "
-             "should be updated, rather than registering a new account.")
+        default=flag_default("update_registration"), dest="update_registration",
+        help=argparse.SUPPRESS)
     helpful.add(
-        ["register", "unregister", "automation"], "-m", "--email",
+        ["register", "update_account", "unregister", "automation"], "-m", "--email",
         default=flag_default("email"),
         help=config_help("email"))
-    helpful.add(["register", "automation"], "--eff-email", action="store_true",
+    helpful.add(["register", "update_account", "automation"], "--eff-email", action="store_true",
                 default=flag_default("eff_email"), dest="eff_email",
                 help="Share your e-mail address with EFF")
-    helpful.add(["register", "automation"], "--no-eff-email", action="store_false",
-                default=flag_default("eff_email"), dest="eff_email",
+    helpful.add(["register", "update_account", "automation"], "--no-eff-email",
+                action="store_false", default=flag_default("eff_email"), dest="eff_email",
                 help="Don't share your e-mail address with EFF")
     helpful.add(
         ["automation", "certonly", "run"],
diff --git a/certbot/main.py b/certbot/main.py
--- a/certbot/main.py
+++ b/certbot/main.py
@@ -654,7 +654,45 @@ def unregister(config, unused_plugins):
 
 
 def register(config, unused_plugins):
-    """Create or modify accounts on the server.
+    """Create accounts on the server.
+
+    :param config: Configuration object
+    :type config: interfaces.IConfig
+
+    :param unused_plugins: List of plugins (deprecated)
+    :type unused_plugins: `list` of `str`
+
+    :returns: `None` or a string indicating and error
+    :rtype: None or str
+
+    """
+    # TODO: When `certbot register --update-registration` is fully deprecated,
+    # delete the true case of if block
+    if config.update_registration:
+        msg = ("Usage 'certbot register --update-registration' is deprecated.\n"
+               "Please use 'cerbot update_account [options]' instead.\n")
+        logger.warning(msg)
+        return update_account(config, unused_plugins)
+
+    # Portion of _determine_account logic to see whether accounts already
+    # exist or not.
+    account_storage = account.AccountFileStorage(config)
+    accounts = account_storage.find_all()
+
+    if len(accounts) > 0:
+        # TODO: add a flag to register a duplicate account (this will
+        #       also require extending _determine_account's behavior
+        #       or else extracting the registration code from there)
+        return ("There is an existing account; registration of a "
+                "duplicate account with this command is currently "
+                "unsupported.")
+    # _determine_account will register an account
+    _determine_account(config)
+    return
+
+
+def update_account(config, unused_plugins):
+    """Modify accounts on the server.
 
     :param config: Configuration object
     :type config: interfaces.IConfig
@@ -673,20 +711,6 @@ def register(config, unused_plugins):
     reporter_util = zope.component.getUtility(interfaces.IReporter)
     add_msg = lambda m: reporter_util.add_message(m, reporter_util.MEDIUM_PRIORITY)
 
-    # registering a new account
-    if not config.update_registration:
-        if len(accounts) > 0:
-            # TODO: add a flag to register a duplicate account (this will
-            #       also require extending _determine_account's behavior
-            #       or else extracting the registration code from there)
-            return ("There is an existing account; registration of a "
-                    "duplicate account with this command is currently "
-                    "unsupported.")
-        # _determine_account will register an account
-        _determine_account(config)
-        return
-
-    # --update-registration
     if len(accounts) == 0:
         return "Could not find an existing account to update."
     if config.email is None:


Test patch:
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
@@ -1398,7 +1398,20 @@ def test_register(self):
                 x = self._call_no_clientmock(["register", "--email", "user@example.org"])
                 self.assertTrue("There is an existing account" in x[0])
 
-    def test_update_registration_no_existing_accounts(self):
+    def test_update_account_no_existing_accounts(self):
+        # with mock.patch('certbot.main.client') as mocked_client:
+        with mock.patch('certbot.main.account') as mocked_account:
+            mocked_storage = mock.MagicMock()
+            mocked_account.AccountFileStorage.return_value = mocked_storage
+            mocked_storage.find_all.return_value = []
+            x = self._call_no_clientmock(
+                ["update_account", "--email",
+                 "user@example.org"])
+            self.assertTrue("Could not find an existing account" in x[0])
+
+    # TODO: When `certbot register --update-registration` is fully deprecated,
+    # delete the following test
+    def test_update_registration_no_existing_accounts_deprecated(self):
         # with mock.patch('certbot.main.client') as mocked_client:
         with mock.patch('certbot.main.account') as mocked_account:
             mocked_storage = mock.MagicMock()
@@ -1409,7 +1422,9 @@ def test_update_registration_no_existing_accounts(self):
                  "user@example.org"])
             self.assertTrue("Could not find an existing account" in x[0])
 
-    def test_update_registration_unsafely(self):
+    # TODO: When `certbot register --update-registration` is fully deprecated,
+    # delete the following test
+    def test_update_registration_unsafely_deprecated(self):
         # This test will become obsolete when register --update-registration
         # supports removing an e-mail address from the account
         with mock.patch('certbot.main.account') as mocked_account:
@@ -1423,7 +1438,39 @@ def test_update_registration_unsafely(self):
 
     @mock.patch('certbot.main.display_ops.get_email')
     @test_util.patch_get_utility()
-    def test_update_registration_with_email(self, mock_utility, mock_email):
+    def test_update_account_with_email(self, mock_utility, mock_email):
+        email = "user@example.com"
+        mock_email.return_value = email
+        with mock.patch('certbot.eff.handle_subscription') as mock_handle:
+            with mock.patch('certbot.main._determine_account') as mocked_det:
+                with mock.patch('certbot.main.account') as mocked_account:
+                    with mock.patch('certbot.main.client') as mocked_client:
+                        mocked_storage = mock.MagicMock()
+                        mocked_account.AccountFileStorage.return_value = mocked_storage
+                        mocked_storage.find_all.return_value = ["an account"]
+                        mocked_det.return_value = (mock.MagicMock(), "foo")
+                        cb_client = mock.MagicMock()
+                        mocked_client.Client.return_value = cb_client
+                        x = self._call_no_clientmock(
+                            ["update_account"])
+                        # When registration change succeeds, the return value
+                        # of register() is None
+                        self.assertTrue(x[0] is None)
+                        # and we got supposedly did update the registration from
+                        # the server
+                        self.assertTrue(
+                            cb_client.acme.update_registration.called)
+                        # and we saved the updated registration on disk
+                        self.assertTrue(mocked_storage.save_regr.called)
+                        self.assertTrue(
+                            email in mock_utility().add_message.call_args[0][0])
+                        self.assertTrue(mock_handle.called)
+
+    # TODO: When `certbot register --update-registration` is fully deprecated,
+    # delete the following test
+    @mock.patch('certbot.main.display_ops.get_email')
+    @test_util.patch_get_utility()
+    def test_update_registration_with_email_deprecated(self, mock_utility, mock_email):
         email = "user@example.com"
         mock_email.return_value = email
         with mock.patch('certbot.eff.handle_subscription') as mock_handle:
diff --git a/tests/certbot-boulder-integration.sh b/tests/certbot-boulder-integration.sh
--- a/tests/certbot-boulder-integration.sh
+++ b/tests/certbot-boulder-integration.sh
@@ -207,10 +207,16 @@ common unregister
 
 common register --email ex1@domain.org,ex2@domain.org
 
+# TODO: When `certbot register --update-registration` is fully deprecated, delete the two following deprecated uses
+
 common register --update-registration --email ex1@domain.org
 
 common register --update-registration --email ex1@domain.org,ex2@domain.org
 
+common update_account --email example@domain.org
+
+common update_account --email ex1@domain.org,ex2@domain.org
+
 common plugins --init --prepare | grep webroot
 
 # We start a server listening on the port for the