diff --git a/jbi/actions/default.py b/jbi/actions/default.py index c7e6c6d8..3a0ee577 100644 --- a/jbi/actions/default.py +++ b/jbi/actions/default.py @@ -7,9 +7,9 @@ import logging from jbi import ActionResult, Operation -from jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest from jbi.environment import get_settings from jbi.errors import ActionError +from jbi.models import BugzillaBug, BugzillaWebhookRequest from jbi.services import get_bugzilla, get_jira settings = get_settings() diff --git a/jbi/actions/default_with_assignee_and_status.py b/jbi/actions/default_with_assignee_and_status.py index 1e90cf00..76869e5a 100644 --- a/jbi/actions/default_with_assignee_and_status.py +++ b/jbi/actions/default_with_assignee_and_status.py @@ -14,7 +14,7 @@ JIRA_REQUIRED_PERMISSIONS as DEFAULT_JIRA_REQUIRED_PERMISSIONS, ) from jbi.actions.default import DefaultExecutor -from jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest +from jbi.models import BugzillaBug, BugzillaWebhookRequest logger = logging.getLogger(__name__) diff --git a/jbi/bugzilla.py b/jbi/bugzilla.py deleted file mode 100644 index 1388f31f..00000000 --- a/jbi/bugzilla.py +++ /dev/null @@ -1,296 +0,0 @@ -""" -Bugzilla Typed Objects for ease of use throughout JBI -View additional bugzilla webhook documentation here: https://bugzilla.mozilla.org/page.cgi?id=webhooks.html - -""" -import datetime -import json -import logging -from functools import cached_property -from typing import Dict, List, Optional -from urllib.parse import ParseResult, urlparse - -from pydantic import BaseModel # pylint: disable=no-name-in-module - -from jbi.errors import ActionNotFoundError -from jbi.models import Action, Actions -from jbi.services import get_bugzilla - -logger = logging.getLogger(__name__) - -JIRA_HOSTNAMES = ("jira", "atlassian") - - -class BugzillaWebhookUser(BaseModel): - """Bugzilla User Object""" - - id: int - login: str - real_name: str - - -class BugzillaWebhookEventChange(BaseModel): - """Bugzilla Change Object""" - - field: str - removed: str - added: str - - -class BugzillaWebhookEvent(BaseModel): - """Bugzilla Event Object""" - - action: str - time: Optional[datetime.datetime] - user: Optional[BugzillaWebhookUser] - changes: Optional[List[BugzillaWebhookEventChange]] - target: Optional[str] - routing_key: Optional[str] - - def changed_fields(self) -> Optional[List[str]]: - """Returns the names of changed fields in a bug""" - if self.changes: - return [c.field for c in self.changes] - - # Private bugs don't include the changes field in the event, but the - # field names are in the routing key. - if self.routing_key is not None and self.routing_key[0:11] == "bug.modify:": - return self.routing_key[11:].split(",") - - return None - - -class BugzillaWebhookAttachment(BaseModel): - """Bugzilla Attachment Object""" - - content_type: Optional[str] - creation_time: Optional[datetime.datetime] - description: Optional[str] - file_name: Optional[str] - flags: Optional[List] - id: int - is_obsolete: Optional[bool] - is_patch: Optional[bool] - is_private: Optional[bool] - last_change_time: Optional[datetime.datetime] - - -class BugzillaWebhookComment(BaseModel): - """Bugzilla Comment Object""" - - body: Optional[str] - id: Optional[int] - number: Optional[int] - is_private: Optional[bool] - creation_time: Optional[datetime.datetime] - - def is_comment_description(self) -> bool: - """Used to determine if `self` is a description or comment.""" - return self.number == 0 - - def is_comment_generic(self) -> bool: - """All comments after comment-0 are generic""" - is_description = self.is_comment_description() - return not is_description - - def is_private_comment(self) -> bool: - """Helper function to determine if this comment private--not accessible or open""" - return bool(self.is_private) - - -class BugzillaBug(BaseModel): - """Bugzilla Bug Object""" - - id: int - is_private: Optional[bool] - type: Optional[str] - product: Optional[str] - component: Optional[str] - whiteboard: Optional[str] - keywords: Optional[List] - flags: Optional[List] - groups: Optional[List] - status: Optional[str] - resolution: Optional[str] - see_also: Optional[List] - summary: Optional[str] - severity: Optional[str] - priority: Optional[str] - creator: Optional[str] - assigned_to: Optional[str] - comment: Optional[BugzillaWebhookComment] - - def get_whiteboard_as_list(self) -> List[str]: - """Convert string whiteboard into list, splitting on ']' and removing '['.""" - if self.whiteboard is not None: - split_list = self.whiteboard.replace("[", "").split("]") - return [x.strip() for x in split_list if x not in ["", " "]] - return [] - - def get_whiteboard_with_brackets_as_list(self) -> List[str]: - """Convert string whiteboard into list, splitting on ']' and removing '['; then re-adding.""" - wb_list = self.get_whiteboard_as_list() - if wb_list is not None and len(wb_list) > 0: - return [f"[{element}]" for element in wb_list] - return [] - - def get_jira_labels(self) -> List[str]: - """ - whiteboard labels are added as a convenience for users to search in jira; - bugzilla is an expected label in Jira - since jira-labels can't contain a " ", convert to "." - """ - wb_list = [wb.replace(" ", ".") for wb in self.get_whiteboard_as_list()] - wb_bracket_list = [ - wb.replace(" ", ".") for wb in self.get_whiteboard_with_brackets_as_list() - ] - - return ["bugzilla"] + wb_list + wb_bracket_list - - def get_potential_whiteboard_config_list(self) -> List[str]: - """Get all possible tags from `whiteboard` field""" - converted_list: List = [] - for whiteboard in self.get_whiteboard_as_list(): - first_tag = whiteboard.strip().lower().split(sep="-", maxsplit=1)[0] - if first_tag: - converted_list.append(first_tag) - - return converted_list - - def issue_type(self) -> str: - """Get the Jira issue type for this bug""" - type_map: dict = {"enhancement": "Task", "task": "Task", "defect": "Bug"} - return type_map.get(self.type, "Task") - - def map_as_jira_issue(self) -> Dict: - """Extract bug info as jira issue dictionary""" - return { - "summary": self.summary, - "labels": self.get_jira_labels(), - } - - def extract_from_see_also(self): - """Extract Jira Issue Key from see_also if jira url present""" - if not self.see_also and len(self.see_also) > 0: - return None - - for url in self.see_also: # pylint: disable=not-an-iterable - try: - parsed_url: ParseResult = urlparse(url=url) - host_parts = parsed_url.hostname.split(".") - except (ValueError, AttributeError): - logger.debug( - "Bug %s `see_also` is not a URL: %s", - self.id, - url, - extra={ - "bug": { - "id": self.id, - } - }, - ) - continue - - if any(part in JIRA_HOSTNAMES for part in host_parts): - parsed_jira_key = parsed_url.path.rstrip("/").split("/")[-1] - if parsed_jira_key: # URL ending with / - return parsed_jira_key - - return None - - def lookup_action(self, actions: Actions) -> Action: - """Find first matching action from bug's whiteboard list""" - tags: List[str] = self.get_potential_whiteboard_config_list() - for tag in tags: - if action := actions.get(tag): - return action - raise ActionNotFoundError(", ".join(tags)) - - -class BugzillaWebhookRequest(BaseModel): - """Bugzilla Webhook Request Object""" - - class Config: - """pydantic model config""" - - keep_untouched = ( - cached_property, - ) # https://github.com/samuelcolvin/pydantic/issues/1241 - - webhook_id: int - webhook_name: str - event: BugzillaWebhookEvent - bug: Optional[BugzillaBug] - - def map_as_jira_comment(self): - """Extract comment from Webhook Event""" - comment: BugzillaWebhookComment = self.bug.comment - commenter: BugzillaWebhookUser = self.event.user - comment_body: str = comment.body - - if comment.is_private: - bug_comments = get_bugzilla().get_comments([self.bug.id]) - comment_list = bug_comments["bugs"][str(self.bug.id)]["comments"] - matching_comments = [c for c in comment_list if c["id"] == comment.id] - if len(matching_comments) != 1: - return None - comment_body = matching_comments[0]["text"] - - body = f"*({commenter.login})* commented: \n{{quote}}{comment_body}{{quote}}" - return body - - def map_as_jira_description(self): - """Extract description as comment from Webhook Event""" - comment: BugzillaWebhookComment = self.bug.comment - comment_body: str = comment.body - body = f"*(description)*: \n{{quote}}{comment_body}{{quote}}" - return body - - def map_as_comments( - self, - status_log_enabled: bool = True, - assignee_log_enabled: bool = True, - ) -> List[str]: - """Extract update dict and comment list from Webhook Event""" - - comments: List = [] - bug: BugzillaBug = self.bug # type: ignore - - if self.event.changes: - user = self.event.user.login if self.event.user else "unknown" - for change in self.event.changes: - - if status_log_enabled and change.field in ["status", "resolution"]: - comments.append( - { - "modified by": user, - "resolution": bug.resolution, - "status": bug.status, - } - ) - - if assignee_log_enabled and change.field in ["assigned_to", "assignee"]: - comments.append({"assignee": bug.assigned_to}) - - return [json.dumps(comment, indent=4) for comment in comments] - - def getbug_as_bugzilla_object(self) -> BugzillaBug: - """Helper method to get up to date bug data from Request.bug.id in BugzillaBug format""" - current_bug_info = get_bugzilla().getbug(self.bug.id) # type: ignore - return BugzillaBug.parse_obj(current_bug_info.__dict__) - - @cached_property - def bugzilla_object(self) -> BugzillaBug: - """Returns the bugzilla bug object, querying the API as needed for private bugs""" - if not self.bug: - raise ValueError("missing bug reference") - if not self.bug.is_private: - return self.bug - return self.getbug_as_bugzilla_object() - - -class BugzillaApiResponse(BaseModel): - """Bugzilla Response Object""" - - faults: Optional[List] - bugs: Optional[List[BugzillaBug]] diff --git a/jbi/models.py b/jbi/models.py index 8a46be79..2d344aec 100644 --- a/jbi/models.py +++ b/jbi/models.py @@ -1,16 +1,27 @@ """ Python Module for Pydantic Models and validation """ +import datetime import functools import importlib +import json +import logging import warnings from inspect import signature from types import ModuleType from typing import Any, Callable, Dict, List, Literal, Mapping, Optional, Set, Union +from urllib.parse import ParseResult, urlparse -from pydantic import EmailStr, Field, PrivateAttr, root_validator, validator +from pydantic import BaseModel, EmailStr, Field, PrivateAttr, root_validator, validator from pydantic_yaml import YamlModel +from jbi.errors import ActionNotFoundError +from jbi.services import get_bugzilla + +logger = logging.getLogger(__name__) + +JIRA_HOSTNAMES = ("jira", "atlassian") + class Action(YamlModel): """ @@ -125,3 +136,265 @@ class Config: """Pydantic configuration""" keep_untouched = (functools.cached_property,) + + +class BugzillaWebhookUser(BaseModel): + """Bugzilla User Object""" + + id: int + login: str + real_name: str + + +class BugzillaWebhookEventChange(BaseModel): + """Bugzilla Change Object""" + + field: str + removed: str + added: str + + +class BugzillaWebhookEvent(BaseModel): + """Bugzilla Event Object""" + + action: str + time: Optional[datetime.datetime] + user: Optional[BugzillaWebhookUser] + changes: Optional[List[BugzillaWebhookEventChange]] + target: Optional[str] + routing_key: Optional[str] + + def changed_fields(self) -> Optional[List[str]]: + """Returns the names of changed fields in a bug""" + if self.changes: + return [c.field for c in self.changes] + + # Private bugs don't include the changes field in the event, but the + # field names are in the routing key. + if self.routing_key is not None and self.routing_key[0:11] == "bug.modify:": + return self.routing_key[11:].split(",") + + return None + + +class BugzillaWebhookAttachment(BaseModel): + """Bugzilla Attachment Object""" + + content_type: Optional[str] + creation_time: Optional[datetime.datetime] + description: Optional[str] + file_name: Optional[str] + flags: Optional[List] + id: int + is_obsolete: Optional[bool] + is_patch: Optional[bool] + is_private: Optional[bool] + last_change_time: Optional[datetime.datetime] + + +class BugzillaWebhookComment(BaseModel): + """Bugzilla Comment Object""" + + body: Optional[str] + id: Optional[int] + number: Optional[int] + is_private: Optional[bool] + creation_time: Optional[datetime.datetime] + + +class BugzillaBug(BaseModel): + """Bugzilla Bug Object""" + + id: int + is_private: Optional[bool] + type: Optional[str] + product: Optional[str] + component: Optional[str] + whiteboard: Optional[str] + keywords: Optional[List] + flags: Optional[List] + groups: Optional[List] + status: Optional[str] + resolution: Optional[str] + see_also: Optional[List] + summary: Optional[str] + severity: Optional[str] + priority: Optional[str] + creator: Optional[str] + assigned_to: Optional[str] + comment: Optional[BugzillaWebhookComment] + + def get_whiteboard_as_list(self) -> List[str]: + """Convert string whiteboard into list, splitting on ']' and removing '['.""" + if self.whiteboard is not None: + split_list = self.whiteboard.replace("[", "").split("]") + return [x.strip() for x in split_list if x not in ["", " "]] + return [] + + def get_whiteboard_with_brackets_as_list(self) -> List[str]: + """Convert string whiteboard into list, splitting on ']' and removing '['; then re-adding.""" + wb_list = self.get_whiteboard_as_list() + if wb_list is not None and len(wb_list) > 0: + return [f"[{element}]" for element in wb_list] + return [] + + def get_jira_labels(self) -> List[str]: + """ + whiteboard labels are added as a convenience for users to search in jira; + bugzilla is an expected label in Jira + since jira-labels can't contain a " ", convert to "." + """ + wb_list = [wb.replace(" ", ".") for wb in self.get_whiteboard_as_list()] + wb_bracket_list = [ + wb.replace(" ", ".") for wb in self.get_whiteboard_with_brackets_as_list() + ] + + return ["bugzilla"] + wb_list + wb_bracket_list + + def get_potential_whiteboard_config_list(self) -> List[str]: + """Get all possible tags from `whiteboard` field""" + converted_list: List = [] + for whiteboard in self.get_whiteboard_as_list(): + first_tag = whiteboard.strip().lower().split(sep="-", maxsplit=1)[0] + if first_tag: + converted_list.append(first_tag) + + return converted_list + + def issue_type(self) -> str: + """Get the Jira issue type for this bug""" + type_map: dict = {"enhancement": "Task", "task": "Task", "defect": "Bug"} + return type_map.get(self.type, "Task") + + def map_as_jira_issue(self) -> Dict: + """Extract bug info as jira issue dictionary""" + return { + "summary": self.summary, + "labels": self.get_jira_labels(), + } + + def extract_from_see_also(self): + """Extract Jira Issue Key from see_also if jira url present""" + if not self.see_also and len(self.see_also) > 0: + return None + + for url in self.see_also: # pylint: disable=not-an-iterable + try: + parsed_url: ParseResult = urlparse(url=url) + host_parts = parsed_url.hostname.split(".") + except (ValueError, AttributeError): + logger.debug( + "Bug %s `see_also` is not a URL: %s", + self.id, + url, + extra={ + "bug": { + "id": self.id, + } + }, + ) + continue + + if any(part in JIRA_HOSTNAMES for part in host_parts): + parsed_jira_key = parsed_url.path.rstrip("/").split("/")[-1] + if parsed_jira_key: # URL ending with / + return parsed_jira_key + + return None + + def lookup_action(self, actions: Actions) -> Action: + """Find first matching action from bug's whiteboard list""" + tags: List[str] = self.get_potential_whiteboard_config_list() + for tag in tags: + if action := actions.get(tag): + return action + raise ActionNotFoundError(", ".join(tags)) + + +class BugzillaWebhookRequest(BaseModel): + """Bugzilla Webhook Request Object""" + + class Config: + """pydantic model config""" + + keep_untouched = ( + functools.cached_property, + ) # https://github.com/samuelcolvin/pydantic/issues/1241 + + webhook_id: int + webhook_name: str + event: BugzillaWebhookEvent + bug: Optional[BugzillaBug] + + def map_as_jira_comment(self): + """Extract comment from Webhook Event""" + comment: BugzillaWebhookComment = self.bug.comment + commenter: BugzillaWebhookUser = self.event.user + comment_body: str = comment.body + + if comment.is_private: + bug_comments = get_bugzilla().get_comments([self.bug.id]) + comment_list = bug_comments["bugs"][str(self.bug.id)]["comments"] + matching_comments = [c for c in comment_list if c["id"] == comment.id] + if len(matching_comments) != 1: + return None + comment_body = matching_comments[0]["text"] + + body = f"*({commenter.login})* commented: \n{{quote}}{comment_body}{{quote}}" + return body + + def map_as_jira_description(self): + """Extract description as comment from Webhook Event""" + comment: BugzillaWebhookComment = self.bug.comment + comment_body: str = comment.body + body = f"*(description)*: \n{{quote}}{comment_body}{{quote}}" + return body + + def map_as_comments( + self, + status_log_enabled: bool = True, + assignee_log_enabled: bool = True, + ) -> List[str]: + """Extract update dict and comment list from Webhook Event""" + + comments: List = [] + bug: BugzillaBug = self.bug # type: ignore + + if self.event.changes: + user = self.event.user.login if self.event.user else "unknown" + for change in self.event.changes: + + if status_log_enabled and change.field in ["status", "resolution"]: + comments.append( + { + "modified by": user, + "resolution": bug.resolution, + "status": bug.status, + } + ) + + if assignee_log_enabled and change.field in ["assigned_to", "assignee"]: + comments.append({"assignee": bug.assigned_to}) + + return [json.dumps(comment, indent=4) for comment in comments] + + def getbug_as_bugzilla_object(self) -> BugzillaBug: + """Helper method to get up to date bug data from Request.bug.id in BugzillaBug format""" + current_bug_info = get_bugzilla().getbug(self.bug.id) # type: ignore + return BugzillaBug.parse_obj(current_bug_info.__dict__) + + @functools.cached_property + def bugzilla_object(self) -> BugzillaBug: + """Returns the bugzilla bug object, querying the API as needed for private bugs""" + if not self.bug: + raise ValueError("missing bug reference") + if not self.bug.is_private: + return self.bug + return self.getbug_as_bugzilla_object() + + +class BugzillaApiResponse(BaseModel): + """Bugzilla Response Object""" + + faults: Optional[List] + bugs: Optional[List[BugzillaBug]] diff --git a/jbi/router.py b/jbi/router.py index 0465131c..a7f8f874 100644 --- a/jbi/router.py +++ b/jbi/router.py @@ -9,10 +9,9 @@ from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates -from jbi.bugzilla import BugzillaWebhookRequest from jbi.configuration import get_actions from jbi.environment import Settings, get_settings, get_version -from jbi.models import Actions +from jbi.models import Actions, BugzillaWebhookRequest from jbi.runner import IgnoreInvalidRequestError, execute_action from jbi.services import jbi_service_health_map, jira_visible_projects diff --git a/jbi/runner.py b/jbi/runner.py index 50923b18..c41ea685 100644 --- a/jbi/runner.py +++ b/jbi/runner.py @@ -6,10 +6,9 @@ from statsd.defaults.env import statsd from jbi import Operation -from jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest from jbi.environment import Settings from jbi.errors import ActionNotFoundError, IgnoreInvalidRequestError -from jbi.models import Actions +from jbi.models import Actions, BugzillaBug, BugzillaWebhookRequest logger = logging.getLogger(__name__) diff --git a/jbi/services.py b/jbi/services.py index fa611c4a..15e60538 100644 --- a/jbi/services.py +++ b/jbi/services.py @@ -1,7 +1,9 @@ """Services and functions that can be used to create custom actions""" +from __future__ import annotations + import concurrent.futures import logging -from typing import Dict, List +from typing import TYPE_CHECKING, Dict, List import backoff import bugzilla as rh_bugzilla @@ -9,7 +11,9 @@ from statsd.defaults.env import statsd from jbi import environment -from jbi.models import Actions + +if TYPE_CHECKING: + from jbi.models import Actions settings = environment.get_settings() diff --git a/tests/conftest.py b/tests/conftest.py index 1ffeaa58..fd703a25 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,9 +8,8 @@ from fastapi.testclient import TestClient from jbi.app import app -from jbi.bugzilla import BugzillaWebhookComment, BugzillaWebhookRequest from jbi.environment import Settings -from jbi.models import Action, Actions +from jbi.models import Action, Actions, BugzillaWebhookComment, BugzillaWebhookRequest @pytest.fixture diff --git a/tests/unit/actions/test_default.py b/tests/unit/actions/test_default.py index 0d5b142d..c1dc91e5 100644 --- a/tests/unit/actions/test_default.py +++ b/tests/unit/actions/test_default.py @@ -9,8 +9,8 @@ from jbi import Operation from jbi.actions import default -from jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest from jbi.errors import ActionError +from jbi.models import BugzillaBug, BugzillaWebhookRequest def test_default_invalid_init(): diff --git a/tests/unit/actions/test_default_with_assignee_and_status.py b/tests/unit/actions/test_default_with_assignee_and_status.py index 8824b74b..5201d418 100644 --- a/tests/unit/actions/test_default_with_assignee_and_status.py +++ b/tests/unit/actions/test_default_with_assignee_and_status.py @@ -1,15 +1,4 @@ -""" -Module for testing jbi/actions/extended.py functionality -""" -# pylint: disable=cannot-enumerate-pytest-fixtures -from unittest import mock -from unittest.mock import MagicMock - -import pytest - -from jbi import Operation from jbi.actions import default_with_assignee_and_status as action -from jbi.bugzilla import BugzillaBug, BugzillaWebhookRequest def test_create_with_no_assignee(webhook_create_example, mocked_jira): diff --git a/tests/unit/test_bugzilla.py b/tests/unit/test_bugzilla.py index 3c56a2d0..4debe6f1 100644 --- a/tests/unit/test_bugzilla.py +++ b/tests/unit/test_bugzilla.py @@ -4,8 +4,8 @@ # pylint: disable=cannot-enumerate-pytest-fixtures import pytest -from jbi import bugzilla from jbi.errors import ActionNotFoundError +from jbi.models import BugzillaBug @pytest.mark.parametrize( @@ -29,7 +29,7 @@ ], ) def test_jira_labels(whiteboard, expected): - bug = bugzilla.BugzillaBug(id=0, whiteboard=whiteboard) + bug = BugzillaBug(id=0, whiteboard=whiteboard) assert bug.get_jira_labels() == expected @@ -40,7 +40,7 @@ def test_jira_labels(whiteboard, expected): (["foo"], None), (["fail:/format"], None), (["foo", "http://jira.net/123"], "123"), - (["http://bugzilla.org/123"], None), + (["http://org/123"], None), (["http://jira.com"], None), (["http://mozilla.jira.com/"], None), (["http://mozilla.jira.com/123"], "123"), @@ -51,32 +51,24 @@ def test_jira_labels(whiteboard, expected): ], ) def test_extract_see_also(see_also, expected): - test_bug = bugzilla.BugzillaBug(id=0, see_also=see_also) + test_bug = BugzillaBug(id=0, see_also=see_also) assert test_bug.extract_from_see_also() == expected def test_lookup_action(actions_example): - bug = bugzilla.BugzillaBug.parse_obj( - {"id": 1234, "whiteboard": "[example][DevTest]"} - ) + bug = BugzillaBug.parse_obj({"id": 1234, "whiteboard": "[example][DevTest]"}) action = bug.lookup_action(actions_example) assert action.whiteboard_tag == "devtest" assert "test config" in action.description def test_lookup_action_missing(actions_example): - bug = bugzilla.BugzillaBug.parse_obj({"id": 1234, "whiteboard": "example DevTest"}) + bug = BugzillaBug.parse_obj({"id": 1234, "whiteboard": "example DevTest"}) with pytest.raises(ActionNotFoundError) as exc_info: bug.lookup_action(actions_example) assert str(exc_info.value) == "example devtest" -def test_comment(webhook_comment_example): - assert not webhook_comment_example.bug.comment.is_comment_description() - assert webhook_comment_example.bug.comment.is_comment_generic() - assert not webhook_comment_example.bug.comment.is_private_comment() - - def test_map_jira_description(webhook_comment_example): desc = webhook_comment_example.map_as_jira_description() assert desc == "*(description)*: \n{quote}hello{quote}" diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 9a20afab..03f6f72b 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -5,7 +5,7 @@ from fastapi.testclient import TestClient from jbi.app import app -from jbi.bugzilla import BugzillaWebhookRequest +from jbi.models import BugzillaWebhookRequest def test_request_summary_is_logged(caplog): diff --git a/tests/unit/test_router.py b/tests/unit/test_router.py index ac3d949f..23678806 100644 --- a/tests/unit/test_router.py +++ b/tests/unit/test_router.py @@ -5,7 +5,7 @@ from fastapi.testclient import TestClient from jbi.app import app -from jbi.bugzilla import BugzillaWebhookRequest +from jbi.models import BugzillaWebhookRequest def test_read_root(anon_client): diff --git a/tests/unit/test_runner.py b/tests/unit/test_runner.py index 09962d06..85410124 100644 --- a/tests/unit/test_runner.py +++ b/tests/unit/test_runner.py @@ -9,10 +9,9 @@ import pytest from jbi import Operation -from jbi.bugzilla import BugzillaWebhookRequest from jbi.environment import Settings from jbi.errors import IgnoreInvalidRequestError -from jbi.models import Action, Actions +from jbi.models import Action, Actions, BugzillaWebhookRequest from jbi.runner import execute_action