Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ask for the push API key again when validation fails #197

Merged
merged 5 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions src/appsignal/cli/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
from abc import ABC, abstractmethod
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass
from functools import cached_property

from ..client import Client
from ..config import Config, Options
from ..push_api_key_validator import PushApiKeyValidator
from .config import _client_from_config_file
from .exit_error import ExitError

Expand Down Expand Up @@ -48,22 +49,50 @@ def _environment_argument(parser: ArgumentParser) -> None:
def run(self) -> int:
raise NotImplementedError

@cached_property
def _push_api_key(self) -> str | None:
def _push_api_key(self) -> str:
key = self.args.push_api_key
while not key:
key = input("Please enter your Push API key: ")
return key

@cached_property
def _name(self) -> str | None:
def _valid_push_api_key(self) -> str:
while True:
key = self._push_api_key()
config = Config(Options(push_api_key=key))
print("Validating API key...")
print()

try:
validation_result = PushApiKeyValidator.validate(config)
except Exception as e:
print(f"Error while validating Push API key: {e}")
print("Reach us at [email protected] for support.")
raise ExitError(1) from e

if validation_result == "valid":
print("✅ API key is valid!")
return key

if validation_result == "invalid":
print(f"❌ API key {key} is not valid.")
print("Please get a new one on https://appsignal.com and try again.")
print()
self.args.push_api_key = None
else:
print(
"Error while validating Push API key. HTTP status code: "
f"{validation_result}"
)
print("Reach us at [email protected] for support.")
raise ExitError(1)

def _name(self) -> str:
name = self.args.application
while not name:
name = input("Please enter the name of your application: ")
return name

@cached_property
def _environment(self) -> str | None:
def _environment(self) -> str:
environment = self.args.environment
while not environment:
environment = input(
Expand Down
13 changes: 6 additions & 7 deletions src/appsignal/cli/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@ def run(self) -> int:
if push_api_key:
client._config.options["push_api_key"] = push_api_key
else:
# Prompt for all the required config in a sensible order
self._name # noqa: B018
self._environment # noqa: B018
self._push_api_key # noqa: B018
name = self._name()
environment = self._environment()
push_api_key = self._push_api_key()
client = Client(
active=True,
name=self._name,
environment=self._environment,
push_api_key=self._push_api_key,
name=name,
environment=environment,
push_api_key=push_api_key,
)

if not client._config.is_active():
Expand Down
39 changes: 9 additions & 30 deletions src/appsignal/cli/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
from argparse import ArgumentParser

from ..client import Client
from ..config import Config, Options
from ..push_api_key_validator import PushApiKeyValidator
from ..config import Options
from .command import AppsignalCLICommand
from .demo import Demo

Expand Down Expand Up @@ -36,47 +35,27 @@ def init_parser(parser: ArgumentParser) -> None:
def run(self) -> int:
options = Options()

# Make sure to show input prompts before the welcome text.
options["name"] = self._name
options["push_api_key"] = self._push_api_key

print("👋 Welcome to the AppSignal for Python installer!")
print()
print("Reach us at [email protected] for support")
print("Documentation available at https://docs.appsignal.com/python")
print()

print()
options["name"] = self._name()
options["push_api_key"] = self._valid_push_api_key()

print("Validating API key")
print()
validation_result = PushApiKeyValidator.validate(Config(options))
if validation_result == "valid":
print("API key is valid!")
elif validation_result == "invalid":
print(f"API key {self._push_api_key} is not valid ")
print("please get a new one on https://appsignal.com")
return 1
else:
print(
"Error while validating Push API key. HTTP status code: "
"{validation_result}"
)
print(
"Reach us at [email protected] for support if this keeps happening."
)
return 1

if self._should_write_file():
print(f"Writing the {INSTALL_FILE_NAME} configuration file...")
self._write_file()
self._write_file(options)
print()
print()

client = Client(
active=True,
name=self._name,
push_api_key=self._push_api_key,
name=options["name"],
push_api_key=options["push_api_key"],
)
client.start()
Demo.transmit()
Expand Down Expand Up @@ -109,11 +88,11 @@ def _input_should_overwrite_file(self) -> bool:
print('Please answer "y" (yes) or "n" (no)')
return self._input_should_overwrite_file()

def _write_file(self) -> None:
def _write_file(self, options: Options) -> None:
with open(INSTALL_FILE_NAME, "w") as f:
file_contents = INSTALL_FILE_TEMPLATE.format(
name=self._name,
push_api_key=self._push_api_key,
name=options["name"],
push_api_key=options["push_api_key"],
)

f.write(file_contents)
Expand Down
Empty file added tests/cli/__init__.py
Empty file.
10 changes: 1 addition & 9 deletions tests/cli/test_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,11 @@

import os
import shutil
from contextlib import contextmanager

from appsignal.cli.base import main
from appsignal.cli.install import INSTALL_FILE_TEMPLATE


@contextmanager
def mock_input(mocker, *pairs: tuple[str, str]):
prompt_calls = [mocker.call(prompt) for (prompt, _) in pairs]
answers = [answer for (_, answer) in pairs]
mock = mocker.patch("builtins.input", side_effect=answers)
yield
assert prompt_calls == mock.mock_calls
from .utils import mock_input


def test_demo_with_valid_config(mocker, capfd):
Expand Down
10 changes: 1 addition & 9 deletions tests/cli/test_diagnose.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,11 @@

import os
import shutil
from contextlib import contextmanager

from appsignal.cli.base import main
from appsignal.cli.install import INSTALL_FILE_TEMPLATE


@contextmanager
def mock_input(mocker, *pairs: tuple[str, str]):
prompt_calls = [mocker.call(prompt) for (prompt, _) in pairs]
answers = [answer for (_, answer) in pairs]
mock = mocker.patch("builtins.input", side_effect=answers)
yield
assert prompt_calls == mock.mock_calls
from .utils import mock_input


def test_diagnose_with_valid_config(mocker, capfd):
Expand Down
32 changes: 20 additions & 12 deletions tests/cli/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import os
import shutil
from contextlib import contextmanager
from unittest.mock import MagicMock

from appsignal.cli.base import main
from appsignal.cli.install import INSTALL_FILE_TEMPLATE

from .utils import mock_input


EXPECTED_FILE_CONTENTS = """from appsignal import Appsignal

Expand All @@ -22,15 +23,6 @@
"""


@contextmanager
def mock_input(mocker, *pairs: tuple[str, str]):
prompt_calls = [mocker.call(prompt) for (prompt, _) in pairs]
answers = [answer for (_, answer) in pairs]
mock = mocker.patch("builtins.input", side_effect=answers)
yield
assert prompt_calls == mock.mock_calls


def mock_file_operations(mocker, file_exists: bool = False):
mocker.patch("os.path.exists", return_value=file_exists)
mocker.patch("appsignal.cli.install.open")
Expand All @@ -47,6 +39,17 @@ def assert_wrote_file_contents(mocker):
)


def assert_did_not_write_file_contents(mocker):
from appsignal.cli import install

builtins_open: MagicMock = install.open # type: ignore[attr-defined]
assert mocker.call("__appsignal__.py", "w") not in builtins_open.mock_calls
assert (
mocker.call().__enter__().write(EXPECTED_FILE_CONTENTS)
not in builtins_open.mock_calls
)


def assert_wrote_real_file_contents(test_dir, name, push_api_key):
with open(os.path.join(test_dir, "__appsignal__.py")) as f:
file_contents = INSTALL_FILE_TEMPLATE.format(
Expand Down Expand Up @@ -159,11 +162,16 @@ def test_install_command_when_file_exists_no_overwrite(mocker):
assert install.open.mock_calls == [] # type: ignore[attr-defined]


def test_install_comand_when_api_key_is_not_valid(mocker):
def test_install_command_when_invalid_api_key_ask_again(mocker):
mock_file_operations(mocker)
mock_validate_push_api_key_request(mocker, status_code=401)

with mock_input(
mocker,
("Please enter the name of your application: ", "My app name"),
("Please enter your Push API key: ", "My push API key"),
("Please enter your Push API key: ", None),
):
assert main(["install", "--push-api-key=bad-push-api-key"]) == 1
main(["install"])

assert_did_not_write_file_contents(mocker)
19 changes: 19 additions & 0 deletions tests/cli/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

from contextlib import contextmanager


@contextmanager
def mock_input(mocker, *pairs: tuple[str, str | None]):
expected_unanswered = pairs[-1][1] is None

prompt_calls = [mocker.call(prompt) for (prompt, _) in pairs]
answers = [answer for (_, answer) in pairs if answer is not None]
mock = mocker.patch("builtins.input", side_effect=answers)
try:
yield
except StopIteration as e:
if not expected_unanswered:
raise e
finally:
assert prompt_calls == mock.mock_calls
Loading