1
0
mirror of https://github.com/ansible/tower-cli.git synced 2026-02-06 00:48:50 +01:00
Files
tower-cli/tests/test_api.py

315 lines
14 KiB
Python
Raw Permalink Normal View History

2015-06-10 21:41:57 -04:00
# Copyright 2015, Ansible, Inc.
# Luke Sneeringer <lsneeringer@ansible.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
2017-06-05 17:49:24 -04:00
import json
import warnings
2017-06-05 17:49:24 -04:00
from datetime import datetime as dt, timedelta
2014-07-11 13:33:56 -05:00
import requests
from requests.sessions import Session
from tower_cli.api import APIResponse, client, BasicTowerAuth,\
2017-06-05 17:49:24 -04:00
TOWER_DATETIME_FMT
2017-07-06 17:22:54 -04:00
from tower_cli import exceptions as exc
from tower_cli.conf import settings
2017-07-06 17:22:54 -04:00
from tower_cli.utils import debug
from tower_cli.utils.data_structures import OrderedDict
2017-07-18 11:58:59 -04:00
from tower_cli.constants import CUR_API_VERSION
from tests.compat import unittest, mock
2016-05-04 21:10:29 -04:00
import click
from fauxquests.adapter import FauxAdapter
REQUESTS_ERRORS = [requests.exceptions.ConnectionError,
requests.exceptions.SSLError]
class ClientTests(unittest.TestCase):
"""A set of tests to ensure that the API Client class works in the
way that we expect.
"""
def test_prefix_implicit_https(self):
"""Establish that the prefix property returns the appropriate
URL prefix given a host with no specified protocol.
"""
with settings.runtime_values(host='33.33.33.33'):
self.assertEqual(client.get_prefix(), 'https://33.33.33.33/api/%s/' % CUR_API_VERSION)
2018-06-08 11:04:05 -04:00
def test_prefix_https_protocol(self):
"""Establish that the http protocol works
"""
with settings.runtime_values(host='https://33.33.33.33'):
self.assertEqual(client.get_prefix(), 'https://33.33.33.33/api/%s/' % CUR_API_VERSION)
def test_prefix_http_protocol(self):
"""Establish that the http protocol works
"""
with settings.runtime_values(host='http://33.33.33.33', verify_ssl=False):
self.assertEqual(client.get_prefix(), 'http://33.33.33.33/api/%s/' % CUR_API_VERSION)
def test_prefix_explicit_protocol(self):
2018-06-08 11:04:05 -04:00
"""Establish that the prefix property can not start with non-http protocol.
"""
with settings.runtime_values(host='bogus://33.33.33.33/'):
2018-06-08 11:04:05 -04:00
with self.assertRaises(exc.ConnectionError):
client.get_prefix()
def test_request_ok(self):
"""Establish that a request that returns a valid JSON response
returns without incident and comes back as an APIResponse.
"""
with client.test_mode as t:
t.register_json('/ping/', {'status': 'ok'})
r = client.get('/ping/')
# Establish that our response is an APIResponse and that our
# JSONification method returns back an ordered dict.
self.assertIsInstance(r, APIResponse)
self.assertIsInstance(r.json(), OrderedDict)
# Establish that our headers have expected auth.
request = r.request
self.assertEqual(request.headers['Authorization'],
'Basic bWVhZ2FuOlRoaXMgaXMgdGhlIGJlc3Qgd2luZS4=')
# Make sure the content matches what we expect.
self.assertEqual(r.json(), {'status': 'ok'})
def test_request_post(self):
"""Establish that on a POST request, we encode the provided data
to JSON automatically.
"""
with client.test_mode as t:
t.register_json('/ping/', {'status': 'ok'}, method='POST')
r = client.post('/ping/', {'payload': 'this is my payload.'})
# Establish that our request has the expected payload, and
# is sent using an application/json content type.
headers = r.request.headers
self.assertEqual(headers['Content-Type'], 'application/json')
self.assertEqual(r.request.body,
'{"payload": "this is my payload."}')
def test_connection_ssl(self):
with client.test_mode as t:
t.register_json('/ping/', {'status': 'ok'})
https_adapter = client.adapters['https://']
with mock.patch.object(FauxAdapter, 'send', wraps=https_adapter.send) as mock_send:
client.get('/ping/')
mock_send.assert_called_once_with(
mock.ANY, cert=None, proxies=mock.ANY, stream=mock.ANY,
timeout=mock.ANY, verify=True
)
self.assertTrue(mock_send.call_args[1]['verify'])
def test_connection_ssl_error(self):
"""Establish that if we get a ConnectionError or an SSLError
back from requests, that we deal with it nicely.
2014-07-11 13:33:56 -05:00
"""
for ErrorType in REQUESTS_ERRORS:
with settings.runtime_values(verbose=False, host='https://foo.co'):
with mock.patch.object(Session, 'request') as req:
req.side_effect = ErrorType
2014-07-11 13:33:56 -05:00
with self.assertRaises(exc.ConnectionError):
client.get('/ping/')
def test_connection_ssl_error_verbose(self):
"""Establish that if we get a ConnectionError or an SSLError
back from requests, that we deal with it nicely, and
additionally print the internal error if verbose is True.
"""
for ErrorType in REQUESTS_ERRORS:
with settings.runtime_values(verbose=True, host='https://foo.co'):
with mock.patch.object(Session, 'request') as req:
req.side_effect = ErrorType
with mock.patch.object(debug, 'log') as dlog:
with self.assertRaises(exc.ConnectionError):
client.get('/ping/')
self.assertEqual(dlog.call_count, 5)
2014-07-11 13:33:56 -05:00
def test_server_error(self):
"""Establish that server errors raise the ServerError
exception as expected.
"""
with client.test_mode as t:
t.register('/ping/', 'ERRORED!!', status_code=500)
with self.assertRaises(exc.ServerError):
client.get('/ping/')
def test_auth_error(self):
"""Establish that authentication errors raise the AuthError
exception.
"""
with client.test_mode as t:
t.register('/ping/', 'ERRORED!!', status_code=401)
with self.assertRaises(exc.AuthError):
client.get('/ping/')
def test_forbidden_error(self):
"""Establish that forbidden errors raise the ForbiddenError
exception.
"""
with client.test_mode as t:
t.register('/ping/', 'ERRORED!!', status_code=403)
with self.assertRaises(exc.Forbidden):
client.get('/ping/')
def test_not_found_error(self):
"""Establish that authentication errors raise the NotFound
exception.
"""
with client.test_mode as t:
t.register('/ping/', 'ERRORED!!', status_code=404)
with self.assertRaises(exc.NotFound):
client.get('/ping/')
def test_method_not_allowed_error(self):
"""Establish that authentication errors raise the MethodNotAllowed
exception.
"""
with client.test_mode as t:
t.register('/ping/', 'ERRORED!!', status_code=405)
with self.assertRaises(exc.MethodNotAllowed):
client.get('/ping/')
def test_bad_request_error(self):
"""Establish that other errors not covered above raise the
BadRequest exception.
"""
with client.test_mode as t:
t.register('/ping/', "I'm a teapot!", status_code=418)
with self.assertRaises(exc.BadRequest):
client.get('/ping/')
2015-10-09 15:15:30 -04:00
def test_insecure_connection(self):
"""Establish that the --insecure flag will cause the program to
call request with verify=False.
"""
with mock.patch('requests.sessions.Session.request') as g:
mock_response = type('statobj', (), {})() # placeholder object
mock_response.status_code = 200
g.return_value = mock_response
with client.test_mode as t:
t.register('/ping/', "I'm a teapot!", status_code=200)
with settings.runtime_values(verify_ssl=False):
client.get('/ping/')
g.assert_called_once_with(
# The point is to assure verify=False below
'GET', mock.ANY, allow_redirects=True,
auth=mock.ANY, verify=False
)
2016-05-04 21:10:29 -04:00
def test_http_contradiction_error(self):
"""Establish that commands can not be ran with verify_ssl set
to false and an http connection."""
with settings.runtime_values(
host='http://33.33.33.33', verify_ssl=True):
with self.assertRaises(exc.TowerCLIError):
client.get_prefix()
2016-05-04 21:10:29 -04:00
def test_failed_suggestion_protocol(self):
"""Establish that if connection fails and protocol not given,
tower-cli suggests that to the user."""
with settings.runtime_values(verbose=False, host='foo.co'):
with mock.patch.object(Session, 'request') as req:
req.side_effect = requests.exceptions.SSLError
with mock.patch.object(click, 'secho') as secho:
with self.assertRaises(exc.ConnectionError):
client.get('/ping/')
2016-09-27 15:24:24 +01:00
self.assertTrue(secho.called)
2017-06-05 17:49:24 -04:00
class TowerAuthTokenTests(unittest.TestCase):
2017-06-05 17:49:24 -04:00
def setUp(self):
class Req(object):
def __init__(self):
self.headers = {}
with settings.runtime_values(use_token=True):
self.auth = BasicTowerAuth('alice', 'pass', client)
2017-06-05 17:49:24 -04:00
self.req = Req()
self.expires = dt.utcnow()
def test_reading_valid_token(self):
self.expires += timedelta(hours=1)
expires = self.expires.strftime(TOWER_DATETIME_FMT)
with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()):
with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}):
with client.test_mode as t:
t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
Send/Receive feature PR Added dependencies and related to POSSIBLE_TYPES They are needed for the import functionality i before e except after c Fixed typo in receive Added receive option Changed classname from Destroyer to Cleaner Added empty command Added required=False to scm-type field. This field is not required by the API to create a project Removed items from export if managed_by_tower = True Added credential_type to dependencies for a credential This allows resoluation of a credential_type when importing a credential Added send command Missing __init__.py file Added type parameter to cleaner Send and Receive now can do json or yaml Removed possible types, adding its items into resource objects Changed how assets are specified for export Added exception if nothing was passed in to process Added all option to cleaner method Fixed issue where properties added in the source were not caught as needed to be updated Added send/recieve/empty docs Updated help text for empty Move removeEncryptedValues to common Added deepclone of asset when determining if asset needs update This fixed an issue when removing items in the asset that were later referenced Fixed some issues when importing credentials Added workflow nodes import/export Initial commit of inventory groups/hosts/sources and fix for vault_creds in job_template Updated API to strip off /api/vX if passed in as url This allows you send URLs retreived from the API back into TowerCLI Caught additional exception If already parsed Java is passed in you could get a TypeError exception which was not caught. Added notification types Added workflow name to error message when extracting nodes When this condition occurs the message is displayed but in a receive no details are present as to what workflow had the error Added all_pages when requesting workflow_nodes Allowed for multiple tokens to be saved in ~/.tower_cli_token.json Added debug log if asset does not have POST options Removed unused import and debug message If an object does not have POST options, just return Changed how errors are logged Added project updates if asset was created or updated Removed local_path from SCM based projects Changed removal of encrypted values from asset to exported_asset Added workflow inventory and credential resolution Added inventory group import/export Added additional credential types for export/import Added options for passwords for new items Variablized workdlow node types Fixed workflow comparison Spell checking All post PR fixes for send/receive Modified print statements for python3 Wrapped TowerCLI class in try This should give us a better error when performing exports Don't get source project if its None Fixed user passwords Moved projects before inventory An inventory with a source from a project requires projects to be created first Added inventory source_script resolution Another change for comparing workflow nodes Revert "Merge remote-tracking branch 'upstream/master'" This reverts commit d8b0d2dbff2713a86aee4c7ca901d789b6e1d470, reversing changes made to 87ea5d2c6118f797dd908404974e790df8e4059a. Fixed unintended injection of documentation Added todo Removed comments for pull request Removed commants on dropping format Fixed bad merge Updated for flake8 Added all_pages to lists Better handeling of extra_vars Multiple changes Made empty take params like receive Output of send and empty are now ansible-ish Changed function name to python standards Extracted 'work' into method for calling programatically Fixed missing : Fixed newline at end of file causing flake8 failure removed closing #'s Changed from passing on TypeError to continuing if not a string Added min_value and max_value to list of known types Changed from .prefix to .get_prefix() Moved out touchup_extra_vars so that it applies to all asset_types Fixed issue where tox was sending back a json_token of 'invalid' causing issues with the dict Added missing POST endpoint Use six for string compare
2018-02-08 08:22:21 -05:00
t.register('/authtoken/', json.dumps({'token': 'foobar', 'expires': expires}), status_code=200,
method='POST')
self.auth(self.req)
self.assertEqual(self.req.headers['Authorization'], 'Token foobar')
2017-06-05 17:49:24 -04:00
def test_reading_invalid_token(self):
self.expires += timedelta(hours=1)
expires = self.expires.strftime(TOWER_DATETIME_FMT)
with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()):
with mock.patch('tower_cli.api.json.load', return_value="invalid"):
with client.test_mode as t:
t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
2017-06-05 17:49:24 -04:00
t.register('/authtoken/', json.dumps({'token': 'barfoo', 'expires': expires}), status_code=200,
method='POST')
self.auth(self.req)
self.assertEqual(self.req.headers['Authorization'], 'Token barfoo')
def test_reading_expired_token(self):
self.expires += timedelta(hours=-1)
expires = self.expires.strftime(TOWER_DATETIME_FMT)
with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()):
with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}):
with client.test_mode as t:
t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
2017-06-05 17:49:24 -04:00
t.register('/authtoken/', json.dumps({'token': 'barfoo', 'expires': expires}), status_code=200,
method='POST')
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
self.auth(self.req)
2017-06-05 17:49:24 -04:00
self.assertEqual(self.req.headers['Authorization'], 'Token barfoo')
def test_reading_invalid_token_from_server(self):
self.expires += timedelta(hours=-1)
expires = self.expires.strftime(TOWER_DATETIME_FMT)
with mock.patch('six.moves.builtins.open', new_callable=mock.mock_open()):
with mock.patch('tower_cli.api.json.load', return_value={'token': 'foobar', 'expires': expires}):
with client.test_mode as t:
with self.assertRaises(exc.AuthError):
t.register('/authtoken/', json.dumps({}), status_code=200, method='OPTIONS')
2017-06-05 17:49:24 -04:00
t.register('/authtoken/', json.dumps({'invalid': 'invalid'}), status_code=200, method='POST')
self.auth(self.req)
def test_auth_token_unsupported(self):
# If the user specifies `use_token=True`, but `/authtoken/` doesn't
# exist (in Tower 3.3 and beyond), just fall back to basic auth
with client.test_mode as t:
with settings.runtime_values(use_token=True):
t.register('/authtoken/', json.dumps({}), status_code=404, method='OPTIONS')
auth = BasicTowerAuth('alice', 'pass', client)
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
auth(self.req)
assert self.req.headers == {'Authorization': 'Basic YWxpY2U6cGFzcw=='}
def test_oauth_bearer_token(self):
token = 'Azly3WBiYWeGKfImK25ftpJR1nvn6JABC123'
with settings.runtime_values(oauth_token=token):
auth = BasicTowerAuth(None, None, client)
auth(self.req)
assert self.req.headers == {
'Authorization': 'Bearer {}'.format(token)
}