diff --git a/CHANGELOG.md b/CHANGELOG.md index be370f50..b2890f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ - Gzip export/import support. - Export performance improvements. - Add filter by email domain registration and invitation by setting. +- Third party integrations: + - Included gogs as builtin integration. + - Improve messages generated on webhooks input. + - Add mentions support in commit messages. + - Cleanup hooks code. ### Misc - [API] Improve performance of some calls over list. diff --git a/settings/common.py b/settings/common.py index 66fbb67f..333d310a 100644 --- a/settings/common.py +++ b/settings/common.py @@ -313,6 +313,7 @@ INSTALLED_APPS = [ "taiga.hooks.github", "taiga.hooks.gitlab", "taiga.hooks.bitbucket", + "taiga.hooks.gogs", "taiga.webhooks", "djmail", @@ -506,6 +507,7 @@ PROJECT_MODULES_CONFIGURATORS = { "github": "taiga.hooks.github.services.get_or_generate_config", "gitlab": "taiga.hooks.gitlab.services.get_or_generate_config", "bitbucket": "taiga.hooks.bitbucket.services.get_or_generate_config", + "gogs": "taiga.hooks.gogs.services.get_or_generate_config", } BITBUCKET_VALID_ORIGIN_IPS = ["131.103.20.165", "131.103.20.166", "104.192.143.192/28", "104.192.143.208/28"] diff --git a/taiga/hooks/bitbucket/api.py b/taiga/hooks/bitbucket/api.py index 24fc478c..07e2829b 100644 --- a/taiga/hooks/bitbucket/api.py +++ b/taiga/hooks/bitbucket/api.py @@ -72,13 +72,5 @@ class BitBucketViewSet(BaseWebhookApiViewSet): return project_secret == secret_key - def _get_project(self, request): - project_id = request.GET.get("project", None) - try: - project = Project.objects.get(id=project_id) - return project - except Project.DoesNotExist: - return None - def _get_event_name(self, request): return request.META.get('HTTP_X_EVENT_KEY', None) diff --git a/taiga/hooks/bitbucket/event_hooks.py b/taiga/hooks/bitbucket/event_hooks.py index 8737aaa7..67ffc3fd 100644 --- a/taiga/hooks/bitbucket/event_hooks.py +++ b/taiga/hooks/bitbucket/event_hooks.py @@ -18,181 +18,67 @@ import re -from django.utils.translation import ugettext as _ - -from taiga.base import exceptions as exc -from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus -from taiga.projects.issues.models import Issue -from taiga.projects.tasks.models import Task -from taiga.projects.userstories.models import UserStory -from taiga.projects.history.services import take_snapshot -from taiga.projects.notifications.services import send_notifications -from taiga.hooks.event_hooks import BaseEventHook -from taiga.hooks.exceptions import ActionSyntaxException -from taiga.base.utils import json - -from .services import get_bitbucket_user +from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook -class PushEventHook(BaseEventHook): - def process_event(self): - if self.payload is None: - return +class BaseBitBucketEventHook(): + platform = "BitBucket" + platform_slug = "bitbucket" + def replace_bitbucket_references(self, project_url, wiki_text): + if wiki_text is None: + wiki_text = "" + + template = "\g<1>[BitBucket#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) + return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) + + +class IssuesEventHook(BaseBitBucketEventHook, BaseNewIssueEventHook): + def get_data(self): + description = self.payload.get('issue', {}).get('content', {}).get('raw', '') + project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None) + return { + "number": self.payload.get('issue', {}).get('id', None), + "subject": self.payload.get('issue', {}).get('title', None), + "url": self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None), + "user_id": self.payload.get('actor', {}).get('uuid', None), + "user_name": self.payload.get('actor', {}).get('username', None), + "user_url": self.payload.get('actor', {}).get('links', {}).get('html', {}).get('href'), + "description": self.replace_bitbucket_references(project_url, description), + } + + +class IssueCommentEventHook(BaseBitBucketEventHook, BaseIssueCommentEventHook): + def get_data(self): + comment_message = self.payload.get('comment', {}).get('content', {}).get('raw', '') + project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None) + issue_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None) + comment_id = self.payload.get('comment', {}).get('id', None) + comment_url = "{}#comment-{}".format(issue_url, comment_id) + return { + "number": self.payload.get('issue', {}).get('id', None), + 'url': issue_url, + 'user_id': self.payload.get('actor', {}).get('uuid', None), + 'user_name': self.payload.get('actor', {}).get('username', None), + 'user_url': self.payload.get('actor', {}).get('links', {}).get('html', {}).get('href'), + 'comment_url': comment_url, + 'comment_message': self.replace_bitbucket_references(project_url, comment_message) + } + + +class PushEventHook(BaseBitBucketEventHook, BasePushEventHook): + def get_data(self): + result = [] changes = self.payload.get("push", {}).get('changes', []) for change in filter(None, changes): - commits = change.get("commits", []) - if not commits: - continue - - for commit in commits: - message = commit.get("message", None) - if not message: - continue - - self._process_message(message, None) - - def _process_message(self, message, bitbucket_user): - """ - The message we will be looking for seems like - TG-XX #yyyyyy - Where: - XX: is the ref for us, issue or task - yyyyyy: is the status slug we are setting - """ - if message is None: - return - - p = re.compile("tg-(\d+) +#([-\w]+)") - for m in p.finditer(message.lower()): - ref = m.group(1) - status_slug = m.group(2) - self._change_status(ref, status_slug, bitbucket_user) - - def _change_status(self, ref, status_slug, bitbucket_user): - if Issue.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Issue - statusClass = IssueStatus - elif Task.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Task - statusClass = TaskStatus - elif UserStory.objects.filter(project=self.project, ref=ref).exists(): - modelClass = UserStory - statusClass = UserStoryStatus - else: - raise ActionSyntaxException(_("The referenced element doesn't exist")) - - element = modelClass.objects.get(project=self.project, ref=ref) - - try: - status = statusClass.objects.get(project=self.project, slug=status_slug) - except statusClass.DoesNotExist: - raise ActionSyntaxException(_("The status doesn't exist")) - - element.status = status - element.save() - - snapshot = take_snapshot(element, - comment=_("Status changed from BitBucket commit"), - user=get_bitbucket_user(bitbucket_user)) - send_notifications(element, history=snapshot) - - -def replace_bitbucket_references(project_url, wiki_text): - template = "\g<1>[BitBucket#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) - return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) - - -class IssuesEventHook(BaseEventHook): - def process_event(self): - number = self.payload.get('issue', {}).get('id', None) - subject = self.payload.get('issue', {}).get('title', None) - - bitbucket_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None) - - bitbucket_user_id = self.payload.get('actor', {}).get('user', {}).get('uuid', None) - bitbucket_user_name = self.payload.get('actor', {}).get('user', {}).get('username', None) - bitbucket_user_url = self.payload.get('actor', {}).get('user', {}).get('links', {}).get('html', {}).get('href') - - project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None) - - description = self.payload.get('issue', {}).get('content', {}).get('raw', '') - description = replace_bitbucket_references(project_url, description) - - user = get_bitbucket_user(bitbucket_user_id) - - if not all([subject, bitbucket_url, project_url]): - raise ActionSyntaxException(_("Invalid issue information")) - - issue = Issue.objects.create( - project=self.project, - subject=subject, - description=description, - status=self.project.default_issue_status, - type=self.project.default_issue_type, - severity=self.project.default_severity, - priority=self.project.default_priority, - external_reference=['bitbucket', bitbucket_url], - owner=user - ) - take_snapshot(issue, user=user) - - if number and subject and bitbucket_user_name and bitbucket_user_url: - comment = _("Issue created by [@{bitbucket_user_name}]({bitbucket_user_url} " - "\"See @{bitbucket_user_name}'s BitBucket profile\") " - "from BitBucket.\nOrigin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} " - "\"Go to 'bb#{number} - {subject}'\"):\n\n" - "{description}").format(bitbucket_user_name=bitbucket_user_name, - bitbucket_user_url=bitbucket_user_url, - number=number, - subject=subject, - bitbucket_url=bitbucket_url, - description=description) - else: - comment = _("Issue created from BitBucket.") - - snapshot = take_snapshot(issue, comment=comment, user=user) - send_notifications(issue, history=snapshot) - - -class IssueCommentEventHook(BaseEventHook): - def process_event(self): - number = self.payload.get('issue', {}).get('id', None) - subject = self.payload.get('issue', {}).get('title', None) - - bitbucket_url = self.payload.get('issue', {}).get('links', {}).get('html', {}).get('href', None) - bitbucket_user_id = self.payload.get('actor', {}).get('user', {}).get('uuid', None) - bitbucket_user_name = self.payload.get('actor', {}).get('user', {}).get('username', None) - bitbucket_user_url = self.payload.get('actor', {}).get('user', {}).get('links', {}).get('html', {}).get('href') - - project_url = self.payload.get('repository', {}).get('links', {}).get('html', {}).get('href', None) - - comment_message = self.payload.get('comment', {}).get('content', {}).get('raw', '') - comment_message = replace_bitbucket_references(project_url, comment_message) - - user = get_bitbucket_user(bitbucket_user_id) - - if not all([comment_message, bitbucket_url, project_url]): - raise ActionSyntaxException(_("Invalid issue comment information")) - - issues = Issue.objects.filter(external_reference=["bitbucket", bitbucket_url]) - tasks = Task.objects.filter(external_reference=["bitbucket", bitbucket_url]) - uss = UserStory.objects.filter(external_reference=["bitbucket", bitbucket_url]) - - for item in list(issues) + list(tasks) + list(uss): - if number and subject and bitbucket_user_name and bitbucket_user_url: - comment = _("Comment by [@{bitbucket_user_name}]({bitbucket_user_url} " - "\"See @{bitbucket_user_name}'s BitBucket profile\") " - "from BitBucket.\nOrigin BitBucket issue: [bb#{number} - {subject}]({bitbucket_url} " - "\"Go to 'bb#{number} - {subject}'\")\n\n" - "{message}").format(bitbucket_user_name=bitbucket_user_name, - bitbucket_user_url=bitbucket_user_url, - number=number, - subject=subject, - bitbucket_url=bitbucket_url, - message=comment_message) - else: - comment = _("Comment From BitBucket:\n\n{message}").format(message=comment_message) - - snapshot = take_snapshot(item, comment=comment, user=user) - send_notifications(item, history=snapshot) + for commit in change.get("commits", []): + message = commit.get("message") + result.append({ + 'user_id': commit.get('author', {}).get('user', {}).get('uuid', None), + "user_name": commit.get('author', {}).get('user', {}).get('username', None), + "user_url": commit.get('author', {}).get('user', {}).get('links', {}).get('html', {}).get('href'), + "commit_id": commit.get("hash", None), + "commit_url": commit.get("links", {}).get('html', {}).get('href'), + "commit_message": message.strip(), + }) + return result diff --git a/taiga/hooks/event_hooks.py b/taiga/hooks/event_hooks.py index f4f6d2e8..85ac892f 100644 --- a/taiga/hooks/event_hooks.py +++ b/taiga/hooks/event_hooks.py @@ -16,11 +16,247 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import re + +from django.utils.translation import ugettext as _ +from django.contrib.auth import get_user_model +from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus +from taiga.projects.issues.models import Issue +from taiga.projects.tasks.models import Task +from taiga.projects.userstories.models import UserStory +from taiga.projects.history.services import take_snapshot +from taiga.projects.notifications.services import send_notifications +from taiga.hooks.exceptions import ActionSyntaxException +from taiga.users.models import AuthData + class BaseEventHook: + platform = "Unknown" + platform_slug = "unknown" + def __init__(self, project, payload): self.project = project self.payload = payload + def ignore(self): + return False + + def get_user(self, user_id, platform): + user = None + + if user_id: + try: + user = AuthData.objects.get(key=platform, value=user_id).user + except AuthData.DoesNotExist: + pass + + if user is None: + user = get_user_model().objects.get(is_system=True, username__startswith=platform) + + return user + + +class BaseIssueCommentEventHook(BaseEventHook): + def get_data(self): + raise NotImplementedError + + def generate_issue_comment_message(self, **kwargs): + _issue_comment_message = _( + "[@{user_name}]({user_url} " + "\"See @{user_name}'s {platform} profile\") " + "says in [{platform}#{number}]({comment_url} \"Go to comment\"):\n\n" + "\"{comment_message}\"" + ) + _simple_issue_comment_message = _("Comment From {platform}:\n\n> {comment_message}") + try: + return _issue_comment_message.format(platform=self.platform, **kwargs) + except Exception: + return _simple_issue_comment_message.format(platform=self.platform, message=kwargs.get('comment_message')) + def process_event(self): - raise NotImplementedError("process_event must be overwritten") + if self.ignore(): + return + + data = self.get_data() + + if not all([data['comment_message'], data['url']]): + raise ActionSyntaxException(_("Invalid issue comment information")) + + comment = self.generate_issue_comment_message(**data) + + issues = Issue.objects.filter(external_reference=[self.platform_slug, data['url']]) + tasks = Task.objects.filter(external_reference=[self.platform_slug, data['url']]) + uss = UserStory.objects.filter(external_reference=[self.platform_slug, data['url']]) + + for item in list(issues) + list(tasks) + list(uss): + snapshot = take_snapshot(item, comment=comment, user=self.get_user(data['user_id'], self.platform_slug)) + send_notifications(item, history=snapshot) + + +class BaseNewIssueEventHook(BaseEventHook): + def get_data(self): + raise NotImplementedError + + def generate_new_issue_comment(self, **kwargs): + _new_issue_message = _( + "Issue created by [@{user_name}]({user_url} " + "\"See @{user_name}'s {platform} profile\") " + "from [{platform}#{number}]({url} \"Go to issue\")." + ) + _simple_new_issue_message = _("Issue created from {platform}.") + try: + return _new_issue_message.format(platform=self.platform, **kwargs) + except Exception: + return _simple_new_issue_message.format(platform=self.platform) + + def process_event(self): + if self.ignore(): + return + + data = self.get_data() + + if not all([data['subject'], data['url']]): + raise ActionSyntaxException(_("Invalid issue information")) + + user = self.get_user(data['user_id'], self.platform_slug) + + issue = Issue.objects.create( + project=self.project, + subject=data['subject'], + description=data['description'], + status=self.project.default_issue_status, + type=self.project.default_issue_type, + severity=self.project.default_severity, + priority=self.project.default_priority, + external_reference=[self.platform_slug, data['url']], + owner=user + ) + take_snapshot(issue, user=user) + + comment = self.generate_new_issue_comment(**data) + + snapshot = take_snapshot(issue, comment=comment, user=user) + send_notifications(issue, history=snapshot) + + +class BasePushEventHook(BaseEventHook): + def get_data(self): + raise NotImplementedError + + def generate_status_change_comment(self, **kwargs): + if kwargs.get('user_url', None) is None: + user_text = kwargs.get('user_name', _('unknown user')) + else: + user_text = "[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\")".format( + platform=self.platform, + **kwargs + ) + _status_change_message = _( + "{user_text} changed the status from " + "[{platform} commit]({commit_url} \"See commit '{commit_id} - {commit_message}'\")\n\n" + " - Status: **{src_status}** → **{dst_status}**" + ) + _simple_status_change_message = _( + "Changed status from {platform} commit.\n\n" + " - Status: **{src_status}** → **{dst_status}**" + ) + try: + return _status_change_message.format(platform=self.platform, user_text=user_text, **kwargs) + except Exception: + return _simple_status_change_message.format(platform=self.platform) + + def generate_commit_reference_comment(self, **kwargs): + if kwargs.get('user_url', None) is None: + user_text = kwargs.get('user_name', _('unknown user')) + else: + user_text = "[@{user_name}]({user_url} \"See @{user_name}'s {platform} profile\")".format( + platform=self.platform, + **kwargs + ) + + _status_change_message = _( + "This {type_name} has been mentioned by {user_text} " + "in the [{platform} commit]({commit_url} \"See commit '{commit_id} - {commit_message}'\") " + "\"{commit_message}\"" + ) + _simple_status_change_message = _( + "This issue has been mentioned in the {platform} commit " + "\"{commit_message}\"" + ) + try: + return _status_change_message.format(platform=self.platform, user_text=user_text, **kwargs) + except Exception: + return _simple_status_change_message.format(platform=self.platform) + + def get_item_classes(self, ref): + if Issue.objects.filter(project=self.project, ref=ref).exists(): + modelClass = Issue + statusClass = IssueStatus + elif Task.objects.filter(project=self.project, ref=ref).exists(): + modelClass = Task + statusClass = TaskStatus + elif UserStory.objects.filter(project=self.project, ref=ref).exists(): + modelClass = UserStory + statusClass = UserStoryStatus + else: + raise ActionSyntaxException(_("The referenced element doesn't exist")) + + return (modelClass, statusClass) + + def get_item_by_ref(self, ref): + (modelClass, statusClass) = self.get_item_classes(ref) + + return modelClass.objects.get(project=self.project, ref=ref) + + def set_item_status(self, ref, status_slug): + (modelClass, statusClass) = self.get_item_classes(ref) + element = modelClass.objects.get(project=self.project, ref=ref) + + try: + status = statusClass.objects.get(project=self.project, slug=status_slug) + except statusClass.DoesNotExist: + raise ActionSyntaxException(_("The status doesn't exist")) + + src_status = element.status.name + dst_status = status.name + + element.status = status + element.save() + return (element, src_status, dst_status) + + def process_event(self): + if self.ignore(): + return + data = self.get_data() + + for commit in data: + consumed_refs = [] + + # Status changes + p = re.compile("tg-(\d+) +#([-\w]+)") + for m in p.finditer(commit['commit_message'].lower()): + ref = m.group(1) + status_slug = m.group(2) + (element, src_status, dst_status) = self.set_item_status(ref, status_slug) + + comment = self.generate_status_change_comment(src_status=src_status, dst_status=dst_status, **commit) + snapshot = take_snapshot(element, + comment=comment, + user=self.get_user(commit['user_id'], self.platform_slug)) + send_notifications(element, history=snapshot) + consumed_refs.append(ref) + + # Reference on commit + p = re.compile("tg-(\d+)") + for m in p.finditer(commit['commit_message'].lower()): + ref = m.group(1) + if ref in consumed_refs: + continue + element = self.get_item_by_ref(ref) + type_name = element.__class__._meta.verbose_name + comment = self.generate_commit_reference_comment(type_name=type_name, **commit) + snapshot = take_snapshot(element, + comment=comment, + user=self.get_user(commit['user_id'], self.platform_slug)) + send_notifications(element, history=snapshot) + consumed_refs.append(ref) diff --git a/taiga/hooks/github/event_hooks.py b/taiga/hooks/github/event_hooks.py index 68e57993..c4ecc300 100644 --- a/taiga/hooks/github/event_hooks.py +++ b/taiga/hooks/github/event_hooks.py @@ -16,201 +16,72 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from django.utils.translation import ugettext as _ - -from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus - -from taiga.projects.issues.models import Issue -from taiga.projects.tasks.models import Task -from taiga.projects.userstories.models import UserStory -from taiga.projects.history.services import take_snapshot -from taiga.projects.notifications.services import send_notifications -from taiga.hooks.event_hooks import BaseEventHook -from taiga.hooks.exceptions import ActionSyntaxException - -from .services import get_github_user - import re - -class PushEventHook(BaseEventHook): - def process_event(self): - if self.payload is None: - return - - github_user = self.payload.get('sender', {}) - - commits = self.payload.get("commits", []) - for commit in commits: - self._process_commit(commit, github_user) - - def _process_commit(self, commit, github_user): - """ - The message we will be looking for seems like - TG-XX #yyyyyy - Where: - XX: is the ref for us, issue or task - yyyyyy: is the status slug we are setting - """ - message = commit.get("message", None) - - if message is None: - return - - p = re.compile("tg-(\d+) +#([-\w]+)") - for m in p.finditer(message.lower()): - ref = m.group(1) - status_slug = m.group(2) - self._change_status(ref, status_slug, github_user, commit) - - def _change_status(self, ref, status_slug, github_user, commit): - if Issue.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Issue - statusClass = IssueStatus - elif Task.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Task - statusClass = TaskStatus - elif UserStory.objects.filter(project=self.project, ref=ref).exists(): - modelClass = UserStory - statusClass = UserStoryStatus - else: - raise ActionSyntaxException(_("The referenced element doesn't exist")) - - element = modelClass.objects.get(project=self.project, ref=ref) - - try: - status = statusClass.objects.get(project=self.project, slug=status_slug) - except statusClass.DoesNotExist: - raise ActionSyntaxException(_("The status doesn't exist")) - - element.status = status - element.save() - - github_user_id = github_user.get('id', None) - github_user_name = github_user.get('login', None) - github_user_url = github_user.get('html_url', None) - commit_id = commit.get("id", None) - commit_url = commit.get("url", None) - commit_message = commit.get("message", None) - - if (github_user_id and github_user_name and github_user_url and - commit_id and commit_url and commit_message): - comment = _("Status changed by [@{github_user_name}]({github_user_url} " - "\"See @{github_user_name}'s GitHub profile\") " - "from GitHub commit [{commit_id}]({commit_url} " - "\"See commit '{commit_id} - {commit_message}'\").").format( - github_user_name=github_user_name, - github_user_url=github_user_url, - commit_id=commit_id[:7], - commit_url=commit_url, - commit_message=commit_message) - - else: - comment = _("Status changed from GitHub commit.") - - snapshot = take_snapshot(element, - comment=comment, - user=get_github_user(github_user_id)) - send_notifications(element, history=snapshot) +from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook -def replace_github_references(project_url, wiki_text): - if wiki_text == None: - wiki_text = "" +class BaseGitHubEventHook(): + platform = "GitHub" + platform_slug = "github" - template = "\g<1>[GitHub#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) - return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) + def replace_github_references(self, project_url, wiki_text): + if wiki_text is None: + wiki_text = "" + + template = "\g<1>[GitHub#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) + return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) -class IssuesEventHook(BaseEventHook): - def process_event(self): - if self.payload.get('action', None) != "opened": - return +class IssuesEventHook(BaseGitHubEventHook, BaseNewIssueEventHook): + def ignore(self): + return self.payload.get('action', None) != "opened" - number = self.payload.get('issue', {}).get('number', None) - subject = self.payload.get('issue', {}).get('title', None) - github_url = self.payload.get('issue', {}).get('html_url', None) - github_user_id = self.payload.get('issue', {}).get('user', {}).get('id', None) - github_user_name = self.payload.get('issue', {}).get('user', {}).get('login', None) - github_user_url = self.payload.get('issue', {}).get('user', {}).get('html_url', None) - project_url = self.payload.get('repository', {}).get('html_url', None) + def get_data(self): description = self.payload.get('issue', {}).get('body', None) - description = replace_github_references(project_url, description) - - user = get_github_user(github_user_id) - - if not all([subject, github_url, project_url]): - raise ActionSyntaxException(_("Invalid issue information")) - - issue = Issue.objects.create( - project=self.project, - subject=subject, - description=description, - status=self.project.default_issue_status, - type=self.project.default_issue_type, - severity=self.project.default_severity, - priority=self.project.default_priority, - external_reference=['github', github_url], - owner=user - ) - take_snapshot(issue, user=user) - - if number and subject and github_user_name and github_user_url: - comment = _("Issue created by [@{github_user_name}]({github_user_url} " - "\"See @{github_user_name}'s GitHub profile\") " - "from GitHub.\nOrigin GitHub issue: [gh#{number} - {subject}]({github_url} " - "\"Go to 'gh#{number} - {subject}'\"):\n\n" - "{description}").format(github_user_name=github_user_name, - github_user_url=github_user_url, - number=number, - subject=subject, - github_url=github_url, - description=description) - else: - comment = _("Issue created from GitHub.") - - snapshot = take_snapshot(issue, comment=comment, user=user) - send_notifications(issue, history=snapshot) - - -class IssueCommentEventHook(BaseEventHook): - def process_event(self): - if self.payload.get('action', None) != "created": - raise ActionSyntaxException(_("Invalid issue comment information")) - - number = self.payload.get('issue', {}).get('number', None) - subject = self.payload.get('issue', {}).get('title', None) - github_url = self.payload.get('issue', {}).get('html_url', None) - github_user_id = self.payload.get('sender', {}).get('id', None) - github_user_name = self.payload.get('sender', {}).get('login', None) - github_user_url = self.payload.get('sender', {}).get('html_url', None) project_url = self.payload.get('repository', {}).get('html_url', None) + return { + "number": self.payload.get('issue', {}).get('number', None), + "subject": self.payload.get('issue', {}).get('title', None), + "url": self.payload.get('issue', {}).get('html_url', None), + "user_id": self.payload.get('issue', {}).get('user', {}).get('id', None), + "user_name": self.payload.get('issue', {}).get('user', {}).get('login', None), + "user_url": self.payload.get('issue', {}).get('user', {}).get('html_url', None), + "description": self.replace_github_references(project_url, description), + } + + +class IssueCommentEventHook(BaseGitHubEventHook, BaseIssueCommentEventHook): + def ignore(self): + return self.payload.get('action', None) != "created" + + def get_data(self): comment_message = self.payload.get('comment', {}).get('body', None) - comment_message = replace_github_references(project_url, comment_message) + project_url = self.payload.get('repository', {}).get('html_url', None) + return { + "number": self.payload.get('issue', {}).get('number', None), + "url": self.payload.get('issue', {}).get('html_url', None), + "user_id": self.payload.get('sender', {}).get('id', None), + "user_name": self.payload.get('sender', {}).get('login', None), + "user_url": self.payload.get('sender', {}).get('html_url', None), + "comment_url": self.payload.get('comment', {}).get('html_url', None), + "comment_message": self.replace_github_references(project_url, comment_message), + } - user = get_github_user(github_user_id) - if not all([comment_message, github_url, project_url]): - raise ActionSyntaxException(_("Invalid issue comment information")) +class PushEventHook(BaseGitHubEventHook, BasePushEventHook): + def get_data(self): + result = [] + github_user = self.payload.get('sender', {}) + commits = self.payload.get("commits", []) + for commit in filter(None, commits): + result.append({ + "user_id": github_user.get('id', None), + "user_name": github_user.get('login', None), + "user_url": github_user.get('html_url', None), + "commit_id": commit.get("id", None), + "commit_url": commit.get("url", None), + "commit_message": commit.get("message", None), + }) - issues = Issue.objects.filter(external_reference=["github", github_url]) - tasks = Task.objects.filter(external_reference=["github", github_url]) - uss = UserStory.objects.filter(external_reference=["github", github_url]) - - for item in list(issues) + list(tasks) + list(uss): - if number and subject and github_user_name and github_user_url: - comment = _("Comment by [@{github_user_name}]({github_user_url} " - "\"See @{github_user_name}'s GitHub profile\") " - "from GitHub.\nOrigin GitHub issue: [gh#{number} - {subject}]({github_url} " - "\"Go to 'gh#{number} - {subject}'\")\n\n" - "{message}").format(github_user_name=github_user_name, - github_user_url=github_user_url, - number=number, - subject=subject, - github_url=github_url, - message=comment_message) - else: - comment = _("Comment From GitHub:\n\n{message}").format(message=comment_message) - - snapshot = take_snapshot(item, comment=comment, user=user) - send_notifications(item, history=snapshot) + return result diff --git a/taiga/hooks/github/services.py b/taiga/hooks/github/services.py index cd244ae3..e7286d86 100644 --- a/taiga/hooks/github/services.py +++ b/taiga/hooks/github/services.py @@ -18,10 +18,8 @@ import uuid -from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse -from taiga.users.models import AuthData from taiga.base.utils.urls import get_absolute_url @@ -38,18 +36,3 @@ def get_or_generate_config(project): url = "%s?project=%s" % (url, project.id) g_config["webhooks_url"] = url return g_config - - -def get_github_user(github_id): - user = None - - if github_id: - try: - user = AuthData.objects.get(key="github", value=github_id).user - except AuthData.DoesNotExist: - pass - - if user is None: - user = get_user_model().objects.get(is_system=True, username__startswith="github") - - return user diff --git a/taiga/hooks/gitlab/api.py b/taiga/hooks/gitlab/api.py index 910ee437..127d7536 100644 --- a/taiga/hooks/gitlab/api.py +++ b/taiga/hooks/gitlab/api.py @@ -70,14 +70,6 @@ class GitLabViewSet(BaseWebhookApiViewSet): return project_secret == secret_key - def _get_project(self, request): - project_id = request.GET.get("project", None) - try: - project = Project.objects.get(id=project_id) - return project - except Project.DoesNotExist: - return None - def _get_event_name(self, request): payload = json.loads(request.body.decode("utf-8")) return payload.get('object_kind', 'push') if payload is not None else 'empty' diff --git a/taiga/hooks/gitlab/event_hooks.py b/taiga/hooks/gitlab/event_hooks.py index aff09e2f..5b4b4006 100644 --- a/taiga/hooks/gitlab/event_hooks.py +++ b/taiga/hooks/gitlab/event_hooks.py @@ -19,158 +19,71 @@ import re import os -from django.utils.translation import ugettext as _ - -from taiga.projects.models import IssueStatus, TaskStatus, UserStoryStatus - -from taiga.projects.issues.models import Issue -from taiga.projects.tasks.models import Task -from taiga.projects.userstories.models import UserStory -from taiga.projects.history.services import take_snapshot -from taiga.projects.notifications.services import send_notifications -from taiga.hooks.event_hooks import BaseEventHook -from taiga.hooks.exceptions import ActionSyntaxException - -from .services import get_gitlab_user +from taiga.hooks.event_hooks import BaseNewIssueEventHook, BaseIssueCommentEventHook, BasePushEventHook -class PushEventHook(BaseEventHook): - def process_event(self): - if self.payload is None: - return +class BaseGitLabEventHook(): + platform = "GitLab" + platform_slug = "gitlab" - commits = self.payload.get("commits", []) - for commit in commits: - message = commit.get("message", None) - self._process_message(message, None) + def replace_gitlab_references(self, project_url, wiki_text): + if wiki_text is None: + wiki_text = "" - def _process_message(self, message, gitlab_user): - """ - The message we will be looking for seems like - TG-XX #yyyyyy - Where: - XX: is the ref for us, issue or task - yyyyyy: is the status slug we are setting - """ - if message is None: - return - - p = re.compile("tg-(\d+) +#([-\w]+)") - for m in p.finditer(message.lower()): - ref = m.group(1) - status_slug = m.group(2) - self._change_status(ref, status_slug, gitlab_user) - - def _change_status(self, ref, status_slug, gitlab_user): - if Issue.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Issue - statusClass = IssueStatus - elif Task.objects.filter(project=self.project, ref=ref).exists(): - modelClass = Task - statusClass = TaskStatus - elif UserStory.objects.filter(project=self.project, ref=ref).exists(): - modelClass = UserStory - statusClass = UserStoryStatus - else: - raise ActionSyntaxException(_("The referenced element doesn't exist")) - - element = modelClass.objects.get(project=self.project, ref=ref) - - try: - status = statusClass.objects.get(project=self.project, slug=status_slug) - except statusClass.DoesNotExist: - raise ActionSyntaxException(_("The status doesn't exist")) - - element.status = status - element.save() - - snapshot = take_snapshot(element, - comment=_("Status changed from GitLab commit"), - user=get_gitlab_user(gitlab_user)) - send_notifications(element, history=snapshot) + template = "\g<1>[GitLab#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) + return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) -def replace_gitlab_references(project_url, wiki_text): - if wiki_text is None: - wiki_text = "" +class IssuesEventHook(BaseGitLabEventHook, BaseNewIssueEventHook): + def ignore(self): + return self.payload.get('object_attributes', {}).get("action", "") != "open" - template = "\g<1>[GitLab#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) - return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) - - -class IssuesEventHook(BaseEventHook): - def process_event(self): - if self.payload.get('object_attributes', {}).get("action", "") != "open": - return - - subject = self.payload.get('object_attributes', {}).get('title', None) + def get_data(self): description = self.payload.get('object_attributes', {}).get('description', None) - gitlab_reference = self.payload.get('object_attributes', {}).get('url', None) - - project_url = None - if gitlab_reference: - project_url = os.path.basename(os.path.basename(gitlab_reference)) - - if not all([subject, gitlab_reference, project_url]): - raise ActionSyntaxException(_("Invalid issue information")) - - issue = Issue.objects.create( - project=self.project, - subject=subject, - description=replace_gitlab_references(project_url, description), - status=self.project.default_issue_status, - type=self.project.default_issue_type, - severity=self.project.default_severity, - priority=self.project.default_priority, - external_reference=['gitlab', gitlab_reference], - owner=get_gitlab_user(None) - ) - take_snapshot(issue, user=get_gitlab_user(None)) - - snapshot = take_snapshot(issue, comment=_("Created from GitLab"), user=get_gitlab_user(None)) - send_notifications(issue, history=snapshot) - - -class IssueCommentEventHook(BaseEventHook): - def process_event(self): - if self.payload.get('object_attributes', {}).get("noteable_type", None) != "Issue": - return - - number = self.payload.get('issue', {}).get('iid', None) - subject = self.payload.get('issue', {}).get('title', None) - project_url = self.payload.get('repository', {}).get('homepage', None) + user_name = self.payload.get('user', {}).get('username', None) + return { + "number": self.payload.get('object_attributes', {}).get('iid', None), + "subject": self.payload.get('object_attributes', {}).get('title', None), + "url": self.payload.get('object_attributes', {}).get('url', None), + "user_id": None, + "user_name": user_name, + "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", user_name), + "description": self.replace_gitlab_references(project_url, description), + } - gitlab_url = os.path.join(project_url, "issues", str(number)) - gitlab_user_name = self.payload.get('user', {}).get('username', None) - gitlab_user_url = os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", gitlab_user_name) +class IssueCommentEventHook(BaseGitLabEventHook, BaseIssueCommentEventHook): + def ignore(self): + return self.payload.get('object_attributes', {}).get("noteable_type", None) != "Issue" + + def get_data(self): comment_message = self.payload.get('object_attributes', {}).get('note', None) - comment_message = replace_gitlab_references(project_url, comment_message) + project_url = self.payload.get('repository', {}).get('homepage', None) + number = self.payload.get('issue', {}).get('iid', None) + user_name = self.payload.get('user', {}).get('username', None) + return { + "number": number, + "url": os.path.join(project_url, "issues", str(number)), + "user_id": None, + "user_name": user_name, + "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), "u", user_name), + "comment_url": self.payload.get('object_attributes', {}).get('url', None), + "comment_message": self.replace_gitlab_references(project_url, comment_message), + } - user = get_gitlab_user(None) - if not all([comment_message, gitlab_url, project_url]): - raise ActionSyntaxException(_("Invalid issue comment information")) - - issues = Issue.objects.filter(external_reference=["gitlab", gitlab_url]) - tasks = Task.objects.filter(external_reference=["gitlab", gitlab_url]) - uss = UserStory.objects.filter(external_reference=["gitlab", gitlab_url]) - - for item in list(issues) + list(tasks) + list(uss): - if number and subject and gitlab_user_name and gitlab_user_url: - comment = _("Comment by [@{gitlab_user_name}]({gitlab_user_url} " - "\"See @{gitlab_user_name}'s GitLab profile\") " - "from GitLab.\nOrigin GitLab issue: [gl#{number} - {subject}]({gitlab_url} " - "\"Go to 'gl#{number} - {subject}'\")\n\n" - "{message}").format(gitlab_user_name=gitlab_user_name, - gitlab_user_url=gitlab_user_url, - number=number, - subject=subject, - gitlab_url=gitlab_url, - message=comment_message) - else: - comment = _("Comment From GitLab:\n\n{message}").format(message=comment_message) - - snapshot = take_snapshot(item, comment=comment, user=user) - send_notifications(item, history=snapshot) +class PushEventHook(BaseGitLabEventHook, BasePushEventHook): + def get_data(self): + result = [] + for commit in self.payload.get("commits", []): + user_name = commit.get('author', {}).get('name', None) + result.append({ + "user_id": None, + "user_name": user_name, + "user_url": None, + "commit_id": commit.get("id", None), + "commit_url": commit.get("url", None), + "commit_message": commit.get("message").strip(), + }) + return result diff --git a/taiga/hooks/gitlab/services.py b/taiga/hooks/gitlab/services.py index cd4751fb..a31352ed 100644 --- a/taiga/hooks/gitlab/services.py +++ b/taiga/hooks/gitlab/services.py @@ -18,7 +18,6 @@ import uuid -from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.conf import settings @@ -41,18 +40,3 @@ def get_or_generate_config(project): url = "{}?project={}&key={}".format(url, project.id, g_config["secret"]) g_config["webhooks_url"] = url return g_config - - -def get_gitlab_user(user_email): - user = None - - if user_email: - try: - user = get_user_model().objects.get(email=user_email) - except get_user_model().DoesNotExist: - pass - - if user is None: - user = get_user_model().objects.get(is_system=True, username__startswith="gitlab") - - return user diff --git a/taiga/hooks/gogs/__init__.py b/taiga/hooks/gogs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/hooks/gogs/api.py b/taiga/hooks/gogs/api.py new file mode 100644 index 00000000..ced551de --- /dev/null +++ b/taiga/hooks/gogs/api.py @@ -0,0 +1,44 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.hooks.api import BaseWebhookApiViewSet + +from . import event_hooks + + +class GogsViewSet(BaseWebhookApiViewSet): + event_hook_classes = { + "push": event_hooks.PushEventHook + } + + def _validate_signature(self, project, request): + payload = self._get_payload(request) + + if not hasattr(project, "modules_config"): + return False + + if project.modules_config.config is None: + return False + + secret = project.modules_config.config.get("gogs", {}).get("secret", None) + if secret is None: + return False + + return payload.get('secret', None) == secret + + def _get_event_name(self, request): + return "push" diff --git a/taiga/hooks/gogs/event_hooks.py b/taiga/hooks/gogs/event_hooks.py new file mode 100644 index 00000000..8e68b8db --- /dev/null +++ b/taiga/hooks/gogs/event_hooks.py @@ -0,0 +1,52 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import re +import os.path + +from taiga.hooks.event_hooks import BasePushEventHook + + +class BaseGogsEventHook(): + platform = "Gogs" + platform_slug = "gogs" + + def replace_gogs_references(self, project_url, wiki_text): + if wiki_text is None: + wiki_text = "" + + template = "\g<1>[Gogs#\g<2>]({}/issues/\g<2>)\g<3>".format(project_url) + return re.sub(r"(\s|^)#(\d+)(\s|$)", template, wiki_text, 0, re.M) + + +class PushEventHook(BaseGogsEventHook, BasePushEventHook): + def get_data(self): + result = [] + commits = self.payload.get("commits", []) + project_url = self.payload.get("repository", {}).get("url", None) + + for commit in filter(None, commits): + user_name = commit.get('author', {}).get('username', None) + result.append({ + "user_id": user_name, + "user_name": user_name, + "user_url": os.path.join(os.path.dirname(os.path.dirname(project_url)), user_name), + "commit_id": commit.get("id", None), + "commit_url": commit.get("url", None), + "commit_message": commit.get("message", None), + }) + return result diff --git a/taiga/hooks/gogs/migrations/0001_initial.py b/taiga/hooks/gogs/migrations/0001_initial.py new file mode 100644 index 00000000..09ba6709 --- /dev/null +++ b/taiga/hooks/gogs/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations +from django.core.files import File + +import uuid +import os + +CUR_DIR = os.path.dirname(__file__) + + +def create_gogs_system_user(apps, schema_editor): + # We get the model from the versioned app registry; + # if we directly import it, it'll be the wrong version + User = apps.get_model("users", "User") + db_alias = schema_editor.connection.alias + random_hash = uuid.uuid4().hex + user = User.objects.using(db_alias).create( + username="gogs-{}".format(random_hash), + email="gogs-{}@taiga.io".format(random_hash), + full_name="Gogs", + is_active=False, + is_system=True, + bio="", + ) + f = open("{}/logo.png".format(CUR_DIR), "rb") + user.photo.save("logo.png", File(f)) + user.save() + + +class Migration(migrations.Migration): + dependencies = [ + ('users', '0010_auto_20150414_0936') + ] + + operations = [ + migrations.RunPython(create_gogs_system_user), + ] diff --git a/taiga/hooks/gogs/migrations/__init__.py b/taiga/hooks/gogs/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/taiga/hooks/gogs/migrations/logo.png b/taiga/hooks/gogs/migrations/logo.png new file mode 100644 index 00000000..384a58d2 Binary files /dev/null and b/taiga/hooks/gogs/migrations/logo.png differ diff --git a/taiga/hooks/gogs/models.py b/taiga/hooks/gogs/models.py new file mode 100644 index 00000000..fca83d73 --- /dev/null +++ b/taiga/hooks/gogs/models.py @@ -0,0 +1 @@ +# This file is needed to load migrations diff --git a/taiga/hooks/gogs/services.py b/taiga/hooks/gogs/services.py new file mode 100644 index 00000000..40d06fab --- /dev/null +++ b/taiga/hooks/gogs/services.py @@ -0,0 +1,37 @@ +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import uuid + +from django.core.urlresolvers import reverse + +from taiga.base.utils.urls import get_absolute_url + + +# Set this in settings.PROJECT_MODULES_CONFIGURATORS["gogs"] +def get_or_generate_config(project): + config = project.modules_config.config + if config and "gogs" in config: + g_config = project.modules_config.config["gogs"] + else: + g_config = {"secret": uuid.uuid4().hex} + + url = reverse("gogs-hook-list") + url = get_absolute_url(url) + url = "%s?project=%s" % (url, project.id) + g_config["webhooks_url"] = url + return g_config diff --git a/taiga/routers.py b/taiga/routers.py index 66e1b9f7..24974b74 100644 --- a/taiga/routers.py +++ b/taiga/routers.py @@ -208,6 +208,12 @@ from taiga.hooks.bitbucket.api import BitBucketViewSet router.register(r"bitbucket-hook", BitBucketViewSet, base_name="bitbucket-hook") +# Gogs webhooks +from taiga.hooks.gogs.api import GogsViewSet + +router.register(r"gogs-hook", GogsViewSet, base_name="gogs-hook") + + # Importer from taiga.export_import.api import ProjectImporterViewSet, ProjectExporterViewSet diff --git a/tests/integration/test_hooks_bitbucket.py b/tests/integration/test_hooks_bitbucket.py index 0f74078e..90023b38 100644 --- a/tests/integration/test_hooks_bitbucket.py +++ b/tests/integration/test_hooks_bitbucket.py @@ -246,6 +246,13 @@ def test_push_event_issue_processing(client): new_status = f.IssueStatusFactory(project=creation_status.project) issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -271,6 +278,13 @@ def test_push_event_task_processing(client): new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -296,6 +310,13 @@ def test_push_event_user_story_processing(client): new_status = f.UserStoryStatusFactory(project=creation_status.project) user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -314,6 +335,108 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (issue.ref) } + ] + } + ] + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (task.ref) } + ] + } + ] + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, + "push": { + "changes": [ + { + "commits": [ + { "message": "test message test TG-%s ok bye!" % (user_story.ref) } + ] + } + ] + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -322,6 +445,13 @@ def test_push_event_multiple_actions(client): issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -349,6 +479,13 @@ def test_push_event_processing_case_insensitive(client): new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -370,6 +507,13 @@ def test_push_event_processing_case_insensitive(client): def test_push_event_task_bad_processing_non_existing_ref(client): issue_status = f.IssueStatusFactory() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -393,6 +537,13 @@ def test_push_event_task_bad_processing_non_existing_ref(client): def test_push_event_us_bad_processing_non_existing_status(client): user_story = f.UserStoryFactory.create() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -417,6 +568,13 @@ def test_push_event_us_bad_processing_non_existing_status(client): def test_push_event_bad_processing_non_existing_status(client): issue = f.IssueFactory.create() payload = { + "actor": { + "user": { + "uuid": "{ce1054cd-3f43-49dc-8aea-d3085ee7ec9b}", + "username": "test-user", + "links": {"html": {"href": "http://bitbucket.com/test-user"}} + } + }, "push": { "changes": [ { @@ -686,8 +844,10 @@ def test_api_patch_project_modules(client): def test_replace_bitbucket_references(): - assert event_hooks.replace_bitbucket_references("project-url", "#2") == "[BitBucket#2](project-url/issues/2)" - assert event_hooks.replace_bitbucket_references("project-url", "#2 ") == "[BitBucket#2](project-url/issues/2) " - assert event_hooks.replace_bitbucket_references("project-url", " #2 ") == " [BitBucket#2](project-url/issues/2) " - assert event_hooks.replace_bitbucket_references("project-url", " #2") == " [BitBucket#2](project-url/issues/2)" - assert event_hooks.replace_bitbucket_references("project-url", "#test") == "#test" + ev_hook = event_hooks.BaseBitBucketEventHook + assert ev_hook.replace_bitbucket_references(None, "project-url", "#2") == "[BitBucket#2](project-url/issues/2)" + assert ev_hook.replace_bitbucket_references(None, "project-url", "#2 ") == "[BitBucket#2](project-url/issues/2) " + assert ev_hook.replace_bitbucket_references(None, "project-url", " #2 ") == " [BitBucket#2](project-url/issues/2) " + assert ev_hook.replace_bitbucket_references(None, "project-url", " #2") == " [BitBucket#2](project-url/issues/2)" + assert ev_hook.replace_bitbucket_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_bitbucket_references(None, "project-url", None) == "" diff --git a/tests/integration/test_hooks_github.py b/tests/integration/test_hooks_github.py index 4cdeec76..815aba49 100644 --- a/tests/integration/test_hooks_github.py +++ b/tests/integration/test_hooks_github.py @@ -172,6 +172,70 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (issue.ref)}, + ]} + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (task.ref)}, + ]} + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = {"commits": [ + {"message": """test message + test TG-%s ok + bye! + """ % (user_story.ref)}, + ]} + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -454,7 +518,7 @@ def test_issues_event_bad_comment(client): take_snapshot(issue, user=issue.owner) payload = { - "action": "other", + "action": "created", "issue": {}, "comment": {}, "repository": { @@ -512,9 +576,10 @@ def test_api_patch_project_modules(client): def test_replace_github_references(): - assert event_hooks.replace_github_references("project-url", "#2") == "[GitHub#2](project-url/issues/2)" - assert event_hooks.replace_github_references("project-url", "#2 ") == "[GitHub#2](project-url/issues/2) " - assert event_hooks.replace_github_references("project-url", " #2 ") == " [GitHub#2](project-url/issues/2) " - assert event_hooks.replace_github_references("project-url", " #2") == " [GitHub#2](project-url/issues/2)" - assert event_hooks.replace_github_references("project-url", "#test") == "#test" - assert event_hooks.replace_github_references("project-url", None) == "" + ev_hook = event_hooks.BaseGitHubEventHook + assert ev_hook.replace_github_references(None, "project-url", "#2") == "[GitHub#2](project-url/issues/2)" + assert ev_hook.replace_github_references(None, "project-url", "#2 ") == "[GitHub#2](project-url/issues/2) " + assert ev_hook.replace_github_references(None, "project-url", " #2 ") == " [GitHub#2](project-url/issues/2) " + assert ev_hook.replace_github_references(None, "project-url", " #2") == " [GitHub#2](project-url/issues/2)" + assert ev_hook.replace_github_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_github_references(None, "project-url", None) == "" diff --git a/tests/integration/test_hooks_gitlab.py b/tests/integration/test_hooks_gitlab.py index f90f74b0..5208a939 100644 --- a/tests/integration/test_hooks_gitlab.py +++ b/tests/integration/test_hooks_gitlab.py @@ -18,6 +18,7 @@ # along with this program. If not, see . import pytest +from copy import deepcopy from unittest import mock @@ -41,6 +42,189 @@ from .. import factories as f pytestmark = pytest.mark.django_db +push_base_payload = { + "object_kind": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "user_id": 4, + "user_name": "John Smith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project": { + "name": "Diaspora", + "description": "", + "web_url": "http://example.com/mike/diaspora", + "avatar_url": None, + "git_ssh_url": "git@example.com:mike/diaspora.git", + "git_http_url": "http://example.com/mike/diaspora.git", + "namespace": "Mike", + "visibility_level": 0, + "path_with_namespace": "mike/diaspora", + "default_branch": "master", + "homepage": "http://example.com/mike/diaspora", + "url": "git@example.com:mike/diaspora.git", + "ssh_url": "git@example.com:mike/diaspora.git", + "http_url": "http://example.com/mike/diaspora.git" + }, + "repository": { + "name": "Diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url": "http://example.com/mike/diaspora.git", + "git_ssh_url": "git@example.com:mike/diaspora.git", + "visibility_level": 0 + }, + "commits": [ + { + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message": "Update Catalan translation to e38cb41.", + "timestamp": "2011-12-12T14:27:31+02:00", + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author": { + "name": "Jordi Mallach", + "email": "jordi@softcatala.org" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + }, + { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + } + ], + "total_commits_count": 4 +} + +new_issue_base_payload = { + "object_kind": "issue", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project": { + "name": "Gitlab Test", + "description": "Aut reprehenderit ut est.", + "web_url": "http://example.com/gitlabhq/gitlab-test", + "avatar_url": None, + "git_ssh_url": "git@example.com:gitlabhq/gitlab-test.git", + "git_http_url": "http://example.com/gitlabhq/gitlab-test.git", + "namespace": "GitlabHQ", + "visibility_level": 20, + "path_with_namespace": "gitlabhq/gitlab-test", + "default_branch": "master", + "homepage": "http://example.com/gitlabhq/gitlab-test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "ssh_url": "git@example.com:gitlabhq/gitlab-test.git", + "http_url": "http://example.com/gitlabhq/gitlab-test.git" + }, + "repository": { + "name": "Gitlab Test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlabhq/gitlab-test" + }, + "object_attributes": { + "id": 301, + "title": "New API: create/update/delete file", + "assignee_id": 51, + "author_id": 51, + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "position": 0, + "branch_name": None, + "description": "Create new API for manipulations with repository", + "milestone_id": None, + "state": "opened", + "iid": 23, + "url": "http://example.com/diaspora/issues/23", + "action": "open" + }, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } +} + +issue_comment_base_payload = { + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project": { + "name": "Gitlab Test", + "description": "Aut reprehenderit ut est.", + "web_url": "http://example.com/gitlab-org/gitlab-test", + "avatar_url": None, + "git_ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "git_http_url": "http://example.com/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 10, + "path_with_namespace": "gitlab-org/gitlab-test", + "default_branch": "master", + "homepage": "http://example.com/gitlab-org/gitlab-test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "ssh_url": "git@example.com:gitlab-org/gitlab-test.git", + "http_url": "http://example.com/gitlab-org/gitlab-test.git" + }, + "repository": { + "name": "diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": None, + "line_code": None, + "commit_id": "", + "noteable_id": 92, + "system": False, + "st_diff": None, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_id": None, + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-12 14:53:17 UTC", + "updated_at": "2015-04-26 08:28:42 UTC", + "position": 0, + "branch_name": None, + "description": "test", + "milestone_id": None, + "state": "closed", + "iid": 17 + } +} def test_bad_signature(client): project = f.ProjectFactory() @@ -90,8 +274,8 @@ def test_ok_empty_payload(client): url = reverse("gitlab-hook-list") url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e") - data = {} - response = client.post(url,"null", content_type="application/json", REMOTE_ADDR="111.111.111.111") + response = client.post(url, "null", content_type="application/json", + REMOTE_ADDR="111.111.111.111") assert response.status_code == 204 @@ -108,8 +292,7 @@ def test_ok_signature_ip_in_network(client): url = reverse("gitlab-hook-list") url = "{}?project={}&key={}".format(url, project.id, "tpnIwJDz4e") data = {"test:": "data"} - response = client.post(url, - json.dumps(data), + response = client.post(url, json.dumps(data), content_type="application/json", REMOTE_ADDR="111.111.111.112") @@ -243,9 +426,13 @@ def test_push_event_detected(client): project = f.ProjectFactory() url = reverse("gitlab-hook-list") url = "%s?project=%s" % (url, project.id) - data = {"commits": [ - {"message": "test message"}, - ]} + data = deepcopy(push_base_payload) + data["commits"] = [{ + "message": "test message", + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + data["total_commits_count"] = 1 GitLabViewSet._validate_signature = mock.Mock(return_value=True) @@ -265,12 +452,16 @@ def test_push_event_issue_processing(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.IssueStatusFactory(project=creation_status.project) issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message - test TG-%s #%s ok - bye! - """ % (issue.ref, new_status.slug)}, - ]} + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s #%s ok + bye! + """ % (issue.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(issue.project, payload) ev_hook.process_event() @@ -285,12 +476,16 @@ def test_push_event_task_processing(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok bye! - """ % (task.ref, new_status.slug)}, - ]} + """ % (task.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(task.project, payload) ev_hook.process_event() @@ -305,12 +500,16 @@ def test_push_event_user_story_processing(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.UserStoryStatusFactory(project=creation_status.project) user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok bye! - """ % (user_story.ref, new_status.slug)}, - ]} + """ % (user_story.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(user_story.project, payload) @@ -320,6 +519,79 @@ def test_push_event_user_story_processing(client): assert len(mail.outbox) == 1 +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (issue.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (task.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message + test TG-%s ok + bye! + """ % (user_story.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + def test_push_event_multiple_actions(client): creation_status = f.IssueStatusFactory() role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) @@ -327,13 +599,17 @@ def test_push_event_multiple_actions(client): new_status = f.IssueStatusFactory(project=creation_status.project) issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #%s ok test TG-%s #%s ok bye! - """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug)}, - ]} + """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook1 = event_hooks.PushEventHook(issue1.project, payload) ev_hook1.process_event() @@ -350,12 +626,16 @@ def test_push_event_processing_case_insensitive(client): f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) new_status = f.TaskStatusFactory(project=creation_status.project) task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test tg-%s #%s ok bye! - """ % (task.ref, new_status.slug.upper())}, - ]} + """ % (task.ref, new_status.slug.upper()), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(task.project, payload) ev_hook.process_event() @@ -366,12 +646,16 @@ def test_push_event_processing_case_insensitive(client): def test_push_event_task_bad_processing_non_existing_ref(client): issue_status = f.IssueStatusFactory() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-6666666 #%s ok bye! - """ % (issue_status.slug)}, - ]} + """ % (issue_status.slug), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] ev_hook = event_hooks.PushEventHook(issue_status.project, payload) @@ -384,12 +668,16 @@ def test_push_event_task_bad_processing_non_existing_ref(client): def test_push_event_us_bad_processing_non_existing_status(client): user_story = f.UserStoryFactory.create() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #non-existing-slug ok bye! - """ % (user_story.ref)}, - ]} + """ % (user_story.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] @@ -403,12 +691,16 @@ def test_push_event_us_bad_processing_non_existing_status(client): def test_push_event_bad_processing_non_existing_status(client): issue = f.IssueFactory.create() - payload = {"commits": [ - {"message": """test message + payload = deepcopy(push_base_payload) + payload["commits"] = [{ + "message": """test message test TG-%s #non-existing-slug ok bye! - """ % (issue.ref)}, - ]} + """ % (issue.ref), + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + }] + payload["total_commits_count"] = 1 mail.outbox = [] @@ -432,15 +724,12 @@ def test_issues_event_opened_issue(client): notify_policy.notify_level = NotifyLevel.all notify_policy.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "title": "test-title", - "description": "test-body", - "url": "http://gitlab.com/test/project/issues/11", - "action": "open", - }, - } + payload = deepcopy(new_issue_base_payload) + payload["object_attributes"]["title"] = "test-title" + payload["object_attributes"]["description"] = "test-body" + payload["object_attributes"]["url"] = "http://gitlab.com/test/project/issues/11" + payload["object_attributes"]["action"] = "open" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -459,15 +748,12 @@ def test_issues_event_other_than_opened_issue(client): issue.project.default_priority = issue.priority issue.project.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "title": "test-title", - "description": "test-body", - "url": "http://gitlab.com/test/project/issues/11", - "action": "update", - }, - } + payload = deepcopy(new_issue_base_payload) + payload["object_attributes"]["title"] = "test-title" + payload["object_attributes"]["description"] = "test-body" + payload["object_attributes"]["url"] = "http://gitlab.com/test/project/issues/11" + payload["object_attributes"]["action"] = "update" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -486,12 +772,12 @@ def test_issues_event_bad_issue(client): issue.project.default_priority = issue.priority issue.project.save() - payload = { - "object_kind": "issue", - "object_attributes": { - "action": "open", - }, - } + payload = deepcopy(new_issue_base_payload) + del payload["object_attributes"]["title"] + del payload["object_attributes"]["description"] + del payload["object_attributes"]["url"] + payload["object_attributes"]["action"] = "open" + payload["repository"]["homepage"] = "test" mail.outbox = [] ev_hook = event_hooks.IssuesEventHook(issue.project, payload) @@ -518,23 +804,13 @@ def test_issue_comment_event_on_existing_issue_task_and_us(client): us = f.UserStoryFactory.create(external_reference=["gitlab", "http://gitlab.com/test/project/issues/11"], owner=project.owner, project=project) take_snapshot(us, user=user) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "11", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - "note": "Test body", - }, - "repository": { - "homepage": "http://gitlab.com/test/project", - }, - } - + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "11" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + payload["object_attributes"]["note"] = "Test body" + payload["repository"]["homepage"] = "http://gitlab.com/test/project" mail.outbox = [] @@ -568,22 +844,13 @@ def test_issue_comment_event_on_not_existing_issue_task_and_us(client): us = f.UserStoryFactory.create(project=issue.project, external_reference=["gitlab", "10"]) take_snapshot(us, user=us.owner) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "99999", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - "note": "test comment", - }, - "repository": { - "homepage": "test", - }, - } + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "99999" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + payload["object_attributes"]["note"] = "test comment" + payload["repository"]["homepage"] = "test" mail.outbox = [] @@ -605,21 +872,14 @@ def test_issues_event_bad_comment(client): issue = f.IssueFactory.create(external_reference=["gitlab", "10"]) take_snapshot(issue, user=issue.owner) - payload = { - "user": { - "username": "test" - }, - "issue": { - "iid": "10", - "title": "test-title", - }, - "object_attributes": { - "noteable_type": "Issue", - }, - "repository": { - "homepage": "test", - }, - } + payload = deepcopy(issue_comment_base_payload) + payload["user"]["username"] = "test" + payload["issue"]["iid"] = "10" + payload["issue"]["title"] = "test-title" + payload["object_attributes"]["noteable_type"] = "Issue" + del payload["object_attributes"]["note"] + payload["repository"]["homepage"] = "test" + ev_hook = event_hooks.IssueCommentEventHook(issue.project, payload) mail.outbox = [] @@ -671,9 +931,10 @@ def test_api_patch_project_modules(client): def test_replace_gitlab_references(): - assert event_hooks.replace_gitlab_references("project-url", "#2") == "[GitLab#2](project-url/issues/2)" - assert event_hooks.replace_gitlab_references("project-url", "#2 ") == "[GitLab#2](project-url/issues/2) " - assert event_hooks.replace_gitlab_references("project-url", " #2 ") == " [GitLab#2](project-url/issues/2) " - assert event_hooks.replace_gitlab_references("project-url", " #2") == " [GitLab#2](project-url/issues/2)" - assert event_hooks.replace_gitlab_references("project-url", "#test") == "#test" - assert event_hooks.replace_gitlab_references("project-url", None) == "" + ev_hook = event_hooks.BaseGitLabEventHook + assert ev_hook.replace_gitlab_references(None, "project-url", "#2") == "[GitLab#2](project-url/issues/2)" + assert ev_hook.replace_gitlab_references(None, "project-url", "#2 ") == "[GitLab#2](project-url/issues/2) " + assert ev_hook.replace_gitlab_references(None, "project-url", " #2 ") == " [GitLab#2](project-url/issues/2) " + assert ev_hook.replace_gitlab_references(None, "project-url", " #2") == " [GitLab#2](project-url/issues/2)" + assert ev_hook.replace_gitlab_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_gitlab_references(None, "project-url", None) == "" diff --git a/tests/integration/test_hooks_gogs.py b/tests/integration/test_hooks_gogs.py new file mode 100644 index 00000000..290bc3f8 --- /dev/null +++ b/tests/integration/test_hooks_gogs.py @@ -0,0 +1,502 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# Copyright (C) 2014-2016 Anler Hernández +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import pytest + +from unittest import mock + +from django.core.urlresolvers import reverse +from django.core import mail + +from taiga.base.utils import json +from taiga.hooks.gogs import event_hooks +from taiga.hooks.gogs.api import GogsViewSet +from taiga.hooks.exceptions import ActionSyntaxException +from taiga.projects import choices as project_choices +from taiga.projects.issues.models import Issue +from taiga.projects.tasks.models import Task +from taiga.projects.userstories.models import UserStory +from taiga.projects.models import Membership +from taiga.projects.history.services import get_history_queryset_by_model_instance, take_snapshot +from taiga.projects.notifications.choices import NotifyLevel +from taiga.projects.notifications.models import NotifyPolicy +from taiga.projects import services +from .. import factories as f + +pytestmark = pytest.mark.django_db + + +def test_bad_signature(client): + project = f.ProjectFactory() + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = { + "secret": "badbadbad" + } + response = client.post(url, json.dumps(data), + content_type="application/json") + response_content = response.data + assert response.status_code == 400 + assert "Bad signature" in response_content["_error_message"] + + +def test_ok_signature(client): + project = f.ProjectFactory() + f.ProjectModulesConfigFactory(project=project, config={ + "gogs": { + "secret": "tpnIwJDz4e" + } + }) + + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = {"test:": "data", "secret": "tpnIwJDz4e"} + response = client.post(url, json.dumps(data), + content_type="application/json") + + assert response.status_code == 204 + + +def test_blocked_project(client): + project = f.ProjectFactory(blocked_code=project_choices.BLOCKED_BY_STAFF) + f.ProjectModulesConfigFactory(project=project, config={ + "gogs": { + "secret": "tpnIwJDz4e" + } + }) + + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = {"test:": "data", "secret": "tpnIwJDz4e"} + response = client.post(url, json.dumps(data), + content_type="application/json") + + assert response.status_code == 451 + + +def test_push_event_detected(client): + project = f.ProjectFactory() + url = reverse("gogs-hook-list") + url = "%s?project=%s" % (url, project.id) + data = { + "commits": [ + { + "message": "test message", + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + GogsViewSet._validate_signature = mock.Mock(return_value=True) + + with mock.patch.object(event_hooks.PushEventHook, "process_event") as process_event_mock: + response = client.post(url, json.dumps(data), + HTTP_X_GITHUB_EVENT="push", + content_type="application/json") + + assert process_event_mock.call_count == 1 + + assert response.status_code == 204 + + +def test_push_event_issue_processing(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.IssueStatusFactory(project=creation_status.project) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (issue.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue = Issue.objects.get(id=issue.id) + assert issue.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_task_processing(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.TaskStatusFactory(project=creation_status.project) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (task.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task = Task.objects.get(id=task.id) + assert task.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_processing(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.UserStoryStatusFactory(project=creation_status.project) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + bye! + """ % (user_story.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + user_story = UserStory.objects.get(id=user_story.id) + assert user_story.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_issue_mention(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + issue = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(issue, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (issue.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(issue.project, payload) + ev_hook.process_event() + issue_history = get_history_queryset_by_model_instance(issue) + assert issue_history.count() == 1 + assert issue_history[0].comment.startswith("This issue has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_task_mention(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(task, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (task.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task_history = get_history_queryset_by_model_instance(task) + assert task_history.count() == 1 + assert task_history[0].comment.startswith("This task has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_user_story_mention(client): + creation_status = f.UserStoryStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_us"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + user_story = f.UserStoryFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + take_snapshot(user_story, user=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s ok + bye! + """ % (user_story.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + ev_hook.process_event() + us_history = get_history_queryset_by_model_instance(user_story) + assert us_history.count() == 1 + assert us_history[0].comment.startswith("This user story has been mentioned by") + assert len(mail.outbox) == 1 + + +def test_push_event_multiple_actions(client): + creation_status = f.IssueStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_issues"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.IssueStatusFactory(project=creation_status.project) + issue1 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + issue2 = f.IssueFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test TG-%s #%s ok + test TG-%s #%s ok + bye! + """ % (issue1.ref, new_status.slug, issue2.ref, new_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook1 = event_hooks.PushEventHook(issue1.project, payload) + ev_hook1.process_event() + issue1 = Issue.objects.get(id=issue1.id) + issue2 = Issue.objects.get(id=issue2.id) + assert issue1.status.id == new_status.id + assert issue2.status.id == new_status.id + assert len(mail.outbox) == 2 + + +def test_push_event_processing_case_insensitive(client): + creation_status = f.TaskStatusFactory() + role = f.RoleFactory(project=creation_status.project, permissions=["view_tasks"]) + f.MembershipFactory(project=creation_status.project, role=role, user=creation_status.project.owner) + new_status = f.TaskStatusFactory(project=creation_status.project) + task = f.TaskFactory.create(status=creation_status, project=creation_status.project, owner=creation_status.project.owner) + payload = { + "commits": [ + { + "message": """test message + test tg-%s #%s ok + bye! + """ % (task.ref, new_status.slug.upper()), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + ev_hook = event_hooks.PushEventHook(task.project, payload) + ev_hook.process_event() + task = Task.objects.get(id=task.id) + assert task.status.id == new_status.id + assert len(mail.outbox) == 1 + + +def test_push_event_task_bad_processing_non_existing_ref(client): + issue_status = f.IssueStatusFactory() + payload = { + "commits": [ + { + "message": """test message + test TG-6666666 #%s ok + bye! + """ % (issue_status.slug), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(issue_status.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The referenced element doesn't exist" + assert len(mail.outbox) == 0 + + +def test_push_event_us_bad_processing_non_existing_status(client): + user_story = f.UserStoryFactory.create() + payload = { + "commits": [ + { + "message": """test message + test TG-%s #non-existing-slug ok + bye! + """ % (user_story.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(user_story.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The status doesn't exist" + assert len(mail.outbox) == 0 + + +def test_push_event_bad_processing_non_existing_status(client): + issue = f.IssueFactory.create() + payload = { + "commits": [ + { + "message": """test message + test TG-%s #non-existing-slug ok + bye! + """ % (issue.ref), + "author": { + "username": "test", + }, + } + ], + "repository": { + "url": "http://test-url/test/project" + } + } + + mail.outbox = [] + + ev_hook = event_hooks.PushEventHook(issue.project, payload) + with pytest.raises(ActionSyntaxException) as excinfo: + ev_hook.process_event() + + assert str(excinfo.value) == "The status doesn't exist" + assert len(mail.outbox) == 0 + + +def test_api_get_project_modules(client): + project = f.create_project() + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("projects-modules", args=(project.id,)) + + client.login(project.owner) + response = client.get(url) + assert response.status_code == 200 + content = response.data + assert "gogs" in content + assert content["gogs"]["secret"] != "" + assert content["gogs"]["webhooks_url"] != "" + + +def test_api_patch_project_modules(client): + project = f.create_project() + f.MembershipFactory(project=project, user=project.owner, is_admin=True) + + url = reverse("projects-modules", args=(project.id,)) + + client.login(project.owner) + data = { + "gogs": { + "secret": "test_secret", + "url": "test_url", + } + } + response = client.patch(url, json.dumps(data), content_type="application/json") + assert response.status_code == 204 + + config = services.get_modules_config(project).config + assert "gogs" in config + assert config["gogs"]["secret"] == "test_secret" + assert config["gogs"]["webhooks_url"] != "test_url" + + +def test_replace_gogs_references(): + ev_hook = event_hooks.BaseGogsEventHook + assert ev_hook.replace_gogs_references(None, "project-url", "#2") == "[Gogs#2](project-url/issues/2)" + assert ev_hook.replace_gogs_references(None, "project-url", "#2 ") == "[Gogs#2](project-url/issues/2) " + assert ev_hook.replace_gogs_references(None, "project-url", " #2 ") == " [Gogs#2](project-url/issues/2) " + assert ev_hook.replace_gogs_references(None, "project-url", " #2") == " [Gogs#2](project-url/issues/2)" + assert ev_hook.replace_gogs_references(None, "project-url", "#test") == "#test" + assert ev_hook.replace_gogs_references(None, "project-url", None) == ""