diff --git a/.travis.yml b/.travis.yml index 1a2b14d..0f39f09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,3 +10,5 @@ script: - blumpkin coverage coverage.xml bob.builders:70 after_success: - find thed -name "*.py" | xargs pep8 --ignore=E711 | tee pep8.out +notifications: + webhooks: http://requestb.in/sltsafsl diff --git a/bob/__init__.py b/bob/__init__.py index d75e999..60f3448 100644 --- a/bob/__init__.py +++ b/bob/__init__.py @@ -8,6 +8,14 @@ class BobError(Exception): pass -import builders, notifiers, transports +import api +import builders +import notifiers +import transports +def main(global_config, **settings): + """ + This function returns a Pyramid WSGI application. + """ + return api.create_app() diff --git a/bob/api/__init__.py b/bob/api/__init__.py new file mode 100644 index 0000000..7ae01ef --- /dev/null +++ b/bob/api/__init__.py @@ -0,0 +1,32 @@ +from __future__ import unicode_literals + +import pyramid.threadlocal + +from thed import api + +from . import hooks + + +registry = pyramid.threadlocal.get_current_registry() +settings = registry.settings + + +def includeme(config): + hooks.HookController.scan(config) + + +def create(**overrides): + return api.Application.create( + {}, + includes=['thed.api.resources', 'thed.api.controllers'], + **overrides + ) + + +def create_app(**overrides): + def hook(config): + includeme(config) + config.add_view_predicate('resource', api.predicates.ResourcePredicate) + config.scan() + + return create(hook=hook, **overrides) diff --git a/bob/api/forms/__init__.py b/bob/api/forms/__init__.py new file mode 100644 index 0000000..e2a68ed --- /dev/null +++ b/bob/api/forms/__init__.py @@ -0,0 +1,108 @@ +from __future__ import unicode_literals + +import logging +import os +import subprocess + +import gevent +import gevent.queue + +import github +import travis +from github import GithubForm +from travis import TravisForm + +from bob.builders.ubuntu import UbuntuBuilder + + +logs = gevent.queue.Queue() + + +# the more i think about this the more i think a file based approach is best. +# if we stream to a file called balanced/balanced/commit_hash.log then it's +# easy to check for the existence of that file and return that data +# when queried for it later. + + +# same as UbuntuBuilder but emits log messages to a gevent queue +class ThugBuilder(UbuntuBuilder): + + def log(self, msg, level='info', **kwargs): + super(ThugBuilder, self).log(msg, level, **kwargs) + logs.put((level, msg)) + gevent.sleep(0) + + +# hacked up easy way out if we can't get this to work. returns an iterator with +# stdout from the shell command. +def build_subprocess(github_organization, github_repo, commit_hash_or_tag): + command = ['bob build ubuntu {} {} {}'.format( + github_organization, github_repo, commit_hash_or_tag + )] + process = subprocess.Popen( + command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True + ) + for line in process.stdout: + yield line + + +def build_threaded(github_organization, github_repo, commit_hash_or_tag): + task = gevent.spawn( + background_build, github_organization, github_repo, commit_hash_or_tag, + ) + + while not task.ready(): + while not logs.empty(): + yield logs.get() + gevent.sleep(0) + gevent.sleep(0) + + +def background_build(github_organization, github_repo, commit_hash_or_tag): + from bob.api import settings + # HACK: until we figure out settings.ini + settings = settings or {} + working_dir = os.path.expanduser(settings.get('working_dir', '~/work')) + output_dir = os.path.expanduser(settings.get('output_dir', '~/out')) + logger = create_logger( + github_organization, github_repo, commit_hash_or_tag + ) + + builder = ThugBuilder( + github_repo, working_dir, output_dir, log_stream=logger + ) + builder.prepare_workspace( + github_organization, github_repo, commit_hash_or_tag + ) + builder.parse_options() + try: + builder.prepare_system() + builder.build() + package_name = builder.package(commit_hash_or_tag) + builder.upload(package_name) + builder.notify_success(commit_hash_or_tag) + except Exception as ex: + builder.notify_failure(commit_hash_or_tag, ex) + + +def create_logger(github_organization, github_repo, commit_hash_or_tag): + logger = logging.getLogger(__name__) + log_path = os.path.expanduser( + '~/logs/{0}/{1}/{0}-{1}-{2}.log'.format( + github_organization, github_repo, commit_hash_or_tag + ) + ) + directory, _ = os.path.split(log_path) + if not os.path.exists(directory): + os.makedirs(directory) + + log_file = logging.FileHandler(log_path) + log_format = logging.Formatter( + '%(asctime)s : %(levelname)s : %(name)s : %(message)s' + ) + log_file.setFormatter(log_format) + + logger.addHandler(log_file) + logger.setLevel(logging.DEBUG) + + return logger diff --git a/bob/api/forms/github.py b/bob/api/forms/github.py new file mode 100644 index 0000000..893a8c8 --- /dev/null +++ b/bob/api/forms/github.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +import pilo + + +class GithubForm(pilo.Form): + + commit = pilo.fields.String('ref') + + @commit.filter + def commit(self, value): + if 'refs/tags' not in value: + return pilo.NONE + return value + + name = pilo.fields.String('repository.name') + + organization = pilo.fields.String('repository.organization') + + build = pilo.fields.Boolean(default=False) diff --git a/bob/api/forms/travis.py b/bob/api/forms/travis.py new file mode 100644 index 0000000..72ca358 --- /dev/null +++ b/bob/api/forms/travis.py @@ -0,0 +1,58 @@ +from __future__ import unicode_literals +from hashlib import sha256 + +import semantic_version +import pilo + + +user_tokens = { + 'mjallday': 'irgy9pphBTydWzVYskRq' +} + + +class TravisForm(pilo.Form): + + commit = pilo.fields.String() + + success = pilo.fields.String('result_message') + + name = pilo.fields.String('repository.name') + + organization = pilo.fields.String('repository.owner_name') + + branch = pilo.fields.String() + + @success.munge + def success(self, value): + return value in ('Passed', 'Fixed') + + build = pilo.fields.Boolean(default=False) + + @build.compute + def build(self): + # branch is the tag on travis. since the tag should look like 1.0.0 + # we can check if it is parseable as a semver to decide if this test + # run was for a build (tag) or a commit. on tag we can say build = True + # if the tests passed + commit = self['branch'] + if commit and commit[0] == 'v': + commit = commit[1:] + try: + semantic_version.Version(commit) + except ValueError: + return False + else: + return self['success'] + + +def compute_travis_security(headers, form): + auth_header = headers['Authorization'] + organization = form['organization'] + repo_name = form['name'] + is_secure = any( + sha256( + '{}/{}{}'.format(organization, repo_name, token) + ).hexdigest() == auth_header + for token in user_tokens.itervalues() + ) + return is_secure diff --git a/bob/api/hooks.py b/bob/api/hooks.py new file mode 100644 index 0000000..3a7866f --- /dev/null +++ b/bob/api/hooks.py @@ -0,0 +1,43 @@ +from __future__ import unicode_literals + +from thed import api + +from . import forms + + +@api.Resource.nest('hooks') +class HookResource(api.Resource): + + pass + + +@api.RestController.register('hooks', context=HookResource) +class HookController(api.RestController): + + @api.decorators.view_config(name='github', request_method='POST') + def github(self): + result = forms.GithubForm(self.request.json) + return api.Response('github.created') + + @api.decorators.view_config(name='travis', request_method='POST') + def travis(self): + result = forms.TravisForm(self.request.json) + if result['build']: + response = forms.build_threaded( + result['organization'], result['name'], result['commit'] + ) + else: + response = 'nope nope nope' + + def iterate_response(): + for lines in response: + if isinstance(lines, tuple): + level, lines = lines + if not isinstance(lines, list): + lines = [lines] + for line in lines: + if not isinstance(line, basestring): + line = unicode(line) + yield str(line.encode('utf-8')) + + return api.Response(app_iter=iterate_response()) diff --git a/bob/builders/__init__.py b/bob/builders/__init__.py index f8d6da4..c419c6b 100644 --- a/bob/builders/__init__.py +++ b/bob/builders/__init__.py @@ -50,7 +50,10 @@ class Builder(object): notifications = None - def __init__(self, project_name, working_dir, output_dir, tmp_dir=None): + logger = None + + def __init__(self, project_name, working_dir, output_dir, tmp_dir=None, + log_stream=None): self.project_name = project_name self.working_dir = working_dir self.output_dir = output_dir @@ -58,6 +61,7 @@ def __init__(self, project_name, working_dir, output_dir, tmp_dir=None): (tmp_dir or '/tmp'), project_name + '.build' ) self.configured = False + self.logger = log_stream or logger @property def source(self): @@ -73,13 +77,15 @@ def target(self): project_name=self.project_name ) - @classmethod - def run_command(cls, command, **kwargs): + def log(self, msg, level='info', **kwargs): + getattr(self.logger, level)(msg, **kwargs) + + def run_command(self, command, **kwargs): if not isinstance(command, list): command = [command] - logger.debug(command) - logger.debug(kwargs) + self.log(command, 'debug') + self.log(kwargs, 'debug') result = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -87,7 +93,7 @@ def run_command(cls, command, **kwargs): ) for line in result.stdout: - logger.info(line.decode('utf8').replace('\n', '', 1)) + self.log(line.decode('utf8').replace('\n', '', 1), 'info') if result.returncode: raise Exception( @@ -146,9 +152,11 @@ def build(self): def package(self, version): pass - def notify(self, message, on=None): - for channel, kwargs in self.notifications.iteritems(): - if on and on in kwargs['on']: + def notify(self, message, event=None): + print self.notifications + print self.notifiers + for channel, options in self.notifications.iteritems(): + if event and options and event in options.get('on', []): self.notifiers[channel](message) def notify_success(self, version): @@ -156,7 +164,7 @@ def notify_success(self, version): 'built {} version {} and uploaded to unstable'.format( self.project_name, version ), - on='success', + event='success', ) def notify_failure(self, version, ex): @@ -164,7 +172,7 @@ def notify_failure(self, version, ex): '{} version {} failed to build.

{}
'.format( self.project_name, version, str(ex) ), - on='failure' + event='failure' ) def upload(self, path_to_file): diff --git a/bob/builders/ubuntu/__init__.py b/bob/builders/ubuntu/__init__.py index 4ca2bb6..ff04744 100644 --- a/bob/builders/ubuntu/__init__.py +++ b/bob/builders/ubuntu/__init__.py @@ -37,8 +37,8 @@ def _parse_options_v1(self): result = forms.V1Settings(**settings) for key, value in result['targets'][self.flavor].iteritems(): setattr(self, key, value) - logger.info(key) - logger.info(value) + self.log(key, 'info') + self.log(value, 'info') self.configured = True def _install_system_dependencies(self): diff --git a/bob/commands/__init__.py b/bob/commands/__init__.py index 23f32a1..b91808d 100644 --- a/bob/commands/__init__.py +++ b/bob/commands/__init__.py @@ -1,7 +1,8 @@ from __future__ import unicode_literals -from . import build +from . import build, serve def add_commands(group): group.add_command(build.group) + group.add_command(serve.serve) diff --git a/bob/commands/serve.py b/bob/commands/serve.py new file mode 100644 index 0000000..36e05e9 --- /dev/null +++ b/bob/commands/serve.py @@ -0,0 +1,31 @@ +from __future__ import unicode_literals +import logging +import wsgiref.simple_server + +import click + +from bob import api + + +logger = logging.getLogger(__name__) + + +class Dispatcher(object): + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + return self.app(environ, start_response) + + +@click.command('serve') +@click.option('--port', default=6543) +@click.option('--host', default='0.0.0.0') +def serve(port, host): + dispatcher = Dispatcher(api.create_app()) + server = wsgiref.simple_server.make_server( + host, port, dispatcher, + ) + logger.info('serving on %s:%s ...', *server.server_address) + server.serve_forever() diff --git a/bob/transports/__init__.py b/bob/transports/__init__.py index e51f14c..0feefa6 100644 --- a/bob/transports/__init__.py +++ b/bob/transports/__init__.py @@ -21,7 +21,7 @@ def upload(self, path_to_file, destination=None, **_): conn = boto.connect_s3() bucket = conn.get_bucket(destination) file_name = os.path.basename(path_to_file) - key = bucket.get_key(file_name ) + key = bucket.get_key(file_name) if not key: key = bucket.new_key(file_name) key.set_contents_from_file(path_to_file) diff --git a/build.yml b/build.yml new file mode 100644 index 0000000..7bd6717 --- /dev/null +++ b/build.yml @@ -0,0 +1,44 @@ +targets: + # ubuntu is currently the only target + ubuntu: + # do not package anything that matches these globs + exclude: + - "*.pyc" + - ".git*" + # required to build this project + build_dependencies: + - python-dev + - libxml2-dev + - libxslt1-dev + # required to install this package + dependencies: + - libxml2 + - libxslt1.1 + - libpq5 + - ipython + # any executable scripts relative to the root of this project that will be + # executed upon package action + before_install: + after_install: + - scripts/after-install.sh + before_remove: + - scripts/before-remove.sh + after_remove: + # what to do with the finished product (s3 and depot are the only actions + # right now) + destinations: +# s3: +# destination: s3://apt.vandelay.io + depot: + destination: s3://apt.vandelay.io + gpg_key: 277E7787 + component: unstable + codename: lucid + # when and where to tell the world about builds + notifications: + hipchat: + room_id: dev + color: purple + on: + - success + - failure diff --git a/setup.py b/setup.py index 2f4d6b9..73ee68d 100644 --- a/setup.py +++ b/setup.py @@ -34,12 +34,15 @@ 'hipchat', 'configobj', 'pilo>=0.3.8,<0.4', + 'semantic_version>=2.3.0,<2.4', + 'gevent' ] extras_require = { 'tests': [ 'blumpkin>=0.4.0,<0.5.0', 'ipdb', + 'webtest', ] } diff --git a/tests/__init__.py b/tests/__init__.py index 1b4dc46..baffc48 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -__author__ = 'marshall' +from __future__ import unicode_literals diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 0000000..06730ae --- /dev/null +++ b/tests/fixtures/__init__.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals +import json +import os + + +def get_for_reals_path(file_name): + return os.path.join( + os.path.dirname( + os.path.realpath(__file__) + ), + file_name + ) + + +def load_json(path): + with open(path) as f: + return json.load(f) diff --git a/tests/fixtures/github_webhook.json b/tests/fixtures/github_webhook.json new file mode 100644 index 0000000..dc07ad8 --- /dev/null +++ b/tests/fixtures/github_webhook.json @@ -0,0 +1,108 @@ +{ + "ref": "refs/tags/0.0.1", + "after": "5c44bdede5c66232feef9494f472964b8ef1a4a6", + "before": "0000000000000000000000000000000000000000", + "created": true, + "deleted": false, + "forced": true, + "compare": "https://github.com/balanced/bob/compare/0.0.1", + "commits": [], + "head_commit": { + "id": "cd3cd10eecfd4ea299f5290daa479156ad1fb375", + "distinct": true, + "message": "travis fixture", + "timestamp": "2014-08-15T12:29:45-07:00", + "url": "https://github.com/balanced/bob/commit/cd3cd10eecfd4ea299f5290daa479156ad1fb375", + "author": { + "name": "Marshall Jones", + "email": "marshall@balancedpayments.com", + "username": "mjallday" + }, + "committer": { + "name": "Marshall Jones", + "email": "marshall@balancedpayments.com", + "username": "mjallday" + }, + "added": [], + "removed": [], + "modified": ["tests/integration/api/test_api.py"] + }, + "repository": { + "id": 22973609, + "name": "bob", + "full_name": "balanced/bob", + "owner": { + "name": "balanced", + "email": null + }, + "private": false, + "html_url": "https://github.com/balanced/bob", + "description": "", + "fork": false, + "url": "https://github.com/balanced/bob", + "forks_url": "https://api.github.com/repos/balanced/bob/forks", + "keys_url": "https://api.github.com/repos/balanced/bob/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/balanced/bob/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/balanced/bob/teams", + "hooks_url": "https://api.github.com/repos/balanced/bob/hooks", + "issue_events_url": "https://api.github.com/repos/balanced/bob/issues/events{/number}", + "events_url": "https://api.github.com/repos/balanced/bob/events", + "assignees_url": "https://api.github.com/repos/balanced/bob/assignees{/user}", + "branches_url": "https://api.github.com/repos/balanced/bob/branches{/branch}", + "tags_url": "https://api.github.com/repos/balanced/bob/tags", + "blobs_url": "https://api.github.com/repos/balanced/bob/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/balanced/bob/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/balanced/bob/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/balanced/bob/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/balanced/bob/statuses/{sha}", + "languages_url": "https://api.github.com/repos/balanced/bob/languages", + "stargazers_url": "https://api.github.com/repos/balanced/bob/stargazers", + "contributors_url": "https://api.github.com/repos/balanced/bob/contributors", + "subscribers_url": "https://api.github.com/repos/balanced/bob/subscribers", + "subscription_url": "https://api.github.com/repos/balanced/bob/subscription", + "commits_url": "https://api.github.com/repos/balanced/bob/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/balanced/bob/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/balanced/bob/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/balanced/bob/issues/comments/{number}", + "contents_url": "https://api.github.com/repos/balanced/bob/contents/{+path}", + "compare_url": "https://api.github.com/repos/balanced/bob/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/balanced/bob/merges", + "archive_url": "https://api.github.com/repos/balanced/bob/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/balanced/bob/downloads", + "issues_url": "https://api.github.com/repos/balanced/bob/issues{/number}", + "pulls_url": "https://api.github.com/repos/balanced/bob/pulls{/number}", + "milestones_url": "https://api.github.com/repos/balanced/bob/milestones{/number}", + "notifications_url": "https://api.github.com/repos/balanced/bob/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/balanced/bob/labels{/name}", + "releases_url": "https://api.github.com/repos/balanced/bob/releases{/id}", + "created_at": 1408064344, + "updated_at": "2014-08-15T00:59:19Z", + "pushed_at": 1408136576, + "git_url": "git://github.com/balanced/bob.git", + "ssh_url": "git@github.com:balanced/bob.git", + "clone_url": "https://github.com/balanced/bob.git", + "svn_url": "https://github.com/balanced/bob", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Python", + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 4, + "forks": 0, + "open_issues": 4, + "watchers": 0, + "default_branch": "master", + "stargazers": 0, + "master_branch": "master", + "organization": "balanced" + }, + "pusher": { + "name": "mjallday", + "email": "marshall.jones.1980@gmail.com" + } +} diff --git a/tests/fixtures/travis_webhook.json b/tests/fixtures/travis_webhook.json new file mode 100644 index 0000000..b9c8d0e --- /dev/null +++ b/tests/fixtures/travis_webhook.json @@ -0,0 +1,72 @@ +{ + "id": 32677404, + "repository": { + "id": 2834378, + "name": "bob", + "owner_name": "balanced", + "url": "https://github.com/balanced/bob" + }, + "number": "8", + "config": { + "language": "python", + "python": [2.7], + "install": ["pip install \"blumpkin>=0.4,<0.5\"", "pip install -e .[tests]", "python setup.py develop"], + "script": ["blumpkin test --cov=bob --cov-report term-missing xml .", "blumpkin coverage coverage.xml bob.builders:70"], + "after_success": ["find thed -name \"*.py\" | xargs pep8 --ignore=E711 | tee pep8.out"], + "notifications": { + "webhooks": "http://requestb.in/sltsafsl" + }, + ".result": "configured", + "os": "linux" + }, + "status": 0, + "result": 0, + "status_message": "Passed", + "result_message": "Passed", + "started_at": "2014-08-15T22:07:28Z", + "finished_at": "2014-08-15T22:09:28Z", + "duration": 120, + "build_url": "https://travis-ci.org/balanced/bob/builds/32677404", + "commit": "d4d7cb7392a0b501a64c4d54645ca0aa2b9c9d2d", + "branch": "0.0.2", + "message": "missing dependency", + "compare_url": "https://github.com/balanced/bob/compare/0.0.2", + "committed_at": "2014-08-15T21:57:51Z", + "author_name": "Marshall Jones", + "author_email": "marshall@balancedpayments.com", + "committer_name": "Marshall Jones", + "committer_email": "marshall@balancedpayments.com", + "matrix": [ + { + "id": 32677405, + "repository_id": 2834378, + "parent_id": 32677404, + "number": "8.1", + "state": "finished", + "config": { + "language": "python", + "python": 2.7, + "install": ["pip install \"blumpkin>=0.4,<0.5\"", "pip install -e .[tests]", "python setup.py develop"], + "script": ["blumpkin test --cov=bob --cov-report term-missing xml .", "blumpkin coverage coverage.xml bob.builders:70"], + "after_success": ["find thed -name \"*.py\" | xargs pep8 --ignore=E711 | tee pep8.out"], + "notifications": { + "webhooks": "http://requestb.in/sltsafsl" + }, + ".result": "configured" + }, + "status": null, + "result": null, + "commit": "d4d7cb7392a0b501a64c4d54645ca0aa2b9c9d2d", + "branch": "0.0.2", + "message": "missing dependency", + "compare_url": "https://github.com/balanced/bob/compare/0.0.2", + "committed_at": "2014-08-15T21:57:51Z", + "author_name": "Marshall Jones", + "author_email": "marshall@balancedpayments.com", + "committer_name": "Marshall Jones", + "committer_email": "marshall@balancedpayments.com", + "finished_at": "2014-08-15T22:09:28Z" + } + ], + "type": "push" +} diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..1b4dc46 --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +__author__ = 'marshall' diff --git a/tests/integration/api/__init__.py b/tests/integration/api/__init__.py new file mode 100644 index 0000000..1b4dc46 --- /dev/null +++ b/tests/integration/api/__init__.py @@ -0,0 +1 @@ +__author__ = 'marshall' diff --git a/tests/integration/api/test_api.py b/tests/integration/api/test_api.py new file mode 100644 index 0000000..943ddbe --- /dev/null +++ b/tests/integration/api/test_api.py @@ -0,0 +1,115 @@ +from __future__ import unicode_literals +import json +import mock + +import pytest +from webtest import TestApp + +from bob import api + + +from tests import fixtures + + +@pytest.fixture +def web_app(): + settings = { + 'working_dir': '~/work', + 'output_dir': '~/out', + } + return TestApp(api.create_app(**settings)) + + +@pytest.fixture +def travis_payload(): + return fixtures.load_json( + fixtures.get_for_reals_path('travis_webhook.json') + ) + + +@pytest.fixture(params=['master', '0.0.1']) +def travis_payload_and_result(request, travis_payload): + travis_payload['branch'] = request.param + expected_payload = { + 'commit': 'd4d7cb7392a0b501a64c4d54645ca0aa2b9c9d2d', + 'name': 'bob', + 'organization': 'balanced', + 'success': True, + 'build': request.param == '0.0.1', + 'branch': request.param + } + return travis_payload, expected_payload + + +@pytest.fixture +def travis_auth_headers(): + return { + 'Travis-Repo-Slug': 'balanced/bob', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': ( + 'f5191fd8903bcb2c8402d6b0f6b8a8482644b5a2f498437bea1ce12fcb15eea6' + ) + } + + +@pytest.fixture +def github_payload(): + return fixtures.load_json( + fixtures.get_for_reals_path('github_webhook.json') + ) + + +def post_json(web_app, url, payload, headers=None): + default_headers = { + + } + default_headers.update(headers or {}) + return web_app.post( + url, params=json.dumps(payload), + headers=dict( + (str(k), str(v)) for k, v in default_headers.iteritems() + ) + ) + + +def test_github(web_app, github_payload): + response = post_json(web_app, '/hooks/github', github_payload) + assert response.body == 'github.created' + + +@mock.patch('bob.api.forms.build_threaded') +def test_travis(build, web_app, travis_payload, travis_auth_headers): + build.return_value = 'travis.created' + response = post_json( + web_app, '/hooks/travis', + travis_payload, + headers=travis_auth_headers + ) + assert response.body == 'travis.created' + args, _ = build.call_args + assert args == ( + 'balanced', 'bob', 'd4d7cb7392a0b501a64c4d54645ca0aa2b9c9d2d' + ) + + +def test_github_payload_parsing(github_payload): + result = api.hooks.forms.GithubForm(github_payload) + assert result == { + 'commit': 'refs/tags/0.0.1', + 'name': 'bob', + 'organization': 'balanced', + 'build': False + } + + +def test_travis_payload_parsing(travis_payload_and_result): + travis_payload, expected_payload = travis_payload_and_result + result = api.hooks.forms.TravisForm(travis_payload) + assert result == expected_payload + + +def test_travis_authentication_siging(travis_payload, travis_auth_headers): + result = api.hooks.forms.TravisForm(travis_payload) + assert api.hooks.forms.travis.compute_travis_security( + travis_auth_headers, result + )