Skip to content

Commit

Permalink
Migrate auth commands
Browse files Browse the repository at this point in the history
  • Loading branch information
ihabunek committed Nov 30, 2023
1 parent 696a9dc commit d8c7084
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 115 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ test:
coverage:
coverage erase
coverage run
coverage html
coverage html --omit toot/tui/*
coverage report

clean :
Expand Down
6 changes: 6 additions & 0 deletions changelog.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
0.40.0:
date: TBA
changes:
- "Migrated to `click` for commandline arguments. BC should be mostly preserved, please report any issues."
- "Removed the deprecated `--disable-https` option for `login` and `login_cli`, pass the base URL instead"

0.39.0:
date: 2023-11-23
changes:
Expand Down
217 changes: 217 additions & 0 deletions tests/integration/test_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
from typing import Any, Dict
from unittest import mock
from unittest.mock import MagicMock

from toot import User, cli
from toot.cli.base import Run

# TODO: figure out how to test login


EMPTY_CONFIG: Dict[Any, Any] = {
"apps": {},
"users": {},
"active_user": None
}

SAMPLE_CONFIG = {
"active_user": "frank@foo.social",
"apps": {
"foo.social": {
"base_url": "http://foo.social",
"client_id": "123",
"client_secret": "123",
"instance": "foo.social"
},
"bar.social": {
"base_url": "http://bar.social",
"client_id": "123",
"client_secret": "123",
"instance": "bar.social"
},
},
"users": {
"frank@foo.social": {
"access_token": "123",
"instance": "foo.social",
"username": "frank"
},
"frank@bar.social": {
"access_token": "123",
"instance": "bar.social",
"username": "frank"
},
}
}


def test_env(run: Run):
result = run(cli.env)
assert result.exit_code == 0
assert "toot" in result.stdout
assert "Python" in result.stdout


@mock.patch("toot.config.load_config")
def test_auth_empty(load_config: MagicMock, run: Run):
load_config.return_value = EMPTY_CONFIG
result = run(cli.auth)
assert result.exit_code == 0
assert result.stdout.strip() == "You are not logged in to any accounts"


@mock.patch("toot.config.load_config")
def test_auth_full(load_config: MagicMock, run: Run):
load_config.return_value = SAMPLE_CONFIG
result = run(cli.auth)
assert result.exit_code == 0
assert result.stdout.strip().startswith("Authenticated accounts:")
assert "frank@foo.social" in result.stdout
assert "frank@bar.social" in result.stdout


# Saving config is mocked so we don't mess up our local config
# TODO: could this be implemented using an auto-use fixture so we have it always
# mocked?
@mock.patch("toot.config.load_app")
@mock.patch("toot.config.save_app")
@mock.patch("toot.config.save_user")
def test_login_cli(
save_user: MagicMock,
save_app: MagicMock,
load_app: MagicMock,
user: User,
run: Run,
):
load_app.return_value = None

result = run(
cli.login_cli,
"--instance", "http://localhost:3000",
"--email", f"{user.username}@example.com",
"--password", "password",
)
assert result.exit_code == 0
assert "✓ Successfully logged in." in result.stdout

save_app.assert_called_once()
(app,) = save_app.call_args.args
assert app.instance == "localhost:3000"
assert app.base_url == "http://localhost:3000"
assert app.client_id
assert app.client_secret

save_user.assert_called_once()
(new_user,) = save_user.call_args.args
assert new_user.instance == "localhost:3000"
assert new_user.username == user.username
# access token will be different since this is a new login
assert new_user.access_token and new_user.access_token != user.access_token
assert save_user.call_args.kwargs == {"activate": True}


@mock.patch("toot.config.load_app")
@mock.patch("toot.config.save_app")
@mock.patch("toot.config.save_user")
def test_login_cli_wrong_password(
save_user: MagicMock,
save_app: MagicMock,
load_app: MagicMock,
user: User,
run: Run,
):
load_app.return_value = None

result = run(
cli.login_cli,
"--instance", "http://localhost:3000",
"--email", f"{user.username}@example.com",
"--password", "wrong password",
)
assert result.exit_code == 1
assert result.stderr.strip() == "Error: Login failed"

save_app.assert_called_once()
(app,) = save_app.call_args.args
assert app.instance == "localhost:3000"
assert app.base_url == "http://localhost:3000"
assert app.client_id
assert app.client_secret

save_user.assert_not_called()


@mock.patch("toot.config.load_config")
@mock.patch("toot.config.delete_user")
def test_logout(delete_user: MagicMock, load_config: MagicMock, run: Run):
load_config.return_value = SAMPLE_CONFIG

result = run(cli.logout, "frank@foo.social")
assert result.exit_code == 0
assert result.stdout.strip() == "✓ Account frank@foo.social logged out"
delete_user.assert_called_once_with(User("foo.social", "frank", "123"))


@mock.patch("toot.config.load_config")
def test_logout_not_logged_in(load_config: MagicMock, run: Run):
load_config.return_value = EMPTY_CONFIG

result = run(cli.logout)
assert result.exit_code == 1
assert result.stderr.strip() == "Error: You're not logged into any accounts"


@mock.patch("toot.config.load_config")
def test_logout_account_not_specified(load_config: MagicMock, run: Run):
load_config.return_value = SAMPLE_CONFIG

result = run(cli.logout)
assert result.exit_code == 1
assert result.stderr.startswith("Error: Specify account to log out")


@mock.patch("toot.config.load_config")
def test_logout_account_does_not_exist(load_config: MagicMock, run: Run):
load_config.return_value = SAMPLE_CONFIG

result = run(cli.logout, "banana")
assert result.exit_code == 1
assert result.stderr.startswith("Error: Account not found")


@mock.patch("toot.config.load_config")
@mock.patch("toot.config.activate_user")
def test_activate(activate_user: MagicMock, load_config: MagicMock, run: Run):
load_config.return_value = SAMPLE_CONFIG

result = run(cli.activate, "frank@foo.social")
assert result.exit_code == 0
assert result.stdout.strip() == "✓ Account frank@foo.social activated"
activate_user.assert_called_once_with(User("foo.social", "frank", "123"))


@mock.patch("toot.config.load_config")
def test_activate_not_logged_in(load_config: MagicMock, run: Run):
load_config.return_value = EMPTY_CONFIG

result = run(cli.activate)
assert result.exit_code == 1
assert result.stderr.strip() == "Error: You're not logged into any accounts"


@mock.patch("toot.config.load_config")
def test_activate_account_not_given(load_config: MagicMock, run: Run):
load_config.return_value = SAMPLE_CONFIG

result = run(cli.activate)
assert result.exit_code == 1
assert result.stderr.startswith("Error: Specify account to activate")


@mock.patch("toot.config.load_config")
def test_activate_invalid_Account(load_config: MagicMock, run: Run):
load_config.return_value = SAMPLE_CONFIG

result = run(cli.activate, "banana")
assert result.exit_code == 1
assert result.stderr.startswith("Error: Account not found")
15 changes: 9 additions & 6 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from argparse import ArgumentTypeError
import click
import pytest

from toot.console import duration
from toot.cli.validators import validate_duration
from toot.wcstring import wc_wrap, trunc, pad, fit_text
from toot.utils import urlencode_url

Expand Down Expand Up @@ -163,6 +163,9 @@ def test_wc_wrap_indented():


def test_duration():
def duration(value):
return validate_duration(None, None, value)

# Long hand
assert duration("1 second") == 1
assert duration("1 seconds") == 1
Expand Down Expand Up @@ -190,17 +193,17 @@ def test_duration():
assert duration("5d 10h 3m 1s") == 5 * 86400 + 10 * 3600 + 3 * 60 + 1
assert duration("5d10h3m1s") == 5 * 86400 + 10 * 3600 + 3 * 60 + 1

with pytest.raises(ArgumentTypeError):
with pytest.raises(click.BadParameter):
duration("")

with pytest.raises(ArgumentTypeError):
with pytest.raises(click.BadParameter):
duration("100")

# Wrong order
with pytest.raises(ArgumentTypeError):
with pytest.raises(click.BadParameter):
duration("1m1d")

with pytest.raises(ArgumentTypeError):
with pytest.raises(click.BadParameter):
duration("banana")


Expand Down
14 changes: 4 additions & 10 deletions toot/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def fetch_app_token(app):
return http.anon_post(f"{app.base_url}/oauth/token", json=json).json()


def login(app, username, password):
def login(app: App, username: str, password: str):
url = app.base_url + '/oauth/token'

data = {
Expand All @@ -152,16 +152,10 @@ def login(app, username, password):
'scope': SCOPES,
}

response = http.anon_post(url, data=data, allow_redirects=False)
return http.anon_post(url, data=data).json()

# If auth fails, it redirects to the login page
if response.is_redirect:
raise AuthenticationError()

return response.json()


def get_browser_login_url(app):
def get_browser_login_url(app: App) -> str:
"""Returns the URL for manual log in via browser"""
return "{}/oauth/authorize/?{}".format(app.base_url, urlencode({
"response_type": "code",
Expand All @@ -171,7 +165,7 @@ def get_browser_login_url(app):
}))


def request_access_token(app, authorization_code):
def request_access_token(app: App, authorization_code: str):
url = app.base_url + '/oauth/token'

data = {
Expand Down
Loading

0 comments on commit d8c7084

Please sign in to comment.