diff --git a/evalai/logout.py b/evalai/logout.py new file mode 100644 index 000000000..3eeeda7be --- /dev/null +++ b/evalai/logout.py @@ -0,0 +1,17 @@ +import click + +from click import echo, style + +from evalai.utils.auth import reset_user_auth_token + + +@click.group(invoke_without_command=True) +def logout(): + """ + Log out the user by resetting authentication token + """ + """ + Invoked by `evalai logout` + """ + reset_user_auth_token() + echo(style("Logout successful", bold=True, fg="green")) diff --git a/evalai/main.py b/evalai/main.py index 34bb252cc..8821f3b3b 100644 --- a/evalai/main.py +++ b/evalai/main.py @@ -9,6 +9,7 @@ from .teams import teams from .get_token import get_token from .login import login +from .logout import logout @click.version_option() @@ -43,3 +44,4 @@ def main(ctx): main.add_command(teams) main.add_command(get_token) main.add_command(login) +main.add_command(logout) diff --git a/evalai/utils/auth.py b/evalai/utils/auth.py index e18f41bbc..369b8de2d 100644 --- a/evalai/utils/auth.py +++ b/evalai/utils/auth.py @@ -98,3 +98,24 @@ def get_host_url(): return str(data) except (OSError, IOError) as e: echo(style(e, bold=True, fg="red")) + + +def reset_user_auth_token(): + """ + Resets the auth token of the user by deleting token.json file + """ + if not os.path.exists(AUTH_TOKEN_PATH): + echo( + style( + "\nThe authentication token has not been configured. Please use the commands " + "`evalai login` or `evalai set_token TOKEN` first to set up the configuration.\n", + bold=True, + fg="red", + ), + ) + sys.exit(1) + try: + os.remove(AUTH_TOKEN_PATH) + except (OSError, IOError) as e: + echo(e) + sys.exit(1) diff --git a/tests/test_auth_utils.py b/tests/test_auth_utils.py new file mode 100644 index 000000000..4eca378dd --- /dev/null +++ b/tests/test_auth_utils.py @@ -0,0 +1,70 @@ +import json +import os +import random +import shutil +import string +import tempfile + +from io import StringIO +from unittest import mock +from unittest import TestCase + +from evalai.utils.auth import reset_user_auth_token + + +class TestResetUserAuthToken(TestCase): + def setUp(self): + self.base_temp_dir = tempfile.mkdtemp() + self.token_dir = os.path.join(self.base_temp_dir, ".evalai") + self.token_path = os.path.join(self.token_dir, "token.json") + + self.token = "".join(random.choice(string.ascii_lowercase) for _ in range(40)) + self.token_json = json.dumps({"token": self.token}) + + os.makedirs(self.token_dir) + with open(self.token_path, "w") as fw: + fw.write(self.token_json) + + self.patcher = mock.patch("evalai.utils.auth.AUTH_TOKEN_PATH", self.token_path) + self.patcher.start() + + def tearDown(self): + self.patcher.stop() + if os.path.exists(self.base_temp_dir): + shutil.rmtree(self.base_temp_dir) + + def test_reset_user_auth_token_success(self): + self.assertTrue(os.path.exists(self.token_path)) # Make sure the path exists already + reset_user_auth_token() + + self.assertFalse(os.path.exists(self.token_path)) + + def test_reset_user_auth_token_when_token_is_not_configured(self): + os.remove(self.token_path) + expected = str( + "The authentication token has not been configured. Please use the commands " + "`evalai login` or `evalai set_token TOKEN` first to set up the configuration." + ) + + with mock.patch("sys.stdout", StringIO()) as fake_out: + with self.assertRaises(SystemExit) as cm: + reset_user_auth_token() + exit_code = cm.exception.code + value = fake_out.getvalue().strip() + + self.assertEqual(exit_code, 1) + self.assertEqual(value, expected) + + @mock.patch("evalai.utils.auth.os.remove") + def test_reset_user_auth_token_when_token_file_removal_fails(self, mock_remove): + error = "ExampleError: Example Error Description" + mock_remove.side_effect = OSError(error) + + with mock.patch("sys.stdout", StringIO()) as fake_out: + with self.assertRaises(SystemExit) as cm: + reset_user_auth_token() + exit_code = cm.exception.code + value = fake_out.getvalue().strip() + + self.assertEqual(exit_code, 1) + self.assertEqual(value, error)