Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions libzapi/application/commands/ticketing/bookmark_cmds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from dataclasses import dataclass


@dataclass(frozen=True, slots=True)
class CreateBookmarkCmd:
ticket_id: int
2 changes: 2 additions & 0 deletions libzapi/application/services/ticketing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from libzapi.application.services.ticketing.attachments_service import AttachmentsService
from libzapi.application.services.ticketing.automations_service import AutomationsService
from libzapi.application.services.ticketing.brand_agents_service import BrandAgentsService
from libzapi.application.services.ticketing.bookmarks_service import BookmarksService
from libzapi.application.services.ticketing.brands_service import BrandsService
from libzapi.application.services.ticketing.custom_ticket_statuses_service import (
CustomTicketStatusesService,
Expand Down Expand Up @@ -81,6 +82,7 @@ def __init__(
self.automations = AutomationsService(api.AutomationApiClient(http))
self.brands = BrandsService(api.BrandApiClient(http))
self.brand_agents = BrandAgentsService(api.BrandAgentApiClient(http))
self.bookmarks = BookmarksService(api.BookmarkApiClient(http))
self.custom_ticket_statuses = CustomTicketStatusesService(
api.CustomTicketStatusApiClient(http)
)
Expand Down
23 changes: 23 additions & 0 deletions libzapi/application/services/ticketing/bookmarks_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

from typing import Iterable

from libzapi.application.commands.ticketing.bookmark_cmds import CreateBookmarkCmd
from libzapi.domain.models.ticketing.bookmark import Bookmark
from libzapi.infrastructure.api_clients.ticketing.bookmark_api_client import (
BookmarkApiClient,
)


class BookmarksService:
def __init__(self, client: BookmarkApiClient) -> None:
self._client = client

def list_all(self) -> Iterable[Bookmark]:
return self._client.list()

def create(self, ticket_id: int) -> Bookmark:
return self._client.create(entity=CreateBookmarkCmd(ticket_id=ticket_id))

def delete(self, bookmark_id: int) -> None:
self._client.delete(bookmark_id=bookmark_id)
18 changes: 18 additions & 0 deletions libzapi/domain/models/ticketing/bookmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

from libzapi.domain.shared_objects.logical_key import LogicalKey


@dataclass(frozen=True, slots=True)
class Bookmark:
id: int
url: Optional[str] = None
ticket: Optional[dict] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None

@property
def logical_key(self) -> LogicalKey:
return LogicalKey("bookmark", f"bookmark_id_{self.id}")
2 changes: 2 additions & 0 deletions libzapi/infrastructure/api_clients/ticketing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from libzapi.infrastructure.api_clients.ticketing.attachment_api_client import AttachmentApiClient
from libzapi.infrastructure.api_clients.ticketing.automation_api_client import AutomationApiClient
from libzapi.infrastructure.api_clients.ticketing.brand_api_client import BrandApiClient
from libzapi.infrastructure.api_clients.ticketing.bookmark_api_client import BookmarkApiClient
from libzapi.infrastructure.api_clients.ticketing.dynamic_content_api_client import (
DynamicContentApiClient,
)
Expand Down Expand Up @@ -65,6 +66,7 @@
"AccountSettingsApiClient",
"AttachmentApiClient",
"AutomationApiClient",
"BookmarkApiClient",
"BrandApiClient",
"BrandAgentApiClient",
"CustomTicketStatusApiClient",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

from typing import Iterator

from libzapi.application.commands.ticketing.bookmark_cmds import CreateBookmarkCmd
from libzapi.domain.models.ticketing.bookmark import Bookmark
from libzapi.infrastructure.http.client import HttpClient
from libzapi.infrastructure.http.pagination import yield_items
from libzapi.infrastructure.mappers.ticketing.bookmark_mapper import to_payload_create
from libzapi.infrastructure.serialization.parse import to_domain


class BookmarkApiClient:
"""HTTP adapter for Zendesk Bookmarks (current user's bookmarks)."""

def __init__(self, http: HttpClient) -> None:
self._http = http

def list(self) -> Iterator[Bookmark]:
for obj in yield_items(
get_json=self._http.get,
first_path="/api/v2/bookmarks",
base_url=self._http.base_url,
items_key="bookmarks",
):
yield to_domain(data=obj, cls=Bookmark)

def create(self, entity: CreateBookmarkCmd) -> Bookmark:
data = self._http.post("/api/v2/bookmarks", to_payload_create(entity))
return to_domain(data=data["bookmark"], cls=Bookmark)

def delete(self, bookmark_id: int) -> None:
self._http.delete(f"/api/v2/bookmarks/{int(bookmark_id)}")
7 changes: 7 additions & 0 deletions libzapi/infrastructure/mappers/ticketing/bookmark_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from __future__ import annotations

from libzapi.application.commands.ticketing.bookmark_cmds import CreateBookmarkCmd


def to_payload_create(cmd: CreateBookmarkCmd) -> dict:
return {"bookmark": {"ticket_id": int(cmd.ticket_id)}}
8 changes: 8 additions & 0 deletions tests/integration/ticketing/test_bookmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import itertools

from libzapi import Ticketing


def test_list_all(ticketing: Ticketing):
items = list(itertools.islice(ticketing.bookmarks.list_all(), 20))
assert isinstance(items, list)
62 changes: 62 additions & 0 deletions tests/unit/ticketing/test_bookmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import pytest

from libzapi.application.commands.ticketing.bookmark_cmds import CreateBookmarkCmd
from libzapi.domain.errors import NotFound, RateLimited, Unauthorized
from libzapi.infrastructure.api_clients.ticketing import BookmarkApiClient


@pytest.fixture
def http(mocker):
m = mocker.Mock()
m.base_url = "https://example.zendesk.com"
return m


@pytest.fixture
def domain(mocker):
return mocker.patch(
"libzapi.infrastructure.api_clients.ticketing.bookmark_api_client.to_domain",
side_effect=lambda data, cls: {"_cls": cls.__name__, **(data or {})},
)


def test_list_yields_items(http, domain):
http.get.return_value = {
"bookmarks": [{"id": 1}, {"id": 2}],
"meta": {"has_more": False},
"links": {"next": None},
}
client = BookmarkApiClient(http)
items = list(client.list())
assert len(items) == 2
assert all(i["_cls"] == "Bookmark" for i in items)
http.get.assert_called_with("/api/v2/bookmarks")


def test_create_posts_payload(http, domain):
http.post.return_value = {"bookmark": {"id": 1}}
client = BookmarkApiClient(http)
result = client.create(CreateBookmarkCmd(ticket_id=42))
assert result["_cls"] == "Bookmark"
http.post.assert_called_with("/api/v2/bookmarks", {"bookmark": {"ticket_id": 42}})


def test_delete(http):
client = BookmarkApiClient(http)
client.delete(5)
http.delete.assert_called_with("/api/v2/bookmarks/5")


@pytest.mark.parametrize("error_cls", [Unauthorized, NotFound, RateLimited])
def test_raises_on_http_error(error_cls, http):
http.get.side_effect = error_cls("error")
client = BookmarkApiClient(http)
with pytest.raises(error_cls):
list(client.list())


def test_bookmark_logical_key():
from libzapi.domain.models.ticketing.bookmark import Bookmark

bookmark = Bookmark(id=7)
assert bookmark.logical_key.as_str() == "bookmark:bookmark_id_7"
14 changes: 14 additions & 0 deletions tests/unit/ticketing/test_bookmark_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from libzapi.application.commands.ticketing.bookmark_cmds import CreateBookmarkCmd
from libzapi.infrastructure.mappers.ticketing.bookmark_mapper import to_payload_create


def test_create_payload():
assert to_payload_create(CreateBookmarkCmd(ticket_id=42)) == {
"bookmark": {"ticket_id": 42}
}


def test_create_coerces_ticket_id():
assert to_payload_create(CreateBookmarkCmd(ticket_id="7")) == { # type: ignore[arg-type]
"bookmark": {"ticket_id": 7}
}
40 changes: 40 additions & 0 deletions tests/unit/ticketing/test_bookmarks_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import pytest
from unittest.mock import Mock, sentinel

from libzapi.application.commands.ticketing.bookmark_cmds import CreateBookmarkCmd
from libzapi.application.services.ticketing.bookmarks_service import BookmarksService
from libzapi.domain.errors import Unauthorized


def _make_service(client=None):
client = client or Mock()
return BookmarksService(client), client


class TestDelegation:
def test_list_all_delegates(self):
service, client = _make_service()
client.list.return_value = sentinel.items
assert service.list_all() is sentinel.items
client.list.assert_called_once_with()

def test_create_builds_cmd(self):
service, client = _make_service()
client.create.return_value = sentinel.bookmark
result = service.create(42)
cmd = client.create.call_args.kwargs["entity"]
assert isinstance(cmd, CreateBookmarkCmd)
assert cmd.ticket_id == 42
assert result is sentinel.bookmark

def test_delete_delegates(self):
service, client = _make_service()
service.delete(5)
client.delete.assert_called_once_with(bookmark_id=5)


def test_propagates_unauthorized():
service, client = _make_service()
client.list.side_effect = Unauthorized("x")
with pytest.raises(Unauthorized):
service.list_all()
Loading