Skip to content

Commit

Permalink
Merge pull request #11 from iotile/fix-make-login-via-token-file-work
Browse files Browse the repository at this point in the history
Relates to AFX-2175 

Fix login with token via config file
  • Loading branch information
haroal authored Aug 1, 2022
2 parents 0630f8d + a5474fc commit 98a957e
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 23 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ dmypy.json

# IDEs
.vscode
.idea
7 changes: 7 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All major changes in each released version of the archfx-cloud plugin are listed here.

## 0.14.0

- Add optional parameter to BaseMain constructor to give the config file path (.ini)
- Config file can configure JWT token (access and refresh)
- Provided user email is not required anymore if logging in via token
- Avoid one API request with empty token if no token provided

## 0.13.0

- Removed unneed pytz dependency
Expand Down
2 changes: 1 addition & 1 deletion archfx_cloud/api/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def __init__(self, domain=None, token_type=None, verify=True, timeout=None, retr
self.domain = domain

self.base_url = f"{self.domain}/{API_PREFIX}"
self.use_token = True

if token_type:
self.token_type = token_type

Expand Down
48 changes: 28 additions & 20 deletions archfx_cloud/utils/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ class BaseMain(object):
domain = 'https://arch.archfx.io'
logging_level = logging.INFO

def __init__(self):
def __init__(self, config_path='.ini'):
"""
Initialize Logging configuration
Initialize argument parsing
Process any extra arguments
Only hard codes one required argument: --user
Additional arguments can be configured by overwriting the add_extra_args() method
Logging configuration can be changed by overwritting the config_logging() method
Logging configuration can be changed by overwriting the config_logging() method
"""
CONFIG.read('.ini')
CONFIG.read(config_path)
self.parser = argparse.ArgumentParser(description=__doc__)
self.parser.add_argument(
'-u', '--user', dest='email', type=str, help='Email used for login'
Expand All @@ -46,10 +46,6 @@ def __init__(self):
self.args = self.parser.parse_args()
self.config_logging()

if not self.args.email:
LOG.error('User email is required: --user')
sys.exit(1)

def _critical_exit(self, msg):
LOG.error(msg)
sys.exit(1)
Expand Down Expand Up @@ -109,27 +105,39 @@ def get_domain(self) -> str:

def login(self) -> bool:
"""
Check if we can user token from .ini
Check if we can use token from .ini
"""

# If there is a token defined, check if legal
ini_token_key = f'c-{self.args.customer}'
customer_section = f'c-{self.args.customer}'
try:
ini_cloud = CONFIG[ini_token_key]
token = ini_cloud.get('token')
customer_config = CONFIG[customer_section]

token = customer_config.get('token')
jwt = {
'access': customer_config.get('jwt_access'),
'refresh': customer_config.get('jwt_refresh')
}
except (configparser.NoSectionError, KeyError):
token = None
jwt = None

if token:
self.api.set_token(token)
self.api.set_token(token, token_type='token')
elif jwt:
self.api.set_token(jwt, token_type='jwt')

if self.api.token:
try:
user = self.api.account.get()
LOG.info('Using token for {}'.format(user['results'][0]['email']))
return True
except HttpClientError as err:
LOG.debug(err)
LOG.info('Token is illegal or has expired')

try:
user = self.api.account.get()
LOG.info('Using token for {}'.format(user['results'][0]['email']))
return True
except HttpClientError as err:
LOG.debug(err)
LOG.info('Token is illegal or has expired')
if not self.args.email:
LOG.error('User email is required: --user')
sys.exit(1)

password = getpass.getpass()
ok = self.api.login(email=self.args.email, password=password)
Expand Down
2 changes: 2 additions & 0 deletions tests/data/test_main_login_token_file.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[c-test]
token=test-token
21 changes: 20 additions & 1 deletion tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import unittest

from archfx_cloud.api.connection import Api
from archfx_cloud.api.exceptions import HttpClientError, HttpServerError
from archfx_cloud.api.exceptions import HttpClientError, HttpServerError, ImproperlyConfigured


class ApiTestCase(unittest.TestCase):
Expand All @@ -20,9 +20,28 @@ def test_init(self):
def test_set_token(self):
api = Api()
self.assertEqual(api.token, None)

api.set_token('big-token')
self.assertEqual(api.token, 'big-token')

api.set_token({
'access': 'access-token',
'refresh': 'refresh-token',
})
self.assertEqual(api.token, 'access-token')
self.assertEqual(api.refresh_token_data, 'refresh-token')

api.set_token({
'token': 'another-token',
})
self.assertEqual(api.token, 'another-token')
self.assertEqual(api.refresh_token_data, 'refresh-token')

with self.assertRaises(ImproperlyConfigured):
api.set_token({
'other': 'dummy',
})

@requests_mock.Mocker()
def test_timeout(self, m):
m.get('http://archfx.test/api/v1/timeout/', exc=requests.exceptions.ConnectTimeout )
Expand Down
106 changes: 106 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import json
import os.path
import unittest
from argparse import Namespace

import mock
import requests_mock

from archfx_cloud.utils.main import BaseMain


class MainTestCase(unittest.TestCase):
@requests_mock.Mocker()
@mock.patch('archfx_cloud.utils.main.argparse.ArgumentParser.parse_args')
@mock.patch('archfx_cloud.utils.main.getpass.getpass')
def test_main_login_email_password_ok(
self, mock_request, mock_getpass, mock_parse_args
):
mock_request.post(
'https://test.archfx.io/api/v1/auth/login/',
text=json.dumps({'jwt': 'big-token', 'username': 'user1'}),
)
mock_request.post(
'https://test.archfx.io/api/v1/auth/logout/', status_code=204
)
mock_getpass.return_value = 'password'
mock_parse_args.return_value = Namespace(
customer='test', server_type='prod', email='[email protected]'
)

main = BaseMain()
main.main()

self.assertEqual(len(mock_request.request_history), 2)
self.assertEqual(
mock_request.request_history[0].url,
'https://test.archfx.io/api/v1/auth/login/',
)
self.assertTrue(
'Authorization' not in mock_request.request_history[0].headers
)
self.assertEqual(
json.loads(mock_request.request_history[0].body),
{'email': '[email protected]', 'password': 'password'},
)
self.assertEqual(
mock_request.request_history[1].url,
'https://test.archfx.io/api/v1/auth/logout/',
)
self.assertEqual(
mock_request.request_history[1].headers['Authorization'],
'jwt big-token',
)

@requests_mock.Mocker()
@mock.patch('archfx_cloud.utils.main.argparse.ArgumentParser.parse_args')
def test_main_login_token_file_ok(self, mock_request, mock_parse_args):
mock_request.get(
'https://test.archfx.io/api/v1/account/',
text=json.dumps({'results': [{'email': '[email protected]'}]}),
)
mock_request.post(
'https://test.archfx.io/api/v1/auth/logout/', status_code=204
)
mock_parse_args.return_value = Namespace(
customer='test',
server_type='prod',
)

main = BaseMain(
config_path=f'{os.path.dirname(__file__)}/data/test_main_login_token_file.ini'
)
main.main()

self.assertEqual(len(mock_request.request_history), 2)
self.assertEqual(
mock_request.request_history[0].url,
'https://test.archfx.io/api/v1/account/',
)
self.assertEqual(
mock_request.request_history[0].headers['Authorization'],
'token test-token',
)
self.assertEqual(
mock_request.request_history[1].url,
'https://test.archfx.io/api/v1/auth/logout/',
)
self.assertEqual(
mock_request.request_history[1].headers['Authorization'],
'token test-token',
)

@mock.patch('archfx_cloud.utils.main.argparse.ArgumentParser.parse_args')
def test_main_login_token_file_ko(self, mock_parse_args):
mock_parse_args.return_value = Namespace(
customer='test',
server_type='prod',
email=None,
)

main = BaseMain(config_path=f'non_existing_config_file.ini')

with self.assertRaises(SystemExit) as e:
main.main()

self.assertEqual(e.exception.code, 1)
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version = '0.13.0'
version = '0.14.0'

0 comments on commit 98a957e

Please sign in to comment.