Repository: zulip/zulip-terminal
Pull request #1316 (2023-03-04 06:55:29)

Documentation: Improve commit message style & Add section on Structuring Commits
<!-- Please see https://github.com/zulip/zulip-terminal#contributor-guidelines ! -->

**What does this PR do?**  <!-- Overall description goes here -->

- Clarify the commit message style section
- Add a new explicit section on structuring commits, discussing 'developmental-style' vs 'minimal coherent style' commits, and why the latter is useful

More notes in the commits.

<!-- If fixing a filed bug or new feature, add 'Fixes #<issue>' or 'Partial fix for #<issue>' -->

<!-- Add a link to a discussion on chat.zulip.org, if relevant -->

**Tested?** <!-- Fine to leave some of these unchecked if this is a draft/work-in-progress -->
- [x] Manually
- [ ] Existing tests (adapted, if necessary)
- [ ] New tests added (for any new behavior)
- [ ] Passed linting & tests (each commit)
<!-- Code must pass CI (GitHub Actions) before merging - look for the green tick! -->

**Notes & Questions** <!-- if any; add/delete/fill-in as appropriate -->

Feedback welcome, particularly from new contributors!


Patch:
diff --git a/README.md b/README.md
index f57f9dc..287142d 100644
--- a/README.md
+++ b/README.md
@@ -356,36 +356,113 @@ the changes.
 You can also use the tools individually on a file or directory, eg.
 `black zulipterminal` or `isort tests/model/test_model.py`
 
-#### Commit Style
+#### Structuring Commits - speeding up reviews, merging & development
+
+As you work locally, investigating changes to make, it's common to make a
+series of small commits to store your progress.
+Often this can include commits that fix linting or testing issues in the
+previous commit(s).
+These are **developmental-style commits** - and almost everyone is likely to
+write commits in this style to some degree.
+
+Developmental-style commits store the changes just fine for you right now.
+However, when sharing your code, commit messages are a great place to instead
+communicate to others what you're changing and why. Tidying the structure can
+also make it easier and quicker for a reader to understand changes, and that you
+respect their time. One example is that very large single commits can take a
+lot of time to review, compared to if they are split up. Another is if you fix
+the tests/linting in a commit: which commit (or commits!) does this fix, and if
+it's in the same branch/PR, why isn't the original commit just fixed instead?
+
+Therefore, when creating a Pull Request (PR), please consider that your code is
+**more likely to be merged, more quickly, if it is easier to read, understand
+and review** - and a big part of that is how you structure your changes into
+commits, and describe those changes in commit messages.
+
+To be productive and make it easier for your PRs to be reviewed and updated, we
+follow an approach taken at
+[Zulip](https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html)
+and elsewhere, aiming for PRs to consist of a series of **minimal coherent**
+commits:
+* **Minimal**:
+  Look at each commit. Does it make different kinds of changes to lots of
+  files? Are you treating the title of a commit as a list of changes?  Consider
+  how to break down a large change into a sequence of smaller changes that you
+  can individually easily describe and that can flow after one another.
+* **Coherent**:
+  Each commit should individually pass all linting and existing automated
+  tests, and if you add new behavior, then add or extend the tests to cover it.
+
+Note that keeping to these principles can give other benefits, both before,
+during and after reviewing a PR, including:
+* Minimal commits can always later be *squashed* (combined) together - splitting commits is more challenging!
+* Coherent commits mean nothing should be broken between and after commits
+  * If a new commit in your branch breaks the linting/tests, it's likely that commit at fault!
+  * This improves the utility of `git bisect` in your branch or on `main`
+* These commits are easier to *reorder* in a branch
+  * Commits at the start of a branch (or can be moved there), may be merged early to simplify a branch
+* Understanding what changes were made and why is easier when reading a git log comprising these commits
+
+We now enforce a limited aspect of the *coherent* nature of commits in a PR in
+a job as part of our Continuous Integration (CI), *Ensure isolated PR commits*,
+which essentially runs `make check` on each commit in your branch.  You can
+replicate this locally before pushing to GitHub using `tools/check-branch`.
+
+While small or proof-of-concept PRs are initially fine to push as they are,
+they will likely only be reviewed based on the overall changes. Generally if
+individual commits look like they have a developmental style then reviewers are
+likely to give less specific feedback, and minimal coherent commits are certain
+to be requested before merging.
+
+**Restructuring commands** - Most restructuring relies upon *interactive
+rebasing* (eg. `git rebase -i upstream/main`), but consider searching online
+for specific actions, as well as searching or asking in **#git help** or
+**#learning** on chat.zulip.org.
+
+**Self review** - Another useful approach is to review your own commits both
+locally (see [Zulip
+suggestions](https://zulip.readthedocs.io/en/latest/git/setup.html#get-a-graphical-client))
+and after you push to GitHub. This allows you to inspect and fix anything that
+looks out of place, which someone is likely to pick up in their review, helping
+your submissions look more polished, as well as again indicating that you
+respect reviewers' time.
+
+#### Commit Message Style
 
 We aim to follow a standard commit style to keep the `git log` consistent and easy to read.
 
-Much like working with code, it's great to refer to the git log, for the style we're actively using.
-
-Our overall style for commit structure and messages broadly follows the general [Zulip version control guidelines](http://zulip.readthedocs.io/en/latest/contributing/version-control.html), so we recommend reading that first.
-
-Our commit titles have slight variations from the general Zulip style, with each:
-* starting with one or more areas in lower case, followed by a colon and space
-* area being slash-separated modified files without extensions, or the type of the change
-* ending with a concise description starting with a capital and ending with a full-stop (period)
-* having a maximum overall length of 72 (fitting github web interface without abbreviation)
-
-Some example commit titles:
-* `file3/file1/file2: Improve behavior of something.` - a general commit updating files `file1.txt`, `file2.py` and `file3.md`
-* `refactor: file1/file2: Extract some common function.` - a pure refactor which doesn't change the functional behavior
-* `bugfix: file1: Avoid some noticeable bug.` - an ideally small commit to fix a bug
-* `tests: file1: Improve test for something.` - only improve tests for `file1`, likely in `test_file1.py`
-* `requirements: Upgrade some-dependency from 9.2 to 9.3.` - upgrade a dependency from version 9.2 to version 9.3
-
-Generally with changes to code we request you update linting and tests to pass on a per-commit basis (not just per pull request).
-If you update tests, you can add eg. `Tests updated.` in your commit text.
-
-Ideally we prefer that behavioral changes are accompanied by test improvements or additions, and an accompanying `Tests added.` or similar is then useful.
+Much like working with code, we suggest you refer to recent commits in the git log, for examples of the style we're actively using.
+
+Our overall style for commit messages broadly follows the general guidelines
+given for
+[Zulip Commit messages](http://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html#commit-messages),
+so we recommend reading that first.
+
+Our commit titles (summaries) have slight variations from the general Zulip style, with each:
+* starting with one or more **areas** - in lower case, followed by a colon and space
+  * generally each commit has at least one area of each modified file - without extensions, separated by a `/`
+  * generally *don't* include test files in this list (instead `Tests updated` or `Tests added` in the commit text)
+  * other common areas include types like `refactor:`, `bugfix:` and `requirements:` (see below)
+* ending with a **concise description** starting with a capital and ending with a full-stop (period)
+* having a maximum overall length of 72 (fitting the github web interface without abbreviation)
+
+Some example commit titles: (ideally more descriptive in practice!)
+* `file3/file1/file2: Improve behavior of something.`
+  - a general commit updating files `file1.txt`, `file2.py` and `file3.md`
+* `refactor: file1/file2: Extract some common function.`
+  - a pure refactor which doesn't change the functional behavior, involving `file1.py` and `file2.py`
+* `bugfix: file1: Avoid some noticeable bug.`
+  - a small commit to fix a bug in `file1.py`
+* `tests: file1: Improve test for something.`
+  - only improve tests for `file1`, likely in `test_file1.py`
+* `requirements: Upgrade some-dependency from ==9.2 to ==9.3.`
+  - upgrade a dependency from version ==9.2 to version ==9.3, in the central dependencies file (*not* some file requirements.*)
 
 To aid in satisfying some of these rules you can use `GitLint`, as described in the following section.
+
 **However**, please check your commits manually versus these style rules, since GitLint cannot check everything - including language or grammar!
 
-##### GitLint
+#### GitLint
 
 If you plan to submit git commits in pull-requests (PRs), then we highly suggest installing the `gitlint` commit-message hook by running `gitlint install-hook` (or `pipenv run gitlint install-hook` with pipenv setups). While the content still depends upon your writing skills, this ensures a more consistent formatting structure between commits, including by different authors.
 
diff --git a/zulipterminal/cli/run.py b/zulipterminal/cli/run.py
index 827e6d4..1f84923 100755
--- a/zulipterminal/cli/run.py
+++ b/zulipterminal/cli/run.py
@@ -9,8 +9,9 @@ import os
 import stat
 import sys
 import traceback
+from enum import Enum
 from os import path, remove
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Dict, List, NamedTuple, Optional, Tuple
 
 import requests
 from urwid import display_common, set_encoding
@@ -28,10 +29,19 @@ from zulipterminal.platform_code import detected_platform
 from zulipterminal.version import ZT_VERSION
 
 
+class ConfigSource(Enum):
+    DEFAULT = "from default config"
+    ZULIPRC = "in zuliprc file"
+    COMMANDLINE = "on command line"
+
+
+class SettingData(NamedTuple):
+    value: str
+    source: ConfigSource
+
+
 TRACEBACK_LOG_FILENAME = "zulip-terminal-tracebacks.log"
 API_CALL_LOG_FILENAME = "zulip-terminal-API-requests.log"
-ZULIPRC_CONFIG = "in zuliprc file"
-NO_CONFIG = "with no config"
 
 # Create a logger for this application
 zt_logger = logging.getLogger(__name__)
@@ -46,6 +56,19 @@ zt_logger.addHandler(zt_logfile_handler)
 requests_logger = logging.getLogger("urllib3")
 requests_logger.setLevel(logging.DEBUG)
 
+# Valid boolean settings, which map from (str, str) to (True, False)
+VALID_BOOLEAN_SETTINGS: Dict[str, Tuple[str, str]] = {
+    "autohide": ("autohide", "no_autohide"),
+    "notify": ("enabled", "disabled"),
+}
+
+COLOR_DEPTH_ARGS_TO_DEPTHS: Dict[str, int] = {
+    "1": 1,
+    "16": 16,
+    "256": 256,
+    "24bit": 2**24,
+}
+
 # These should be the defaults without config file or command-line overrides
 DEFAULT_SETTINGS = {
     "theme": "zt_dark",
@@ -55,6 +78,9 @@ DEFAULT_SETTINGS = {
     "color-depth": "256",
     "maximum-footlinks": "3",
 }
+assert DEFAULT_SETTINGS["autohide"] in VALID_BOOLEAN_SETTINGS["autohide"]
+assert DEFAULT_SETTINGS["notify"] in VALID_BOOLEAN_SETTINGS["notify"]
+assert DEFAULT_SETTINGS["color-depth"] in COLOR_DEPTH_ARGS_TO_DEPTHS
 
 
 def in_color(color: str, text: str) -> str:
@@ -113,7 +139,7 @@ def parse_args(argv: List[str]) -> argparse.Namespace:
     )
     parser.add_argument(
         "--color-depth",
-        choices=["1", "16", "256", "24bit"],
+        choices=COLOR_DEPTH_ARGS_TO_DEPTHS,
         help=f"force the color depth (default: {DEFAULT_SETTINGS['color-depth']})",
     )
     parser.add_argument(
@@ -196,7 +222,7 @@ def get_login_id(realm_url: str) -> str:
     return styled_input(label)
 
 
-def get_api_key(realm_url: str) -> Tuple[requests.Response, str]:
+def get_api_key(realm_url: str) -> Optional[Tuple[str, str]]:
     from getpass import getpass
 
     login_id = get_login_id(realm_url)
@@ -208,7 +234,9 @@ def get_api_key(realm_url: str) -> Tuple[requests.Response, str]:
             "password": password,
         },
     )
-    return response, login_id
+    if response.status_code == requests.codes.OK:
+        return login_id, str(response.json()["api_key"])
+    return None
 
 
 def fetch_zuliprc(zuliprc_path: str) -> None:
@@ -231,16 +259,17 @@ def fetch_zuliprc(zuliprc_path: str) -> None:
     # Remove trailing "/"s from realm_url to simplify the below logic
     # for adding "/api"
     realm_url = realm_url.rstrip("/")
-    res, login_id = get_api_key(realm_url)
+    login_data = get_api_key(realm_url)
 
-    while res.status_code != 200:
+    while login_data is None:
         print(in_color("red", "\nIncorrect Email(or Username) or Password!\n"))
-        res, login_id = get_api_key(realm_url)
+        login_data = get_api_key(realm_url)
 
+    login_id, api_key = login_data
     save_zuliprc_failure = _write_zuliprc(
         zuliprc_path,
         login_id=login_id,
-        api_key=str(res.json()["api_key"]),
+        api_key=api_key,
         server_url=realm_url,
     )
     if not save_zuliprc_failure:
@@ -268,7 +297,7 @@ def _write_zuliprc(
         return f"{ex.__class__.__name__}: zuliprc could not be created at {to_path}"
 
 
-def parse_zuliprc(zuliprc_str: str) -> Dict[str, Any]:
+def parse_zuliprc(zuliprc_str: str) -> Dict[str, SettingData]:
     zuliprc_path = path.expanduser(zuliprc_str)
     while not path.exists(zuliprc_path):
         try:
@@ -312,13 +341,14 @@ def parse_zuliprc(zuliprc_str: str) -> Dict[str, Any]:
 
     # Initialize with default settings
     settings = {
-        setting: (default, NO_CONFIG) for setting, default in DEFAULT_SETTINGS.items()
+        setting: SettingData(default, ConfigSource.DEFAULT)
+        for setting, default in DEFAULT_SETTINGS.items()
     }
 
     if "zterm" in zuliprc:
         config = zuliprc["zterm"]
         for conf in config:
-            settings[conf] = (config[conf], ZULIPRC_CONFIG)
+            settings[conf] = SettingData(config[conf], ConfigSource.ZULIPRC)
 
     return settings
 
@@ -385,25 +415,18 @@ def main(options: Optional[List[str]] = None) -> None:
     try:
         zterm = parse_zuliprc(zuliprc_path)
 
-        if args.autohide:
-            zterm["autohide"] = (args.autohide, "on command line")
-
-        if args.theme:
-            theme_to_use = (args.theme, "on command line")
-        else:
-            theme_to_use = zterm["theme"]
-
+        ### Validate footlinks settings (not from command line)
         if (
-            zterm["footlinks"][1] == ZULIPRC_CONFIG
-            and zterm["maximum-footlinks"][1] == ZULIPRC_CONFIG
+            zterm["footlinks"].source == ConfigSource.ZULIPRC
+            and zterm["maximum-footlinks"].source == ConfigSource.ZULIPRC
         ):
             exit_with_error(
                 "Configuration Error: "
                 "footlinks and maximum-footlinks options cannot be used together"
             )
 
-        if zterm["maximum-footlinks"][1] == ZULIPRC_CONFIG:
-            maximum_footlinks = int(zterm["maximum-footlinks"][0])
+        if zterm["maximum-footlinks"].source == ConfigSource.ZULIPRC:
+            maximum_footlinks = int(zterm["maximum-footlinks"].value)
             if maximum_footlinks < 0:
                 exit_with_error(
                     "Configuration Error: "
@@ -411,85 +434,58 @@ def main(options: Optional[List[str]] = None) -> None:
                     f"you used '{maximum_footlinks}'"
                 )
 
-        if zterm["footlinks"][1] == ZULIPRC_CONFIG:
-            if zterm["footlinks"][0] == DEFAULT_SETTINGS["footlinks"]:
+        if zterm["footlinks"].source == ConfigSource.ZULIPRC:
+            if zterm["footlinks"].value == DEFAULT_SETTINGS["footlinks"]:
                 maximum_footlinks = 3
             else:
                 maximum_footlinks = 0
         else:
-            maximum_footlinks = int(zterm["maximum-footlinks"][0])
+            maximum_footlinks = int(zterm["maximum-footlinks"].value)
 
+        ### Load theme override & validate
+        if args.theme:
+            theme_to_use = SettingData(args.theme, ConfigSource.COMMANDLINE)
+        else:
+            theme_to_use = zterm["theme"]
+
+        theme_alias_suffix = ""
         available_themes = all_themes()
         theme_aliases = aliased_themes()
         is_valid_theme = (
-            theme_to_use[0] in available_themes or theme_to_use[0] in theme_aliases
+            theme_to_use.value in available_themes
+            or theme_to_use.value in theme_aliases
         )
         if not is_valid_theme:
             exit_with_error(
                 "Invalid theme '{}' was specified {}.".format(*theme_to_use),
                 helper_text=list_themes(),
             )
-        if theme_to_use[0] not in available_themes:
+        if theme_to_use.value not in available_themes:
             # theme must be an alias, as it is valid
-            real_theme_name = theme_aliases[theme_to_use[0]]
-            theme_to_use = (
-                real_theme_name,
-                f"{theme_to_use[1]} (by alias '{theme_to_use[0]}')",
-            )
+            theme_alias_suffix = f" (by alias '{theme_to_use.value}')"
+            real_theme_name = theme_aliases[theme_to_use.value]
+            theme_to_use = SettingData(real_theme_name, theme_to_use.source)
 
-        if args.color_depth:
-            zterm["color-depth"] = (args.color_depth, "on command line")
+        ### Load overrides & validate remaining settings
+        if args.autohide:
+            zterm["autohide"] = SettingData(args.autohide, ConfigSource.COMMANDLINE)
 
-        color_depth_str = zterm["color-depth"][0]
-        if color_depth_str == "24bit":
-            color_depth = 2**24
-        else:
-            color_depth = int(color_depth_str)
+        if args.color_depth:
+            zterm["color-depth"] = SettingData(
+                args.color_depth, ConfigSource.COMMANDLINE
+            )
 
         if args.notify:
-            zterm["notify"] = (args.notify, "on command line")
+            zterm["notify"] = SettingData(args.notify, ConfigSource.COMMANDLINE)
 
-        print("Loading with:")
-        print("   theme '{}' specified {}.".format(*theme_to_use))
-        complete, incomplete = complete_and_incomplete_themes()
-        if theme_to_use[0] in incomplete:
-            if complete:
-                incomplete_theme_warning = (
-                    "   WARNING: Incomplete theme; results may vary!\n"
-                    "      (you could try: {})".format(", ".join(complete))
-                )
-            else:
-                incomplete_theme_warning = (
-                    "   WARNING: Incomplete theme; results may vary!\n"
-                    "      (all themes are incomplete)"
-                )
-            print(in_color("yellow", incomplete_theme_warning))
-        print("   autohide setting '{}' specified {}.".format(*zterm["autohide"]))
-        if zterm["footlinks"][1] == ZULIPRC_CONFIG:
-            print(
-                "   maximum footlinks value '{}' specified {} from footlinks.".format(
-                    maximum_footlinks, zterm["footlinks"][1]
-                )
-            )
-        else:
-            print(
-                "   maximum footlinks value '{}' specified {}.".format(
-                    *zterm["maximum-footlinks"]
-                )
-            )
-        print("   color depth setting '{}' specified {}.".format(*zterm["color-depth"]))
-        print("   notify setting '{}' specified {}.".format(*zterm["notify"]))
-
-        # For binary settings
-        # Specify setting in order True, False
-        valid_settings = {
-            "autohide": ["autohide", "no_autohide"],
-            "notify": ["enabled", "disabled"],
-            "color-depth": ["1", "16", "256", "24bit"],
-        }
-        boolean_settings: Dict[str, bool] = dict()
-        for setting, valid_values in valid_settings.items():
-            if zterm[setting][0] not in valid_values:
+        valid_remaining_settings = dict(
+            VALID_BOOLEAN_SETTINGS,
+            **{"color-depth": COLOR_DEPTH_ARGS_TO_DEPTHS},
+        )
+
+        # Validate remaining settings
+        for setting, valid_values in valid_remaining_settings.items():
+            if zterm[setting].value not in valid_values:
                 helper_text = (
                     ["Valid values are:"]
                     + [f"  {option}" for option in valid_values]
@@ -501,16 +497,49 @@ def main(options: Optional[List[str]] = None) -> None:
                     ),
                     helper_text="\n".join(helper_text),
                 )
-            if setting == "color-depth":
-                break
-            boolean_settings[setting] = zterm[setting][0] == valid_values[0]
 
-        theme_data = generate_theme(theme_to_use[0], color_depth)
+        def print_setting(setting: str, data: SettingData, suffix: str = "") -> None:
+            print(f"   {setting} '{data.value}' specified {data.source.value}{suffix}.")
+
+        ### Let the user know we're starting to load, with what options, from where
+        print("Loading with:")
+        print_setting("theme", theme_to_use, theme_alias_suffix)
+        complete, incomplete = complete_and_incomplete_themes()
+        if theme_to_use.value in incomplete:
+            incomplete_theme_text = "   WARNING: Incomplete theme; results may vary!\n"
+            if complete:
+                incomplete_theme_text += f"      (you could try: {', '.join(complete)})"
+            else:
+                incomplete_theme_text += "      (all themes are incomplete)"
+            print(in_color("yellow", incomplete_theme_text))
+        print_setting("autohide setting", zterm["autohide"])
+        if zterm["footlinks"].source == ConfigSource.ZULIPRC:
+            print_setting(
+                "maximum footlinks value",
+                SettingData(str(maximum_footlinks), zterm["footlinks"].source),
+                " from footlinks",
+            )
+        else:
+            print_setting("maximum footlinks value", zterm["maximum-footlinks"])
+        print_setting("color depth setting", zterm["color-depth"])
+        print_setting("notify setting", zterm["notify"])
+
+        ### Generate data not output to user, but into Controller
+        # Generate urwid palette
+        color_depth_str = zterm["color-depth"].value
+        color_depth = COLOR_DEPTH_ARGS_TO_DEPTHS[color_depth_str]
+
+        theme_data = generate_theme(theme_to_use.value, color_depth)
+
+        # Translate valid strings for boolean values into True/False
+        boolean_settings: Dict[str, bool] = dict()
+        for setting, valid_values in VALID_BOOLEAN_SETTINGS.items():
+            boolean_settings[setting] = zterm[setting].value == valid_values[0]
 
         Controller(
             config_file=zuliprc_path,
             maximum_footlinks=maximum_footlinks,
-            theme_name=theme_to_use[0],
+            theme_name=theme_to_use.value,
             theme=theme_data,
             color_depth=color_depth,
             in_explore_mode=args.explore,


Test patch:
diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py
index d6a896c..7653425 100644
--- a/tests/cli/test_run.py
+++ b/tests/cli/test_run.py
@@ -136,11 +136,11 @@ def test_valid_zuliprc_but_no_connection(
     expected_lines = [
         f"Detected platform: {platform}",
         "Loading with:",
-        "   theme 'zt_dark' specified with no config.",
-        "   autohide setting 'no_autohide' specified with no config.",
-        "   maximum footlinks value '3' specified with no config.",
-        "   color depth setting '256' specified with no config.",
-        "   notify setting 'disabled' specified with no config.",
+        "   theme 'zt_dark' specified from default config.",
+        "   autohide setting 'no_autohide' specified from default config.",
+        "   maximum footlinks value '3' specified from default config.",
+        "   color depth setting '256' specified from default config.",
+        "   notify setting 'disabled' specified from default config.",
         "\x1b[91m",
         f"Error connecting to Zulip server: {server_connection_error}.\x1b[0m",
     ]
@@ -192,10 +192,10 @@ def test_warning_regarding_incomplete_theme(
         f"   theme '{bad_theme}' specified on command line.",
         "\x1b[93m   WARNING: Incomplete theme; results may vary!",
         f"      {expected_warning}\x1b[0m",
-        "   autohide setting 'no_autohide' specified with no config.",
-        "   maximum footlinks value '3' specified with no config.",
-        "   color depth setting '256' specified with no config.",
-        "   notify setting 'disabled' specified with no config.",
+        "   autohide setting 'no_autohide' specified from default config.",
+        "   maximum footlinks value '3' specified from default config.",
+        "   color depth setting '256' specified from default config.",
+        "   notify setting 'disabled' specified from default config.",
         "\x1b[91m",
         f"Error connecting to Zulip server: {server_connection_error}.\x1b[0m",
     ]
@@ -328,8 +328,7 @@ def test_main_cannot_write_zuliprc_given_good_credentials(
 
     # Give some arbitrary input and fake that it's always valid
     mocker.patch.object(builtins, "input", lambda _: "text\n")
-    response = mocker.Mock(json=lambda: dict(api_key=""), status_code=200)
-    mocker.patch(MODULE + ".get_api_key", return_value=(response, None))
+    mocker.patch(MODULE + ".get_api_key", return_value=("my_login", "my_api_key"))
 
     with pytest.raises(SystemExit):
         main([])