pax_global_header00006660000000000000000000000064127777025520014530gustar00rootroot0000000000000052 comment=3dd077c984ad077c4738036a2644cf4955a581fe mock-services-0.3/000077500000000000000000000000001277770255200141445ustar00rootroot00000000000000mock-services-0.3/.gitignore000066400000000000000000000000431277770255200161310ustar00rootroot00000000000000*.egg *.egg-info *.pyc *.swp .tox* mock-services-0.3/AUTHORS000066400000000000000000000000771277770255200152200ustar00rootroot00000000000000Authors ======= * Florent Pigout mock-services-0.3/CHANGELOG000066400000000000000000000010641277770255200153570ustar00rootroot00000000000000Changelog ========= 0.3 (2016-10-13) ---------------- - Python 3 compatibility - Update pip 0.2 (2016-01-07) ---------------- - Add HEAD method - Add missing PUT method - Move to requests-mock - Raise ConnectionError on unmatched request - Fix too restrictive body argument handling 0.1 (2015-12-10) ---------------- - Add/remove logs - Add validators - Make optional field validation - Fix UUID serialization - Enhance README - Rename to mock-services - Add service behaviour - Add CI shield - Add make release - Add first implementation - Initial commit mock-services-0.3/LICENSE000066400000000000000000000020461277770255200151530ustar00rootroot00000000000000Copyright (c) 2015 Novapost/PeopleDoc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mock-services-0.3/MANIFEST.in000066400000000000000000000001671277770255200157060ustar00rootroot00000000000000recursive-include mock_services * include AUTHORS include CHANGELOG include LICENSE include README.rst include VERSION mock-services-0.3/Makefile000066400000000000000000000003631277770255200156060ustar00rootroot00000000000000all: .PHONY: install install: pip install -e . .PHONY: develop develop: install pip install -e ".[test]" .PHONY: test test: flake8 . python -m unittest discover tests/ .PHONY: release release: pip install -e ".[release]" fullrelease mock-services-0.3/README.rst000066400000000000000000000165601277770255200156430ustar00rootroot00000000000000============= Mock services ============= .. image:: https://circleci.com/gh/novafloss/mock-services.svg?style=shield :target: https://circleci.com/gh/novafloss/mock-services :alt: We are under CI!! Aims to provide an easy way to mock an entire service API based on `requests-mock`_ and a simple dict definition of a service. The idea is to mock everything at start according given rules. Then `mock-services`_ allows to *start/stop* http mock locally. During our session we can: - add rules - permit external calls - stop mocking - reset rules - restart mocking - etc. Mock endpoints explicitly ========================= *Note:* rules urls must be regex. They always will be compiled before updating the main `requests-mock`_ urls registry. Let's mock our favorite search engine:: >>> def fake_duckduckgo_cb(request): ... return 200, {}, 'Coincoin!' >>> rules = [ ... { ... 'text': fake_duckduckgo_cb, ... 'headers': {'Content-Type': 'text/html'}, ... 'method': 'GET', ... 'url': r'^https://duckduckgo.com/\?q=' ... }, ... ] >>> from mock_services import update_http_rules >>> update_http_rules(rules) >>> import requests >>> requests.get('https://duckduckgo.com/?q=mock-services').content[:15] '' >>> from mock_services import start_http_mock >>> start_http_mock() >>> requests.get('https://duckduckgo.com/?q=mock-services').content 'Coincoin!' When the http_mock is started if you try to call an external url, it should fail:: >>> requests.get('https://www.google.com/#q=mock-services') ... ConnectionError: Connection refused: GET https://www.google.com/#q=mock-services Then you can allow external calls if needed:: >>> from mock_services import http_mock >>> http_mock.set_allow_external(True) >>> requests.get('https://www.google.com/#q=mock-services').content[:15] '' At anytime you can stop the mocking as follow:: >>> from mock_services import stop_http_mock >>> stop_http_mock() >>> requests.get('https://duckduckgo.com/?q=mock-services').content[:15] '' Or stop mocking during a function call:: >>> start_http_mock() >>> @no_http_mock ... def please_do_not_mock_me(): ... return requests.get('https://duckduckgo.com/?q=mock-services').content[:15] == '', 'mocked!' >>> please_do_not_mock_me Or start mocking for another function call:: >>> stop_http_mock() >>> @with_http_mock ... def please_mock_me(): ... assert requests.get('https://duckduckgo.com/?q=mock-services').content == 'Coincoin', 'no mock!' >>> please_mock_me Mock service easy ================= You can add REST rules with an explicit method. It will add rules as above and automatically bind callbacks to fake a REST service. *Note:* *resource* and *id* regex options are mandatory in the rules urls. Additionally, `mock_services`_ include `attrs`_ library. It can be use for field validation as follow. This service mock will create, get, update and delete resources for you:: >>> import attr >>> rest_rules = [ ... { ... 'method': 'LIST', ... 'url': r'^http://my_fake_service/(?Papi)$' ... }, ... { ... 'method': 'GET', ... 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)$', ... }, ... { ... 'method': 'GET', ... 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)/(?Pdownload)$', ... }, ... { ... 'method': 'POST', ... 'url': r'^http://my_fake_service/(?Papi)$', ... 'id_name': 'id', ... 'id_factory': int, ... 'attrs': { ... 'bar': attr.ib(), ... 'foo':attr.ib(default=True) ... } ... }, ... { ... 'method': 'PATCH', ... 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)$', ... }, ... { ... 'method': 'DELETE', ... 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)$' ... }, ... ] >>> from mock_services import update_rest_rules >>> update_rest_rules(rest_rules) >>> from mock_services import start_http_mock >>> start_http_mock() >>> response = requests.get('http://my_fake_service/api') >>> response.status_code 200 >>> response.json() [] >>> response = requests.get('http://my_fake_service/api/1') >>> response.status_code 404 >>> import json >>> response = requests.post('http://my_fake_service/api', ... data=json.dumps({}), ... headers={'content-type': 'application/json'}) >>> response.status_code 400 >>> response = requests.post('http://my_fake_service/api', ... data=json.dumps({'bar': 'Python will save the world'}), ... headers={'content-type': 'application/json'}) >>> response.status_code 201 >>> response.json() { 'id': 1, 'foo'; True, 'bar'; 'Python will save the world.' } >>> response = requests.patch('http://my_fake_service/api/1', ... data=json.dumps({'bar': "Python will save the world. I don't know how. But it will."}), ... headers={'content-type': 'application/json'}) >>> response.status_code 200 >>> response = requests.get('http://my_fake_service/api/1') >>> response.status_code 200 >>> response.json() { 'id': 1, 'foo'; True, 'bar'; "Python will save the world. I don't know how. But it will." } >>> response = requests.delete('http://my_fake_service/api/1') >>> response.status_code 204 More validation =============== Is some cases you need to validate a resource against another. Then you can add global validators per endpoint as follow:: >>> from mock_services import storage >>> from mock_services.service import ResourceContext >>> from mock_services.exceptions import Http409 >>> def duplicate_foo(request): ... data = json.loads(request.body) ... ctx = ResourceContext(hostname='my_fake_service', resource='api') ... if data['foo'] in [o['foo'] for o in storage.list(ctx)]: ... raise Http409 >>> rest_rules_with_validators = [ ... { ... 'method': 'POST', ... 'url': r'^http://my_fake_service/(?Papi)$', ... 'validators': [ ... duplicate_foo, ... ], ... }, ... ] >>> response = requests.post('http://my_fake_service/api', ... data=json.dumps({'foo': 'bar'}), ... headers={'content-type': 'application/json'}) >>> response.status_code 201 >>> response = requests.post('http://my_fake_service/api', ... data=json.dumps({'foo': 'bar'}), ... headers={'content-type': 'application/json'}) >>> response.status_code 409 Have fun in testing external APIs ;) .. _`attrs`: https://github.com/hynek/attrs .. _`requests-mock`: https://github.com/openstack/requests-mock .. _`mock-services`: https://github.com/novafloss/mock-services mock-services-0.3/VERSION000066400000000000000000000000041277770255200152060ustar00rootroot000000000000000.3 mock-services-0.3/circle.yml000066400000000000000000000002171277770255200161300ustar00rootroot00000000000000machine: post: - pyenv global 2.7.10 3.4.4 3.5.1 dependencies: pre: - pip install -U pip setuptools test: override: - tox -r mock-services-0.3/mock_services/000077500000000000000000000000001277770255200170005ustar00rootroot00000000000000mock-services-0.3/mock_services/__init__.py000066400000000000000000000011451277770255200211120ustar00rootroot00000000000000# -*- coding: utf-8 -*- import pkg_resources from .decorators import no_http_mock from .decorators import with_http_mock from .helpers import is_http_mock_started from .helpers import start_http_mock from .helpers import stop_http_mock from .rules import update_http_rules from .rules import update_rest_rules from .rules import reset_rules __all__ = [ 'no_http_mock', 'with_http_mock', 'is_http_mock_started', 'start_http_mock', 'stop_http_mock', 'reset_rules', 'update_http_rules', 'update_rest_rules', ] __version__ = pkg_resources.get_distribution(__package__).version mock-services-0.3/mock_services/decorators.py000066400000000000000000000040151277770255200215170ustar00rootroot00000000000000# -*- coding: utf-8 -*- import json import logging from functools import wraps from .exceptions import Http400 from .exceptions import Http401 from .exceptions import Http403 from .exceptions import Http404 from .exceptions import Http405 from .exceptions import Http409 from .exceptions import Http500 from .helpers import start_http_mock from .helpers import stop_http_mock logger = logging.getLogger(__name__) def no_http_mock(f): @wraps(f) def wrapped(*args, **kwargs): stopped = stop_http_mock() r = f(*args, **kwargs) if stopped: start_http_mock() return r return wrapped def with_http_mock(f): @wraps(f) def wrapped(*args, **kwargs): started = start_http_mock() return f(*args, **kwargs) if started: stop_http_mock() return wrapped def trap_errors(f): @wraps(f) def wrapped(request, context, *args, **kwargs): try: return f(request, context, *args, **kwargs) except Http400: context.status_code = 400 return 'Bad Request' except Http401: context.status_code = 401 return 'Unauthorized' except Http403: context.status_code = 403 return 'Forbidden' except Http404: context.status_code = 404 return 'Not Found' except Http405: context.status_code = 405 return 'Method Not Allowed' except Http409: context.status_code = 409 return 'Conflict' except (Exception, Http500) as e: logger.exception(e) context.status_code = 500 return 'Internal Server Error' return wrapped def to_json(f): @wraps(f) def wrapped(request, context, *args, **kwargs): data = f(request, context, *args, **kwargs) # traped error are not json by default if context.status_code >= 400: data = {'error': data} return json.dumps(data) return wrapped mock-services-0.3/mock_services/exceptions.py000066400000000000000000000004331277770255200215330ustar00rootroot00000000000000# -*- coding: utf-8 -*- class Http400(Exception): pass class Http401(Exception): pass class Http403(Exception): pass class Http404(Exception): pass class Http405(Exception): pass class Http409(Exception): pass class Http500(Exception): pass mock-services-0.3/mock_services/helpers.py000066400000000000000000000007111277770255200210130ustar00rootroot00000000000000# -*- coding: utf-8 -*- import logging from . import http_mock logger = logging.getLogger(__name__) def is_http_mock_started(): return http_mock.is_started() def start_http_mock(): if not http_mock.is_started(): http_mock.start() logger.debug('http mock started') return True def stop_http_mock(): if http_mock.is_started(): http_mock.stop() logger.debug('http mock stopped') return True mock-services-0.3/mock_services/http_mock.py000066400000000000000000000040201277770255200213360ustar00rootroot00000000000000import requests from requests.exceptions import ConnectionError from requests_mock import Adapter from requests_mock import MockerCore from requests_mock.exceptions import NoMockAddress class HttpAdapter(Adapter): def get_rules(self): return self._matchers def reset(self): self._matchers = [] _adapter = HttpAdapter() class HttpMock(MockerCore): def __init__(self, *args, **kwargs): super(HttpMock, self).__init__(*args, **kwargs) self._adapter = _adapter def is_started(self): return self._real_send def set_allow_external(self, allow): """Set flag to authorize external calls when no matching mock. Will raise a ConnectionError otherwhise. """ self._real_http = allow def _patch_real_send(self): _fake_send = requests.Session.send def _patched_fake_send(session, request, **kwargs): try: return _fake_send(session, request, **kwargs) except NoMockAddress: request = _adapter.last_request error_msg = 'Connection refused: {0} {1}'.format( request.method, request.url ) response = ConnectionError(error_msg) response.request = request raise response requests.Session.send = _patched_fake_send def start(self): """Overrides default start behaviour by raising ConnectionError instead of custom requests_mock.exceptions.NoMockAddress. """ super(HttpMock, self).start() self._patch_real_send() _http_mock = HttpMock() __all__ = [] # expose mocker instance public methods for __attr in [a for a in dir(_http_mock) if not a.startswith('_')]: __all__.append(__attr) globals()[__attr] = getattr(_http_mock, __attr) # expose adapter instance public methods for __attr in [a for a in dir(_adapter) if not a.startswith('_')]: __all__.append(__attr) globals()[__attr] = getattr(_adapter, __attr) mock-services-0.3/mock_services/rules.py000066400000000000000000000052771277770255200205170ustar00rootroot00000000000000# -*- coding: utf-8 -*- import logging import re from copy import deepcopy from functools import partial from requests_mock.response import _BODY_ARGS from . import http_mock from . import service from . import storage logger = logging.getLogger(__name__) METHODS = [ 'LIST', # custom 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', ] def reset_rules(): storage.reset() http_mock.reset() def update_http_rules(rules, content_type='text/plain'): """Adds rules to global http mock. It permits to set mock in a more global way than decorators, cf.: https://github.com/openstack/requests-mock Here we assume urls in the passed dict are regex we recompile before adding a rule. Rules example: >>> def fake_duckduckgo_cb(request): ... return 200, {}, 'Coincoin!' >>> rules = [ { 'method': 'GET', 'status_code': 200, 'text': 'I am watching you', 'url': r'^https://www.google.com/#q=' }, { 'method': 'GET', 'text': fake_duckduckgo_cb, 'url': r'^https://duckduckgo.com/?q=' }, ] """ for kw in deepcopy(rules): kw['url'] = re.compile(kw['url']) # ensure headers dict for at least have a default content type if 'Content-Type' not in kw.get('headers', {}): kw['headers'] = dict(kw.get('headers', {}), **{ 'Content-Type': content_type, }) method = kw.pop('method') url = kw.pop('url') http_mock.register_uri(method, url, **kw) def update_rest_rules(rules, content_type='application/json'): http_rules = [] for kw in deepcopy(rules): if kw['method'] not in METHODS: raise NotImplementedError('invalid method "{method}" for: {url}'.format(**kw)) # noqa # set callback if does not has one if not any(x for x in _BODY_ARGS if x in kw): _cb = getattr(service, '{0}_cb'.format(kw['method'].lower())) kw['text'] = partial(_cb, **kw.copy()) # no content if kw['method'] in ['DELETE', 'HEAD'] \ and'Content-Type' not in kw.get('headers', {}): kw['headers'] = dict(kw.get('headers', {}), **{ 'Content-Type': 'text/plain', }) # restore standard method if kw['method'] == 'LIST': kw['method'] = 'GET' # clean extra kwargs kw.pop('attrs', None) kw.pop('id_name', None) kw.pop('id_factory', None) kw.pop('validators', None) # update http_rules http_rules.append(kw) update_http_rules(http_rules, content_type=content_type) mock-services-0.3/mock_services/service.py000066400000000000000000000077141277770255200210230ustar00rootroot00000000000000# -*- coding: utf-8 -*- import json import logging import re try: from urllib import parse as urlparse except ImportError: # Python 2 import urlparse import attr from . import storage from .decorators import to_json from .decorators import trap_errors from .exceptions import Http400 from .exceptions import Http404 logger = logging.getLogger(__name__) @attr.s class ResourceContext(object): hostname = attr.ib() resource = attr.ib() action = attr.ib(default='default') id = attr.ib(default=None) @property def key(self): return '{hostname}/{resource}/{action}'.format(**attr.asdict(self)) def parse_url(request, url_pattern, id=None, require_id=False): logger.debug('url_pattern: %s', url_pattern) logger.debug('url: %s', request.url) url_kw = re.compile(url_pattern).search(request.url).groupdict() logger.debug('url_kw: %s', url_kw) if 'resource' not in url_kw: raise Http404 if require_id and 'id' not in url_kw: raise Http404 hostname = urlparse.urlparse(request.url).hostname logger.debug('hostname: %s', hostname) action = url_kw.pop('action', 'default') logger.debug('action: %s', action) resource_context = ResourceContext( hostname=hostname, resource=url_kw.pop('resource'), action=action, id=url_kw.pop('id', id), ) logger.debug('resource_context: %s', attr.asdict(resource_context)) return resource_context def validate_data(request, attrs=None, validators=None): logger.debug('attrs: %s', attrs) logger.debug('body: %s', request.body) data = json.loads(request.body) data_to_validate = {k: v for k, v in data.items() if k in (attrs or {}).keys()} logger.debug('data_to_validate: %s', data_to_validate) # missing field if attrs and not data_to_validate: raise Http400 # invalid field if data_to_validate: try: attr.make_class("C", attrs)(**data_to_validate) except (TypeError, ValueError): raise Http400 # custom validation for validate_func in (validators or []): validate_func(request) return data @to_json @trap_errors def list_cb(request, context, url=None, **kwargs): resource_context = parse_url(request, url) context.status_code = 200 return storage.to_list(resource_context) @to_json @trap_errors def get_cb(request, context, url=None, **kwargs): resource_context = parse_url(request, url, require_id=True) context.status_code = 200 return storage.get(resource_context) @trap_errors def head_cb(request, context, url=None, id_name='id', **kwargs): resource_context = parse_url(request, url, require_id=True) context.headers = dict(context.headers or {}, **{id_name: resource_context.id}) context.status_code = 200 return '' @to_json @trap_errors def post_cb(request, context, url=None, id_name='id', id_factory=int, attrs=None, validators=None, **kwargs): data = validate_data(request, attrs=attrs, validators=validators) id = storage.next_id(id_factory) logger.debug('id: %s', id) data.update({ id_name: id }) logger.debug('data: %s', data) resource_context = parse_url(request, url, id=id) context.status_code = 201 return storage.add(resource_context, data) @to_json @trap_errors def patch_cb(request, context, url=None, attrs=None, validators=None, **kwargs): data = validate_data(request, attrs=attrs, validators=validators) logger.debug('data: %s', data) resource_context = parse_url(request, url, require_id=True) context.status_code = 200 return storage.update(resource_context, data) put_cb = patch_cb @trap_errors def delete_cb(request, context, url=None, **kwargs): resource_context = parse_url(request, url, require_id=True) context.status_code = 204 return storage.remove(resource_context) or '' mock-services-0.3/mock_services/storage.py000066400000000000000000000040001277770255200210100ustar00rootroot00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import import logging import uuid from collections import defaultdict from functools import wraps from itertools import count from .exceptions import Http404 from .exceptions import Http409 from .exceptions import Http500 logger = logging.getLogger(__name__) def check_conflict(f): @wraps(f) def wrapped(self, ctx, *args, **kwargs): ctx.id = str(ctx.id) if ctx.id in self._registry[ctx.key]: raise Http409 return f(self, ctx, *args, **kwargs) return wrapped def check_exist(f): @wraps(f) def wrapped(self, ctx, *args, **kwargs): ctx.id = str(ctx.id) if ctx.id not in self._registry[ctx.key]: raise Http404 return f(self, ctx, *args, **kwargs) return wrapped class Storage(object): _counter = None _registry = None def __init__(self): self.reset() @check_conflict def add(self, ctx, data): self._registry[ctx.key][ctx.id] = data return data @check_exist def get(self, ctx): return self._registry[ctx.key][ctx.id] def to_list(self, ctx): return list(self._registry[ctx.key].values()) def next_id(self, id_factory): if id_factory == int: return next(self._counter) if id_factory == uuid.UUID: return str(uuid.uuid4()) logger.error('invalid id factory: %s', id_factory) raise Http500 @check_exist def remove(self, ctx): del self._registry[ctx.key][ctx.id] def reset(self): self._counter = count(start=1) self._registry = defaultdict(dict) @check_exist def update(self, ctx, data): self._registry[ctx.key][ctx.id].update(data) return self._registry[ctx.key][ctx.id] _storage = Storage() __all__ = [] # expose storage instance public methods for __attr in (a for a in dir(_storage) if not a.startswith('_')): __all__.append(__attr) globals()[__attr] = getattr(_storage, __attr) mock-services-0.3/setup.cfg000066400000000000000000000000501277770255200157600ustar00rootroot00000000000000[zest.releaser] with create-wheel = yes mock-services-0.3/setup.py000066400000000000000000000024621277770255200156620ustar00rootroot00000000000000#!/usr/bin/env python import os from setuptools import setup here = os.path.abspath(os.path.dirname(__file__)) setup( name='mock-services', version=open(os.path.join(here, 'VERSION')).read().strip(), description='Mock services.', long_description=open(os.path.join(here, 'README.rst')).read(), classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Console', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', ], keywords=[ 'http', 'mock', 'requests', 'rest', ], author='Florent Pigout', author_email='florent.pigout@novapost.fr', url='https://github.com/novafloss/mock-services', license='MIT', install_requires=[ 'attrs', 'funcsigs', 'requests-mock', ], extras_require={ 'test': [ 'flake8' ], 'release': [ 'wheel', 'zest.releaser' ], }, packages=[ 'mock_services' ], ) mock-services-0.3/tests/000077500000000000000000000000001277770255200153065ustar00rootroot00000000000000mock-services-0.3/tests/__init__.py000066400000000000000000000000001277770255200174050ustar00rootroot00000000000000mock-services-0.3/tests/test_http_mock.py000066400000000000000000000156471277770255200207240ustar00rootroot00000000000000import logging import unittest import requests from requests.exceptions import ConnectionError from mock_services import http_mock from mock_services import is_http_mock_started from mock_services import no_http_mock from mock_services import reset_rules from mock_services import start_http_mock from mock_services import stop_http_mock from mock_services import update_http_rules from mock_services import with_http_mock logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)-8s %(name)s %(message)s' ) def fake_duckduckgo_cb(request, context): return 'Coincoin!' rules = [ { 'text': fake_duckduckgo_cb, 'headers': {'Content-Type': 'text/html'}, 'method': 'GET', 'url': r'^https://duckduckgo.com/\?q=' }, ] class HttpTestCase(unittest.TestCase): def setUp(self): stop_http_mock() reset_rules() http_mock.set_allow_external(False) tearDown = setUp def test_reset_rules(self): self.assertFalse(http_mock.get_rules()) update_http_rules(rules) self.assertEqual(len(http_mock.get_rules()), 1) # reset reset_rules() self.assertFalse(http_mock.get_rules()) def test_update_rules(self): self.assertFalse(http_mock.get_rules()) # add first rule update_http_rules(rules) self.assertEqual(len(http_mock.get_rules()), 1) matcher = http_mock.get_rules()[0] self.assertEqual(matcher._method, 'GET') self.assertTrue(hasattr(matcher._url, 'match')) self.assertTrue(matcher._url.match('https://duckduckgo.com/?q=mock-services')) # noqa response = matcher._responses[0] self.assertTrue(hasattr(response._params['text'], '__call__')) self.assertEqual(response._params['headers']['Content-Type'], 'text/html') # noqa # add second rule update_http_rules([ { 'method': 'POST', 'status_code': 201, 'text': '{"coin": 1}', 'url': r'http://dummy/', }, ]) self.assertEqual(len(http_mock.get_rules()), 2) matcher = http_mock.get_rules()[1] self.assertTrue(hasattr(matcher._url, 'match')) self.assertTrue(matcher._url.match('http://dummy/')) self.assertEqual(matcher._method, 'POST') response = matcher._responses[0] self.assertEqual(response._params['status_code'], 201) self.assertEqual(response._params['text'], '{"coin": 1}') self.assertEqual(response._params['headers']['Content-Type'], 'text/plain') # noqa def test_start_http_mock(self): update_http_rules(rules) response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content[:15], b'') self.assertTrue(start_http_mock()) response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'Coincoin!') def test_stop_http_mock(self): update_http_rules(rules) self.assertTrue(start_http_mock()) response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'Coincoin!') self.assertTrue(stop_http_mock()) response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content[:15], b'') def test_restart_http_mock(self): update_http_rules(rules) start_http_mock() response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'Coincoin!') self.assertTrue(stop_http_mock()) # already stopped self.assertFalse(stop_http_mock()) response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content[:15], b'') self.assertTrue(start_http_mock()) response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'Coincoin!') # already started self.assertFalse(start_http_mock()) def test_is_http_mock_started(self): update_http_rules(rules) self.assertFalse(is_http_mock_started()) self.assertTrue(start_http_mock()) self.assertTrue(is_http_mock_started()) def test_no_http_mock(self): update_http_rules(rules) self.assertTrue(start_http_mock()) @no_http_mock def please_do_not_mock_me(): self.assertFalse(is_http_mock_started()) response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content[:15], b'') self.assertTrue(is_http_mock_started()) def test_with_http_mock(self): update_http_rules(rules) self.assertFalse(is_http_mock_started()) @with_http_mock def please_do_not_mock_me(): self.assertTrue(is_http_mock_started()) response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'Coincoin!') self.assertFalse(is_http_mock_started()) def test_real_http_0(self): update_http_rules(rules) self.assertTrue(start_http_mock()) # mocked response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'Coincoin!') # not mocked but fail self.assertRaises(ConnectionError, requests.get, 'https://www.google.com/#q=mock-services') # test we keep the request try: url = 'https://www.google.com/#q=mock-services' requests.get(url) except ConnectionError as e: self.assertEqual(e.request.url, url) def test_real_http_1(self): update_http_rules(rules) self.assertTrue(start_http_mock()) # allow external call http_mock.set_allow_external(True) # mocked response = requests.get('https://duckduckgo.com/?q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'Coincoin!') # not mocked but do an external call response = requests.get('https://www.google.com/#q=mock-services') self.assertEqual(response.status_code, 200) self.assertEqual(response.content[:15], b'') mock-services-0.3/tests/test_rest_mock.py000066400000000000000000000263511277770255200207140ustar00rootroot00000000000000import json import logging import unittest import uuid from functools import partial import attr import requests from mock_services import reset_rules from mock_services import start_http_mock from mock_services import stop_http_mock from mock_services import update_rest_rules from mock_services import http_mock from mock_services import storage from mock_services.exceptions import Http400 from mock_services.exceptions import Http409 from mock_services.service import ResourceContext CONTENTTYPE_JSON = {'Content-Type': 'application/json'} logging.basicConfig( level=logging.INFO, format='%(asctime)s %(levelname)-8s %(name)s %(message)s' ) rest_rules = [ { 'method': 'LIST', 'url': r'^http://my_fake_service/(?Papi)$' }, { 'method': 'HEAD', 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)$', }, { 'method': 'GET', 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)$', }, { 'method': 'GET', 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)/(?Pdownload)$', # noqa }, { 'method': 'POST', 'url': r'^http://my_fake_service/(?Papi)$', 'id_name': 'id', 'id_factory': int, 'attrs': { 'bar': attr.ib() } }, { 'method': 'PATCH', 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)$', }, { 'method': 'PUT', 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)$', 'attrs': { 'foo': attr.ib(), 'bar': attr.ib() } }, { 'method': 'DELETE', 'url': r'^http://my_fake_service/(?Papi)/(?P\d+)$' }, { 'method': 'GET', 'url': r'^http://my_fake_service/(?Papi/v2)/(?P\w+-\w+-\w+-\w+-\w+)$', # noqa }, { 'method': 'POST', 'url': r'^http://my_fake_service/(?Papi/v2)$', 'id_name': 'uuid', 'id_factory': uuid.UUID, }, ] def should_have_foo(request): data = json.loads(request.body) if 'foo' not in data: raise Http400 def duplicate_foo(request): data = json.loads(request.body) ctx = ResourceContext(hostname='my_fake_service', resource='api') if data['foo'] in [o['foo'] for o in storage.to_list(ctx)]: raise Http409 rest_rules_with_validators = [ { 'method': 'POST', 'url': r'^http://my_fake_service/(?Papi)$', 'validators': [ should_have_foo, duplicate_foo, ], }, ] class RestTestCase(unittest.TestCase): def setUp(self): stop_http_mock() reset_rules() tearDown = setUp def _test_rule(self, index, method, url_to_match, content_type='application/json'): matcher = http_mock.get_rules()[index] self.assertEqual(matcher._method, method) self.assertTrue(hasattr(matcher._url, 'match')) self.assertTrue(matcher._url.match(url_to_match)) response = matcher._responses[0] self.assertTrue(hasattr(response._params['text'], '__call__')) self.assertEqual(response._params['headers']['Content-Type'], content_type) # noqa def test_update_rules(self): self.assertFalse(http_mock.get_rules()) update_rest_rules(rest_rules) self.assertEqual(len(http_mock.get_rules()), 10) self._test_rule(0, 'GET', 'http://my_fake_service/api') self._test_rule(1, 'HEAD', 'http://my_fake_service/api/1', content_type='text/plain') self._test_rule(2, 'GET', 'http://my_fake_service/api/1') self._test_rule(3, 'GET', 'http://my_fake_service/api/1/download') self._test_rule(4, 'POST', 'http://my_fake_service/api') self._test_rule(5, 'PATCH', 'http://my_fake_service/api/1') self._test_rule(6, 'PUT', 'http://my_fake_service/api/1') self._test_rule(7, 'DELETE', 'http://my_fake_service/api/1', content_type='text/plain') self._test_rule(8, 'GET', 'http://my_fake_service/api/v2/{0}'.format(uuid.uuid4())) # noqa self._test_rule(9, 'POST', 'http://my_fake_service/api/v2') def test_update_rules_invalid_method(self): update_func = partial(update_rest_rules, [ { 'text': '', 'method': 'INVALID', 'status_code': 200, 'url': r'^https://invalid_method.com/' } ]) self.assertRaises(NotImplementedError, update_func, 'invalid method "INVALID" for: ^https://invalid_method.com/') # noqa def test_rest_mock(self): url = 'http://my_fake_service/api' update_rest_rules(rest_rules) self.assertTrue(start_http_mock()) r = requests.get(url) self.assertEqual(r.status_code, 200) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), []) r = requests.get(url + '/1') self.assertEqual(r.status_code, 404) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), {'error': 'Not Found'}) r = requests.get(url + '/1/download') self.assertEqual(r.status_code, 404) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), {'error': 'Not Found'}) r = requests.post(url, data=json.dumps({}), headers=CONTENTTYPE_JSON) self.assertEqual(r.status_code, 400) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), {'error': 'Bad Request'}) r = requests.patch(url + '/1', data=json.dumps({})) self.assertEqual(r.status_code, 404) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), {'error': 'Not Found'}) r = requests.delete(url + '/1') self.assertEqual(r.status_code, 404) self.assertEqual(r.headers, {'content-type': 'text/plain'}) self.assertEqual(r.content, b'Not Found') # add some data r = requests.post(url, data=json.dumps({ 'foo': True, 'bar': 'Python will save the world.', }), headers=CONTENTTYPE_JSON) self.assertEqual(r.status_code, 201) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), { 'id': 1, 'foo': True, 'bar': 'Python will save the world.', }) r = requests.head(url + '/1') self.assertEqual(r.status_code, 200) self.assertEqual(r.headers, { 'content-type': 'text/plain', 'id': '1', }) self.assertEqual(r.content, b'') # recheck list get ... r = requests.get(url) self.assertEqual(r.status_code, 200) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), [ { 'id': 1, 'foo': True, 'bar': 'Python will save the world.', } ]) r = requests.patch(url + '/1', data=json.dumps({ 'bar': "Python will save the world. I don't know how. But it will." })) self.assertEqual(r.status_code, 200) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), { 'id': 1, 'foo': True, 'bar': "Python will save the world. I don't know how. But it will.", # noqa }) # missing foo field -> 400 r = requests.put(url + '/1', data=json.dumps({ 'bar': "Python will save the world. I don't know how. But it will." })) self.assertEqual(r.status_code, 400) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), {'error': 'Bad Request'}) r = requests.put(url + '/1', data=json.dumps({ 'foo': False, 'bar': "Python will save the world. I don't know how. But it will." })) self.assertEqual(r.status_code, 200) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), { 'id': 1, 'foo': False, 'bar': "Python will save the world. I don't know how. But it will.", # noqa }) r = requests.get(url + '/1') self.assertEqual(r.status_code, 200) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), { 'id': 1, 'foo': False, 'bar': "Python will save the world. I don't know how. But it will.", # noqa }) r = requests.delete(url + '/1') self.assertEqual(r.status_code, 204) self.assertEqual(r.headers, {'content-type': 'text/plain'}) self.assertEqual(r.content, b'') r = requests.get(url + '/1') self.assertEqual(r.status_code, 404) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), { 'error': 'Not Found' }) def test_rest_mock_with_uuid(self): url = 'http://my_fake_service/api/v2' update_rest_rules(rest_rules) self.assertTrue(start_http_mock()) r = requests.get(url + '/{0}'.format(uuid.uuid4())) self.assertEqual(r.status_code, 404) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), {'error': 'Not Found'}) r = requests.post(url, data=json.dumps({'foo': 'bar'}), headers=CONTENTTYPE_JSON) self.assertEqual(r.status_code, 201) self.assertEqual(r.headers, {'content-type': 'application/json'}) data = r.json() _uuid = data.get('uuid') self.assertTrue(uuid.UUID(_uuid)) self.assertEqual(data, { 'uuid': _uuid, 'foo': 'bar', }) r = requests.get(url + '/' + _uuid) self.assertEqual(r.status_code, 200) self.assertEqual(r.headers, {'content-type': 'application/json'}) self.assertEqual(r.json(), { 'uuid': _uuid, 'foo': 'bar', }) def test_validators(self): url = 'http://my_fake_service/api' update_rest_rules(rest_rules_with_validators) self.assertTrue(start_http_mock()) r = requests.post(url, data=json.dumps({}), headers=CONTENTTYPE_JSON) self.assertEqual(r.status_code, 400) r = requests.post(url, data=json.dumps({ 'foo': 'bar', }), headers=CONTENTTYPE_JSON) self.assertEqual(r.status_code, 201) r = requests.post(url, data=json.dumps({ 'foo': 'bar', }), headers=CONTENTTYPE_JSON) self.assertEqual(r.status_code, 409) def test_update_rules_with_another_body_arg(self): update_rest_rules([ { 'content': b'Coincoin Content!', 'method': 'GET', 'url': r'^http://my_fake_service', } ]) self.assertTrue(start_http_mock()) r = requests.get('http://my_fake_service') self.assertEqual(r.content, b'Coincoin Content!') mock-services-0.3/tox.ini000066400000000000000000000003131277770255200154540ustar00rootroot00000000000000[tox] envlist = py{27,34,35},flake8 [testenv] usedevelop = True commands = python -m unittest discover [testenv:flake8] usedevelop = False deps = flake8==3.0.2 commands = flake8 mock_services