Skip to content
This repository has been archived by the owner on Jan 28, 2022. It is now read-only.

Commit

Permalink
Merge pull request #420 from Clariteia/0.0.19
Browse files Browse the repository at this point in the history
0.0.19
  • Loading branch information
Sergio García Prado authored Nov 3, 2021
2 parents 642a769 + 31ee851 commit 1b504ca
Show file tree
Hide file tree
Showing 16 changed files with 152 additions and 53 deletions.
6 changes: 6 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,9 @@ History

* Add `PeriodicTask`, `PeriodicTaskScheduler` and `PeriodicTaskSchedulerService`.
* Add `@enroute.periodic.event` decorator

0.0.19 (2021-11-03)
------------------

* Add `"user"` context variable to be accessible during `Request` handling (same as `Request.user`).
* Add support for `Request.user` propagation over `CommandBroker`.
3 changes: 2 additions & 1 deletion minos/networks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.0.18"
__version__ = "0.0.19"

from .brokers import (
Broker,
Expand Down Expand Up @@ -60,6 +60,7 @@
HandlerSetup,
)
from .messages import (
USER_CONTEXT_VAR,
Request,
Response,
ResponseException,
Expand Down
14 changes: 12 additions & 2 deletions minos/networks/brokers/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,27 @@ def _from_config(cls, *args, config: MinosConfig, **kwargs) -> CommandBroker:
return cls(*args, **config.broker.queue._asdict(), default_reply_topic=default_reply_topic, **kwargs)

# noinspection PyMethodOverriding
async def send(self, data: Any, topic: str, saga: UUID, reply_topic: Optional[str] = None, **kwargs) -> int:
async def send(
self,
data: Any,
topic: str,
saga: UUID,
reply_topic: Optional[str] = None,
user: Optional[UUID] = None,
**kwargs,
) -> int:
"""Send a ``Command``.
:param data: The data to be send.
:param topic: Topic in which the message will be published.
:param saga: Saga identifier.
:param reply_topic: Topic name in which the reply will be published.
:param user: Optional user identifier. If the value is not `None` then the command is authenticated, otherwise
the command is not authenticated.
:return: This method does not return anything.
"""
if reply_topic is None:
reply_topic = self.default_reply_topic
command = Command(topic, data, saga, reply_topic)
command = Command(topic, data, saga, reply_topic, user)
logger.info(f"Sending '{command!s}'...")
return await self.enqueue(command.topic, command.avro_bytes)
7 changes: 6 additions & 1 deletion minos/networks/handlers/commands/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
EnrouteBuilder,
)
from ...messages import (
USER_CONTEXT_VAR,
Response,
ResponseException,
)
Expand Down Expand Up @@ -95,8 +96,10 @@ def get_callback(
"""

async def _fn(command: Command) -> Tuple[Any, CommandStatus]:
request = HandlerRequest(command)
token = USER_CONTEXT_VAR.set(request.user)

try:
request = HandlerRequest(command)
response = fn(request)
if isawaitable(response):
response = await response
Expand All @@ -109,5 +112,7 @@ async def _fn(command: Command) -> Tuple[Any, CommandStatus]:
except Exception as exc:
logger.exception(f"Raised a system exception: {exc!r}")
return repr(exc), CommandStatus.SYSTEM_ERROR
finally:
USER_CONTEXT_VAR.reset(token)

return _fn
7 changes: 7 additions & 0 deletions minos/networks/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
ABC,
abstractmethod,
)
from contextvars import (
ContextVar,
)
from inspect import (
isawaitable,
)
from typing import (
Any,
Callable,
Final,
Optional,
)
from uuid import (
Expand All @@ -26,6 +30,9 @@
MinosException,
)

USER_CONTEXT_VAR: Final[ContextVar[Optional[UUID]]] = ContextVar("user", default=None)
USER_CONTEXT_VAR.set(None) # needed to "register" the context variable.


class Request(ABC):
"""Request interface."""
Expand Down
4 changes: 4 additions & 0 deletions minos/networks/rest/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
EnrouteBuilder,
)
from ..messages import (
USER_CONTEXT_VAR,
Response,
ResponseException,
)
Expand Down Expand Up @@ -127,6 +128,7 @@ async def _fn(request: web.Request) -> web.Response:
logger.info(f"Dispatching '{request!s}' from '{request.remote!s}'...")

request = RestRequest(request)
token = USER_CONTEXT_VAR.set(request.user)

try:
response = fn(request)
Expand All @@ -143,6 +145,8 @@ async def _fn(request: web.Request) -> web.Response:
except Exception as exc:
logger.exception(f"Raised a system exception: {exc!r}")
raise web.HTTPInternalServerError()
finally:
USER_CONTEXT_VAR.reset(token)

return _fn

Expand Down
13 changes: 12 additions & 1 deletion minos/networks/rest/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,18 @@ def user(self) -> Optional[UUID]:
"""
Returns the UUID of the user making the Request.
"""
return UUID(self.raw_request.headers["User"])
if "User" not in self.headers:
return None
return UUID(self.headers["User"])

@property
def headers(self) -> dict[str, str]:
"""Get the headers of the request.
:return: A dictionary in which keys are ``str`` instances and values are ``str`` instances.
"""
# noinspection PyTypeChecker
return self.raw_request.headers

async def content(self, model_type: Union[ModelType, Type[Model], str] = "Content", **kwargs) -> Any:
"""Get the request content.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "minos_microservice_networks"
version = "0.0.18"
version = "0.0.19"
description = "Python Package with the common network classes and utilities used in Minos Microservice."
readme = "README.md"
repository = "https://github.com/clariteia/minos_microservice_network"
Expand Down
16 changes: 16 additions & 0 deletions tests/test_networks/test_brokers/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@ async def test_send_with_default_reply_topic(self):
self.assertEqual("fake", args[0])
self.assertEqual(Command("fake", FakeModel("foo"), saga, "OrderReply"), Command.from_avro_bytes(args[1]))

async def test_send_with_user(self):
mock = AsyncMock(return_value=56)
saga = uuid4()
user = uuid4()

async with CommandBroker.from_config(config=self.config) as broker:
broker.enqueue = mock
identifier = await broker.send(FakeModel("foo"), "fake", saga, "ekaf", user)

self.assertEqual(56, identifier)
self.assertEqual(1, mock.call_count)

args = mock.call_args.args
self.assertEqual("fake", args[0])
self.assertEqual(Command("fake", FakeModel("foo"), saga, "ekaf", user), Command.from_avro_bytes(args[1]))


if __name__ == "__main__":
unittest.main()
16 changes: 15 additions & 1 deletion tests/test_networks/test_handlers/test_commands/test_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
PostgresAsyncTestCase,
)
from minos.networks import (
USER_CONTEXT_VAR,
CommandHandler,
HandlerEntry,
HandlerRequest,
Expand Down Expand Up @@ -56,7 +57,8 @@ def setUp(self) -> None:
super().setUp()
self.broker = FakeBroker()
self.handler = CommandHandler.from_config(config=self.config, broker=self.broker)
self.command = Command("AddOrder", FakeModel("foo"), uuid4(), "UpdateTicket")
self.user = uuid4()
self.command = Command("AddOrder", FakeModel("foo"), self.user, "UpdateTicket")

def test_from_config(self):
broker = FakeBroker()
Expand Down Expand Up @@ -117,6 +119,18 @@ async def test_get_callback_raises_exception(self):
expected = (repr(ValueError()), CommandStatus.SYSTEM_ERROR)
self.assertEqual(expected, await fn(self.command))

async def test_get_callback_with_user(self):
async def _fn(request) -> None:
self.assertEqual(self.user, request.user)
self.assertEqual(self.user, USER_CONTEXT_VAR.get())

mock = AsyncMock(side_effect=_fn)

handler = self.handler.get_callback(mock)
await handler(self.command)

self.assertEqual(1, mock.call_count)


if __name__ == "__main__":
unittest.main()
5 changes: 1 addition & 4 deletions tests/test_networks/test_handlers/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ def setUp(self) -> None:

def test_repr(self):
request = HandlerRequest(self.command)
expected = (
"HandlerRequest(Command(topic=FooCreated, data=[FakeModel(text=foo), FakeModel(text=bar)], "
f"saga={self.saga!s}, reply_topic=AddOrderReply, user=None))"
)
expected = f"HandlerRequest({self.command!r})"
self.assertEqual(expected, repr(request))

def test_eq_true(self):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_networks/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ async def test_eq_false(self):

async def test_repr(self):
response = Response(self.data)
self.assertEqual("Response([FakeModel(text=blue), FakeModel(text=red)])", repr(response))
self.assertEqual(f"Response({self.data!r})", repr(response))

def test_hash(self):
self.assertIsInstance(hash(Response("test")), int)
Expand Down
38 changes: 24 additions & 14 deletions tests/test_networks/test_rest/test_handlers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import unittest
from unittest.mock import (
AsyncMock,
)
from uuid import (
uuid4,
)

from aiohttp import (
web,
Expand All @@ -7,20 +13,21 @@
HTTPBadRequest,
HTTPInternalServerError,
)
from yarl import (
URL,
)

from minos.common.testing import (
PostgresAsyncTestCase,
)
from minos.networks import (
USER_CONTEXT_VAR,
Request,
Response,
RestHandler,
RestResponse,
RestResponseException,
)
from tests.test_networks.test_rest.utils import (
MockedRequest,
)
from tests.utils import (
BASE_PATH,
)
Expand All @@ -44,17 +51,6 @@ async def _fn_raises_exception(request: Request) -> Response:
raise ValueError


class MockedRequest:
def __init__(self, data=None):
self.data = data
self.remote = "127.0.0.1"
self.rel_url = URL("localhost")
self.match_info = dict()

async def json(self):
return self.data


class TestRestHandler(PostgresAsyncTestCase):
CONFIG_FILE_PATH = BASE_PATH / "test_config.yml"

Expand Down Expand Up @@ -97,6 +93,20 @@ async def test_get_callback_raises_exception(self):
with self.assertRaises(HTTPInternalServerError):
await handler(MockedRequest({"foo": "bar"}))

async def test_get_callback_with_user(self):
user = uuid4()

async def _fn(request) -> None:
self.assertEqual(user, request.user)
self.assertEqual(user, USER_CONTEXT_VAR.get())

mock = AsyncMock(side_effect=_fn)

handler = self.handler.get_callback(mock)
await handler(MockedRequest({"foo": "bar"}, user=user))

self.assertEqual(1, mock.call_count)


if __name__ == "__main__":
unittest.main()
40 changes: 14 additions & 26 deletions tests/test_networks/test_rest/test_messages.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import unittest
import uuid
from json import (
JSONDecodeError,
)
from unittest.mock import (
PropertyMock,
patch,
)

from yarl import (
URL,
from uuid import (
uuid4,
)

from minos.common import (
Expand All @@ -19,28 +14,14 @@
RestRequest,
RestResponse,
)
from tests.test_networks.test_rest.test_handlers import (
MockedRequest,
)
from tests.utils import (
FakeModel,
)


class MockedRequest:
def __init__(self, data=None):
self.data = data
self.remote = "127.0.0.1"
self.rel_url = URL("localhost")
self.match_info = dict()
self.headers = {"User": str(uuid.uuid1())}

def __repr__(self):
return "repr"

async def json(self):
if self.data is None:
raise JSONDecodeError("", "", 1)
return self.data


class TestRestRequest(unittest.IsolatedAsyncioTestCase):
def test_raw_request(self):
raw_request = MockedRequest()
Expand All @@ -58,11 +39,18 @@ def test_eq_true(self):
def test_eq_false(self):
self.assertNotEqual(RestRequest(MockedRequest()), RestRequest(MockedRequest()))

def test_headers(self):
uuid = uuid4()
raw_request = MockedRequest(user=uuid)
request = RestRequest(raw_request)
self.assertEqual({"User": str(uuid), "something": "123"}, request.headers)

def test_user(self):
raw_request = MockedRequest()
uuid = uuid4()
raw_request = MockedRequest(user=uuid)
request = RestRequest(raw_request)
user = request.user
self.assertEqual(uuid.UUID(raw_request.headers["User"]), user)
self.assertEqual(uuid, user)

async def test_content(self):
Content = ModelType.build(
Expand Down
Loading

0 comments on commit 1b504ca

Please sign in to comment.