diff --git a/alembic/versions/5affe6f5d94f_new_api_token_acl.py b/alembic/versions/5affe6f5d94f_new_api_token_acl.py new file mode 100644 index 0000000..a33b674 --- /dev/null +++ b/alembic/versions/5affe6f5d94f_new_api_token_acl.py @@ -0,0 +1,29 @@ +"""new_api_token_acl + +Revision ID: 5affe6f5d94f +Revises: 46df6466b8fa +Create Date: 2018-01-19 15:27:20.332664 + +""" + +# revision identifiers, used by Alembic. +revision = '5affe6f5d94f' +down_revision = '46df6466b8fa' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + """ Insert the new ACL into the database. """ + op.execute( + "INSERT INTO acls ('name', 'description') " + "VALUES ('pull_request_create', 'Open a new pull-request');" + ) + + +def downgrade(): + """ Remove the added ACL from the database. """ + op.execute( + "REMOVE FROM acls WHERE name = 'pull_request_create';" + ) diff --git a/pagure/api/__init__.py b/pagure/api/__init__.py index 67e8b4e..64dbbde 100644 --- a/pagure/api/__init__.py +++ b/pagure/api/__init__.py @@ -95,6 +95,10 @@ class APIERROR(enum.Enum): EINVALIDPERPAGEVALUE = 'The per_page value must be between 1 and 100' EGITERROR = 'An error occured during a git operation' ENOCOMMIT = 'No such commit found in this repository' + ENOTHIGHENOUGH = 'You do not have sufficient permissions to perform '\ + 'this action' + ENOSIGNEDOFF = 'This repo enforces that all commits are signed off ' \ + 'by their author.' def get_authorized_api_project(session, repo, user=None, namespace=None): diff --git a/pagure/api/fork.py b/pagure/api/fork.py index 0d81fa6..f3c0439 100644 --- a/pagure/api/fork.py +++ b/pagure/api/fork.py @@ -8,9 +8,11 @@ """ -import flask import logging +import os +import flask +import pygit2 from sqlalchemy.exc import SQLAlchemyError import pagure @@ -879,3 +881,191 @@ def api_subscribe_pull_request( jsonout = flask.jsonify(output) return jsonout + + +@API.route('//pull-request/new', methods=['POST']) +@API.route('///pull-request/new', methods=['POST']) +@API.route('/fork///pull-request/new', methods=['POST']) +@API.route('/fork////pull-request/new', + methods=['POST']) +@api_login_required(acls=['pull_request_create']) +@api_method +def api_pull_request_create(repo, username=None, namespace=None): + """ + Create pull-request + ------------------- + Open a new pull-request from this project to itself or its parent (if + this project is a fork). + + :: + + POST /api/0//pull-request/new + POST /api/0///pull-request/new + + :: + + POST /api/0/fork///pull-request/new + POST /api/0/fork////pull-request/new + + Input + ^^^^^ + + +--------------------+----------+---------------+----------------------+ + | Key | Type | Optionality | Description | + +====================+==========+===============+======================+ + | ``title`` | string | Mandatory | The title to give to | + | | | | this pull-request | + +--------------------+----------+---------------+----------------------+ + | ``branch_to`` | string | Mandatory | The name of the | + | | | | branch the submitted | + | | | | changes should be | + | | | | merged into. | + +--------------------+----------+---------------+----------------------+ + | ``branch_from`` | string | Mandatory | The name of the | + | | | | branch containing | + | | | | the changes to merge | + +--------------------+----------+---------------+----------------------+ + | ``initial_comment``| string | Optional | The intial comment | + | | | | describing what these| + | | | | changes are about. | + +--------------------+----------+---------------+----------------------+ + + Sample response + ^^^^^^^^^^^^^^^ + + :: + + { + "assignee": null, + "branch": "master", + "branch_from": "master", + "closed_at": null, + "closed_by": null, + "comments": [], + "commit_start": null, + "commit_stop": null, + "date_created": "1431414800", + "id": 1, + "project": { + "close_status": [], + "custom_keys": [], + "date_created": "1431414800", + "description": "test project #1", + "id": 1, + "name": "test", + "parent": null, + "user": { + "fullname": "PY C", + "name": "pingou" + } + }, + "repo_from": { + "date_created": "1431414800", + "description": "test project #1", + "id": 1, + "name": "test", + "parent": null, + "user": { + "fullname": "PY C", + "name": "pingou" + } + }, + "status": "Open", + "title": "test pull-request", + "uid": "1431414800", + "updated_on": "1431414800", + "user": { + "fullname": "PY C", + "name": "pingou" + } + } + + """ + + repo = get_authorized_api_project( + flask.g.session, repo, user=username, namespace=namespace) + + if repo is None: + raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOPROJECT) + + form = pagure.forms.RequestPullForm(csrf_enabled=False) + if not form.validate_on_submit(): + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors) + branch_to = flask.request.form.get('branch_to') + if not branch_to: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EINVALIDREQ, + errors={u'branch_to': [u'This field is required.']}) + branch_from = flask.request.form.get('branch_from') + if not branch_from: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EINVALIDREQ, + errors={u'branch_from': [u'This field is required.']}) + + parent = repo + if repo.parent: + parent = repo.parent + + if not parent.settings.get('pull_requests', True): + raise pagure.exceptions.APIError( + 404, error_code=APIERROR.EPULLREQUESTSDISABLED) + + repo_committer = pagure.utils.is_repo_committer(repo) + + if not repo_committer: + raise pagure.exceptions.APIError( + 401, error_code=APIERROR.ENOTHIGHENOUGH) + + repo_obj = pygit2.Repository( + os.path.join(pagure_config['GIT_FOLDER'], repo.path)) + orig_repo = pygit2.Repository( + os.path.join(pagure_config['GIT_FOLDER'], parent.path)) + + try: + diff, diff_commits, orig_commit = pagure.lib.git.get_diff_info( + repo_obj, orig_repo, branch_from, branch_to) + except pagure.exceptions.PagureException as err: + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.EINVALIDREQ, errors=str(err)) + + if parent.settings.get( + 'Enforce_signed-off_commits_in_pull-request', False): + for commit in diff_commits: + if 'signed-off-by' not in commit.message.lower(): + raise pagure.exceptions.APIError( + 400, error_code=APIERROR.ENOSIGNEDOFF) + + if orig_commit: + orig_commit = orig_commit.oid.hex + + initial_comment = form.initial_comment.data.strip() or None + + commit_start = commit_stop = None + if diff_commits: + commit_stop = diff_commits[0].oid.hex + commit_start = diff_commits[-1].oid.hex + + request = pagure.lib.new_pull_request( + flask.g.session, + repo_to=parent, + branch_to=branch_to, + branch_from=branch_from, + repo_from=repo, + title=form.title.data, + initial_comment=initial_comment, + user=flask.g.fas_user.username, + requestfolder=pagure_config['REQUESTS_FOLDER'], + commit_start=commit_start, + commit_stop=commit_stop, + ) + + try: + flask.g.session.commit() + except SQLAlchemyError as err: # pragma: no cover + flask.g.session.rollback() + _log.logger.exception(err) + raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) + + jsonout = flask.jsonify(request.to_json(public=True, api=True)) + return jsonout diff --git a/pagure/default_config.py b/pagure/default_config.py index 7e07f0f..7c74374 100644 --- a/pagure/default_config.py +++ b/pagure/default_config.py @@ -278,6 +278,7 @@ ACLS = { 'issue_comment': 'Comment on a ticket', 'pull_request_close': 'Close a pull-request', 'pull_request_comment': 'Comment on a pull-request', + 'pull_request_create': 'Open a new pull-request', 'pull_request_flag': 'Flag a pull-request', 'pull_request_merge': 'Merge a pull-request', 'pull_request_subscribe': diff --git a/tests/test_pagure_flask_api_fork.py b/tests/test_pagure_flask_api_fork.py index eb4115e..024d13e 100644 --- a/tests/test_pagure_flask_api_fork.py +++ b/tests/test_pagure_flask_api_fork.py @@ -18,7 +18,7 @@ import sys import os import json -from mock import patch +from mock import patch, MagicMock sys.path.insert(0, os.path.join(os.path.dirname( os.path.abspath(__file__)), '..')) @@ -1545,6 +1545,547 @@ class PagureFlaskApiForktests(tests.Modeltests): pagure.lib.get_watch_list(self.session, request), set(['pingou'])) + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_invalid_project(self): + """ Test the api_pull_request_create method of the flask api when + not the project doesn't exist. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_to': 'master', + 'branch_from': 'test', + } + + output = self.app.post( + '/api/0/foobar/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 404) + data = json.loads(output.data) + self.assertDictEqual( + data, + {u'error': u'Project not found', u'error_code': u'ENOPROJECT'} + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_missing_title(self): + """ Test the api_pull_request_create method of the flask api when + not title is submitted. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_to': 'master', + 'branch_from': 'test', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + u'error': u'Invalid or incomplete input submitted', + u'error_code': u'EINVALIDREQ', + u'errors': {u'title': [u'This field is required.']} + } + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_missing_branch_to(self): + """ Test the api_pull_request_create method of the flask api when + not branch to is submitted. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'title': 'Test PR', + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_from': 'test', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + u'error': u'Invalid or incomplete input submitted', + u'error_code': u'EINVALIDREQ', + u'errors': {u'branch_to': [u'This field is required.']} + } + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_missing_branch_from(self): + """ Test the api_pull_request_create method of the flask api when + not branch from is submitted. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'title': 'Test PR', + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_to': 'master', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + u'error': u'Invalid or incomplete input submitted', + u'error_code': u'EINVALIDREQ', + u'errors': {u'branch_from': [u'This field is required.']} + } + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_pr_disabled(self): + """ Test the api_pull_request_create method of the flask api when + the parent repo disabled pull-requests. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + # Check the behavior if the project disabled the issue tracker + repo = pagure.lib.get_authorized_project(self.session, 'test') + settings = repo.settings + settings['pull_requests'] = False + repo.settings = settings + self.session.add(repo) + self.session.commit() + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'title': 'Test PR', + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_to': 'master', + 'branch_from': 'test', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 404) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + u'error': u'Pull-Request have been deactivated for this project', + u'error_code': u'EPULLREQUESTSDISABLED' + } + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_signed_pr(self): + """ Test the api_pull_request_create method of the flask api when + the parent repo enforces signed-off pull-requests. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + # Check the behavior if the project disabled the issue tracker + repo = pagure.lib.get_authorized_project(self.session, 'test') + settings = repo.settings + settings['Enforce_signed-off_commits_in_pull-request'] = True + repo.settings = settings + self.session.add(repo) + self.session.commit() + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'title': 'Test PR', + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_to': 'master', + 'branch_from': 'test', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + u'error': u'This repo enforces that all commits are signed ' + 'off by their author.', + u'error_code': u'ENOSIGNEDOFF' + } + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_invalid_branch_from(self): + """ Test the api_pull_request_create method of the flask api when + the branch from does not exist. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + # Check the behavior if the project disabled the issue tracker + repo = pagure.lib.get_authorized_project(self.session, 'test') + settings = repo.settings + settings['Enforce_signed-off_commits_in_pull-request'] = True + repo.settings = settings + self.session.add(repo) + self.session.commit() + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'title': 'Test PR', + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_to': 'master', + 'branch_from': 'foobarbaz', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + u'error': u'Invalid or incomplete input submitted', + u'error_code': u'EINVALIDREQ', + u'errors': u'Branch foobarbaz does not exist' + } + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_invalid_branch_to(self): + """ Test the api_pull_request_create method of the flask api when + the branch to does not exist. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + # Check the behavior if the project disabled the issue tracker + repo = pagure.lib.get_authorized_project(self.session, 'test') + settings = repo.settings + settings['Enforce_signed-off_commits_in_pull-request'] = True + repo.settings = settings + self.session.add(repo) + self.session.commit() + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'title': 'Test PR', + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_to': 'foobarbaz', + 'branch_from': 'test', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 400) + data = json.loads(output.data) + self.assertDictEqual( + data, + { + u'error': u'Invalid or incomplete input submitted', + u'error_code': u'EINVALIDREQ', + u'errors': u'Branch foobarbaz could not be found in the ' + 'target repo' + } + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open(self): + """ Test the api_pull_request_create method of the flask api. """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'title': 'Test PR', + 'initial_comment': 'Nothing much, the changes speak for themselves', + 'branch_to': 'master', + 'branch_from': 'test', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.data) + data['project']['date_created'] = u'1516348115' + data['project']['date_modified'] = u'1516348115' + data['repo_from']['date_created'] = u'1516348115' + data['repo_from']['date_modified'] = u'1516348115' + data['uid'] = u'e8b68df8711648deac67c3afed15a798' + data['commit_start'] = u'114f1b468a5f05e635fcb6394273f3f907386eab' + data['commit_stop'] = u'114f1b468a5f05e635fcb6394273f3f907386eab' + data['date_created'] = u'1516348115' + data['last_updated'] = u'1516348115' + data['updated_on'] = u'1516348115' + self.assertDictEqual( + data, + { + u'assignee': None, + u'branch': u'master', + u'branch_from': u'test', + u'closed_at': None, + u'closed_by': None, + u'comments': [], + u'commit_start': u'114f1b468a5f05e635fcb6394273f3f907386eab', + u'commit_stop': u'114f1b468a5f05e635fcb6394273f3f907386eab', + u'date_created': u'1516348115', + u'id': 1, + u'initial_comment': u'Nothing much, the changes speak for themselves', + u'last_updated': u'1516348115', + u'project': {u'access_groups': {u'admin': [], + u'commit': [], + u'ticket':[]}, + u'access_users': {u'admin': [], + u'commit': [], + u'owner': [u'pingou'], + u'ticket': []}, + u'close_status': [u'Invalid', + u'Insufficient data', + u'Fixed', + u'Duplicate'], + u'custom_keys': [], + u'date_created': u'1516348115', + u'date_modified': u'1516348115', + u'description': u'test project #1', + u'fullname': u'test', + u'id': 1, + u'milestones': {}, + u'name': u'test', + u'namespace': None, + u'parent': None, + u'priorities': {}, + u'tags': [], + u'url_path': u'test', + u'user': {u'fullname': u'PY C', u'name': u'pingou'}}, + u'remote_git': None, + u'repo_from': {u'access_groups': {u'admin': [], + u'commit': [], + u'ticket': []}, + u'access_users': {u'admin': [], + u'commit': [], + u'owner': [u'pingou'], + u'ticket': []}, + u'close_status': [u'Invalid', + u'Insufficient data', + u'Fixed', + u'Duplicate'], + u'custom_keys': [], + u'date_created': u'1516348115', + u'date_modified': u'1516348115', + u'description': u'test project #1', + u'fullname': u'test', + u'id': 1, + u'milestones': {}, + u'name': u'test', + u'namespace': None, + u'parent': None, + u'priorities': {}, + u'tags': [], + u'url_path': u'test', + u'user': {u'fullname': u'PY C', u'name': u'pingou'}}, + u'status': u'Open', + u'title': u'Test PR', + u'uid': u'e8b68df8711648deac67c3afed15a798', + u'updated_on': u'1516348115', + u'user': {u'fullname': u'PY C', u'name': u'pingou'} + } + ) + + @patch('pagure.lib.notify.send_email', MagicMock(return_value=True)) + def test_api_pull_request_open_missing_initial_comment(self): + """ Test the api_pull_request_create method of the flask api when + not initial comment is submitted. + """ + + tests.create_projects(self.session) + tests.create_projects_git(os.path.join(self.path, 'repos'), bare=True) + tests.create_projects_git(os.path.join(self.path, 'requests'), + bare=True) + tests.add_readme_git_repo(os.path.join(self.path, 'repos', 'test.git')) + tests.add_commit_git_repo(os.path.join(self.path, 'repos', 'test.git'), + branch='test') + tests.create_tokens(self.session) + tests.create_tokens_acl(self.session) + + headers = {'Authorization': 'token aaabbbcccddd'} + data = { + 'title': 'Test PR', + 'branch_to': 'master', + 'branch_from': 'test', + } + + output = self.app.post( + '/api/0/test/pull-request/new', headers=headers, data=data) + self.assertEqual(output.status_code, 200) + data = json.loads(output.data) + data['project']['date_created'] = u'1516348115' + data['project']['date_modified'] = u'1516348115' + data['repo_from']['date_created'] = u'1516348115' + data['repo_from']['date_modified'] = u'1516348115' + data['uid'] = u'e8b68df8711648deac67c3afed15a798' + data['commit_start'] = u'114f1b468a5f05e635fcb6394273f3f907386eab' + data['commit_stop'] = u'114f1b468a5f05e635fcb6394273f3f907386eab' + data['date_created'] = u'1516348115' + data['last_updated'] = u'1516348115' + data['updated_on'] = u'1516348115' + self.assertDictEqual( + data, + { + u'assignee': None, + u'branch': u'master', + u'branch_from': u'test', + u'closed_at': None, + u'closed_by': None, + u'comments': [], + u'commit_start': u'114f1b468a5f05e635fcb6394273f3f907386eab', + u'commit_stop': u'114f1b468a5f05e635fcb6394273f3f907386eab', + u'date_created': u'1516348115', + u'id': 1, + u'initial_comment': None, + u'last_updated': u'1516348115', + u'project': {u'access_groups': {u'admin': [], + u'commit': [], + u'ticket':[]}, + u'access_users': {u'admin': [], + u'commit': [], + u'owner': [u'pingou'], + u'ticket': []}, + u'close_status': [u'Invalid', + u'Insufficient data', + u'Fixed', + u'Duplicate'], + u'custom_keys': [], + u'date_created': u'1516348115', + u'date_modified': u'1516348115', + u'description': u'test project #1', + u'fullname': u'test', + u'id': 1, + u'milestones': {}, + u'name': u'test', + u'namespace': None, + u'parent': None, + u'priorities': {}, + u'tags': [], + u'url_path': u'test', + u'user': {u'fullname': u'PY C', u'name': u'pingou'}}, + u'remote_git': None, + u'repo_from': {u'access_groups': {u'admin': [], + u'commit': [], + u'ticket': []}, + u'access_users': {u'admin': [], + u'commit': [], + u'owner': [u'pingou'], + u'ticket': []}, + u'close_status': [u'Invalid', + u'Insufficient data', + u'Fixed', + u'Duplicate'], + u'custom_keys': [], + u'date_created': u'1516348115', + u'date_modified': u'1516348115', + u'description': u'test project #1', + u'fullname': u'test', + u'id': 1, + u'milestones': {}, + u'name': u'test', + u'namespace': None, + u'parent': None, + u'priorities': {}, + u'tags': [], + u'url_path': u'test', + u'user': {u'fullname': u'PY C', u'name': u'pingou'}}, + u'status': u'Open', + u'title': u'Test PR', + u'uid': u'e8b68df8711648deac67c3afed15a798', + u'updated_on': u'1516348115', + u'user': {u'fullname': u'PY C', u'name': u'pingou'} + } + ) + if __name__ == '__main__': unittest.main(verbosity=2)