From f0243bbb1739d716d9a9419caf97b68f7dbe67c7 Mon Sep 17 00:00:00 2001 From: Michela Iannaccone Date: Mon, 13 May 2024 22:24:26 -0400 Subject: [PATCH] add scope and tests --- canvas_cli/apps/auth/tests.py | 223 ++++++++++++++++++++++------------ canvas_cli/apps/auth/utils.py | 22 ++-- 2 files changed, 151 insertions(+), 94 deletions(-) diff --git a/canvas_cli/apps/auth/tests.py b/canvas_cli/apps/auth/tests.py index 8cd18b19..a214b3a1 100644 --- a/canvas_cli/apps/auth/tests.py +++ b/canvas_cli/apps/auth/tests.py @@ -1,81 +1,142 @@ -# def test_get_api_token_without_existing_host_or_client_credentials_raises_exception() -> None: -# """Test getting an api token with no default host or client credentials.""" - -# runner.invoke(app, "auth remove-api-client-credentials http://george.com") - -# result_without_host = runner.invoke(app, "auth get-api-token") -# assert result_without_host.exit_code == 2 -# assert ( -# "Invalid value: Please specify a host or set a default via the `auth` command" -# in result_without_host.stdout -# ) - -# result_without_client_id = runner.invoke(app, "auth get-api-token --host http://george.com") -# assert result_without_client_id.exit_code == 2 -# print(result_without_client_id.stdout) -# assert ( -# "Invalid value: Please specify a client_id and client_secret or add them via" -# in result_without_client_id.stdout -# ) - -# result_without_client_secret = runner.invoke( -# app, "auth get-api-token --host http://george.com --client-id mock-client-id" -# ) -# assert result_without_client_secret.exit_code == 2 -# assert ( -# "Invalid value: Please specify a client_id and client_secret or add them via" -# in result_without_client_secret.stdout -# ) - - -# @patch("requests.post") -# def test_get_api_token_requests_token_from_the_host_if_not_stored_in_context( -# mock_post: MagicMock, -# ) -> None: -# class FakeResponse: -# status_code = 200 - -# def json(self) -> dict: -# return {"access_token": "a-valid-api-token", "expires_in": 3600} - -# mock_post.return_value = FakeResponse() - -# result = runner.invoke( -# app, -# "auth get-api-token --host http://george.com --client-id mock-client-id --client-secret mock-client-secret", -# ) -# mock_post.assert_called_once() -# assert result.exit_code == 0 -# assert '{"success": true, "token": "a-valid-api-token"}' in result.stdout -# assert context.token_expiration_date is not None -# assert datetime.fromisoformat(context.token_expiration_date) > datetime.now() - - -# @patch("keyring.get_password") -# @patch("requests.post") -# def test_get_api_token_uses_token_stored_in_context_first( -# mock_post: MagicMock, -# mock_get_password: MagicMock, -# ) -> None: -# mock_get_password.return_value = "a-valid-api-token" -# result = runner.invoke( -# app, -# "auth get-api-token --host http://george.com --client-id mock-client-id --client-secret mock-client-secret", -# ) -# assert result.exit_code == 0 -# mock_get_password.assert_called_once_with( -# "canvas_cli.apps.auth.utils", "http://george.com|token" -# ) -# mock_post.assert_not_called() - - -# def test_get_api_token_uses_credentials_stored_in_context() -> None: -# runner.invoke( -# app, -# "auth add-api-client-credentials --host http://george.com --client-id mock-client-id --client-secret mock-client-secret --is-default", -# ) -# assert context.default_host == "http://george.com" - -# result = runner.invoke(app, "auth get-api-token") -# assert result.exit_code == 0 -# assert '{"success": true, "token": "a-valid-api-token"}' in result.stdout +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest + +from canvas_cli.apps.auth import get_or_request_api_token + + +@pytest.fixture +def valid_token_response() -> Any: + class TokenResponse: + status_code = 200 + + def json(self) -> dict: + return {"access_token": "a-valid-api-token", "expires_in": 3600} + + return TokenResponse() + + +@pytest.fixture +def error_token_response() -> Any: + class TokenResponse: + status_code = 500 + + return TokenResponse() + + +@pytest.fixture +def expired_token_response() -> Any: + class TokenResponse: + status_code = 200 + + def json(self) -> dict: + return {"access_token": "a-valid-api-token", "expires_in": -1} + + return TokenResponse() + + +@patch("keyring.get_password") +@patch("requests.Session.post") +@patch("canvas_cli.apps.auth.utils.is_token_valid") +def test_get_or_request_api_token_uses_stored_token( + mock_is_token_valid: MagicMock, + mock_post: MagicMock, + mock_get_password: MagicMock, + valid_token_response: Any, +) -> None: + mock_is_token_valid.return_value = True + mock_get_password.return_value = "a-stored-valid-token" + mock_post.return_value = valid_token_response + + token = get_or_request_api_token("http://localhost:8000") + + assert token == "a-stored-valid-token" + mock_post.assert_not_called() + + +@patch("keyring.set_password") +@patch("keyring.get_password") +@patch("requests.Session.post") +@patch("canvas_cli.apps.auth.utils.get_api_client_credentials") +def test_get_or_request_api_token_requests_token_if_none_stored( + mock_client_credentials: MagicMock, + mock_post: MagicMock, + mock_get_password: MagicMock, + mock_set_password: MagicMock, + valid_token_response: Any, +) -> None: + mock_client_credentials.return_value = "client_id=id&client_secret=secret" + mock_get_password.return_value = None + mock_post.return_value = valid_token_response + + token = get_or_request_api_token("http://localhost:8000") + + assert token == "a-valid-api-token" + mock_post.assert_called_once_with( + "http://localhost:8000/auth/token/", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + json=None, + data="grant_type=client_credentials&scope=system/Plugins.*&client_id=id&client_secret=secret", + ) + mock_set_password.assert_called_with( + "canvas_cli.apps.auth.utils", + username="http://localhost:8000|token", + password="a-valid-api-token", + ) + + +@patch("keyring.get_password") +@patch("requests.Session.post") +@patch("canvas_cli.apps.auth.utils.get_api_client_credentials") +def test_get_or_request_api_token_raises_exception_if_error_token_response( + mock_client_credentials: MagicMock, + mock_post: MagicMock, + mock_get_password: MagicMock, + error_token_response: Any, +) -> None: + mock_client_credentials.return_value = "client_id=id&client_secret=secret" + mock_get_password.return_value = None + mock_post.return_value = error_token_response + + with pytest.raises(Exception) as e: + get_or_request_api_token("http://localhost:8000") + + assert "Unable to get a valid access token from the given host 'http://localhost:8000'" in repr( + e + ) + + mock_post.assert_called_once_with( + "http://localhost:8000/auth/token/", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + json=None, + data="grant_type=client_credentials&scope=system/Plugins.*&client_id=id&client_secret=secret", + ) + + +@patch("keyring.get_password") +@patch("requests.Session.post") +@patch("canvas_cli.apps.auth.utils.get_api_client_credentials") +def test_get_or_request_api_token_raises_exception_if_expired_token( + mock_client_credentials: MagicMock, + mock_post: MagicMock, + mock_get_password: MagicMock, + expired_token_response: Any, +) -> None: + mock_client_credentials.return_value = "client_id=id&client_secret=secret" + mock_get_password.return_value = None + mock_post.return_value = expired_token_response + + with pytest.raises(Exception) as e: + get_or_request_api_token("http://localhost:8000") + + assert ( + "A valid token could not be acquired from the given host 'http://localhost:8000'" in repr(e) + ) + + mock_post.assert_called_once_with( + "http://localhost:8000/auth/token/", + headers={"Content-Type": "application/x-www-form-urlencoded"}, + json=None, + data="grant_type=client_credentials&scope=system/Plugins.*&client_id=id&client_secret=secret", + ) diff --git a/canvas_cli/apps/auth/utils.py b/canvas_cli/apps/auth/utils.py index 27ffa976..9cab2490 100644 --- a/canvas_cli/apps/auth/utils.py +++ b/canvas_cli/apps/auth/utils.py @@ -37,11 +37,11 @@ def get_config() -> configparser.ConfigParser: raise Exception( f"""Please add your configuration file at '{CONFIG_PATH}' with the following format: - [my-canvas-instance] + [my-canvas-subdomain] client_id=myclientid client_secret=myclientsecret - [my-dev-canvas-instance] + [my-dev-canvas-subdomain] client_id=devclientid client_secret=devclientsecret is_default=true @@ -91,16 +91,17 @@ def get_default_host(host: str | None = None) -> str: def request_api_token(host: str, api_client_credentials: str) -> dict: """Request an api token using the provided client_id and client_secret.""" + grant_type = "grant_type=client_credentials" + scope = "scope=system/Plugins.*" + http = Http() token_response = http.post( f"{host}/auth/token/", headers={"Content-Type": "application/x-www-form-urlencoded"}, - data=f"grant_type=client_credentials&{api_client_credentials}", + data=f"{grant_type}&{scope}&{api_client_credentials}", ) if token_response.status_code != requests.codes.ok: - raise Exception( - "Unable to get a valid access token. Please check your host, client_id, and client_secret" - ) + raise Exception(f"Unable to get a valid access token from the given host '{host}'") return token_response.json() @@ -130,22 +131,17 @@ def get_or_request_api_token(host: str | None = None) -> str: host_token_key = f"{host}|token" token = get_password(host_token_key) - if token and is_token_valid(host_token_key): return token api_client_credentials = get_api_client_credentials(host) if not (token_response := request_api_token(host, api_client_credentials)): - raise Exception( - "A token could not be acquired from the given host, client_id, and client_secret" - ) + raise Exception(f"A token could not be acquired from the given host '{host}'") token_expiration_date = datetime.now() + timedelta(seconds=token_response["expires_in"]) if not is_token_valid(host_token_key, token_expiration_date): - raise Exception( - "A valid token could not be acquired from the given host, client_id, and client_secret" - ) + raise Exception(f"A valid token could not be acquired from the given host '{host}'") new_token = token_response["access_token"] set_password(host_token_key, new_token)