1
0
mirror of https://github.com/ansible/tower-cli.git synced 2026-02-07 03:47:37 +01:00
Files
tower-cli/tests/test_api.py
John Westcott IV 090da3fd3c 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-03-14 13:52:04 -04:00

290 lines
13 KiB
Python

# 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.
import json
import warnings
from datetime import datetime as dt, timedelta
import requests
from requests.sessions import Session
from tower_cli.api import APIResponse, client, BasicTowerAuth,\
TOWER_DATETIME_FMT
from tower_cli import exceptions as exc
from tower_cli.conf import settings
from tower_cli.utils import debug
from tower_cli.utils.data_structures import OrderedDict
from tower_cli.constants import CUR_API_VERSION
from tests.compat import unittest, mock
import click
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)
def test_prefix_explicit_protocol(self):
"""Establish that the prefix property returns the appropriate
URL prefix and don't clobber over an explicit protocol.
"""
with settings.runtime_values(host='bogus://33.33.33.33/'):
self.assertEqual(client.get_prefix(), 'bogus://33.33.33.33/api/%s/' % CUR_API_VERSION)
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_error(self):
"""Establish that if we get a ConnectionError or an SSLError
back from requests, that we deal with it nicely.
"""
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
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)
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/')
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
)
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()
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/')
self.assertTrue(secho.called)
class TowerAuthTokenTests(unittest.TestCase):
def setUp(self):
class Req(object):
def __init__(self):
self.headers = {}
with settings.runtime_values(use_token=True):
self.auth = BasicTowerAuth('alice', 'pass', client)
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')
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')
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')
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')
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)
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')
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)
}