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 + )