From 7273119cdae4e0db4fc57e96f67282b09452391f Mon Sep 17 00:00:00 2001 From: Alex Denham Date: Mon, 13 Nov 2023 09:17:14 +0000 Subject: [PATCH] mesh-12345 added endpoint to dynamically create new mailbox and added steps to reset mailbox to clear down memory --- README.md | 50 ++++++++++++++++++-------- src/mesh_sandbox/common/__init__.py | 2 +- src/mesh_sandbox/common/messaging.py | 4 +++ src/mesh_sandbox/handlers/admin.py | 13 +++++++ src/mesh_sandbox/routers/admin.py | 20 +++++++++++ src/mesh_sandbox/store/base.py | 4 +++ src/mesh_sandbox/store/canned_store.py | 3 ++ src/mesh_sandbox/store/memory_store.py | 18 ++++++++++ 8 files changed, 98 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 96be5c4..7ad0865 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,17 @@ -MESH Sandbox -=========== +# MESH Sandbox MESH sandbox for local testing of [NHS Digital's MESH API](https://digital.nhs.uk/developer/api-catalogue/message-exchange-for-social-care-and-health-api). -Installation ------------- - -Example use ------------ - -pip ---- +## Installation and example use +#### pip ```bash pip install mesh-sandbox STORE_MODE=file MAILBOXES_DATA_DIR=/tmp/mesh uvicorn mesh_sandbox.api:app --reload --port 8700 --workers=1 curl http://localhost:8700/health ``` -docker compose --------------- - +#### docker compose ```yaml version: '3.9' @@ -50,6 +41,35 @@ services: ``` -Guidance for contributors -------------------------- + +## Ways to use mesh-sandbox + +#### Store Mode + +Store mode is set using environment variable `STORE_MODE` + +Accepted parameters: + - **canned** - Read only mailboxes + - **memory** - Mailbox state persists only while instance is active. + - **file** - Mailbox state persists using files which are stored in location defined by environment variable `FILE_STORE_DIR` + +> Note: Initial state of mailboxes is defined in `src/mesh_sandbox/store/data` + +#### Authentication Mode + +Authentication mode is set using environment variable: `AUTH_MODE` + +Accepted parameters: + - **none** - No authentication against passwords + - **full** - Requires valid password and certificates + + +#### Admin endpoints +Admin endpoints that can be used for testing purposes: + +- Reset all mailboxes: `/admin/reset` +- Reset single mailbox: `/admin/reset/{mailbox_id}` +- Create new mailbox: `/admin/create/{mailbox_id}` + +## Guidance for contributors [contributing](CONTRIBUTING.md) diff --git a/src/mesh_sandbox/common/__init__.py b/src/mesh_sandbox/common/__init__.py index 65acca4..d1a16f3 100644 --- a/src/mesh_sandbox/common/__init__.py +++ b/src/mesh_sandbox/common/__init__.py @@ -49,7 +49,7 @@ def strtobool(val: Any) -> Optional[bool]: class EnvConfig: env: str = field(default="local") build_label: str = field(default="latest") - auth_mode: str = field(default="no_auth") + auth_mode: str = field(default="none") store_mode: str = field(default="canned") shared_key: str = field(default="Banana") mailboxes_dir: str = field(default="/tmp/mesh_store") diff --git a/src/mesh_sandbox/common/messaging.py b/src/mesh_sandbox/common/messaging.py index 1111e2c..36f56ef 100644 --- a/src/mesh_sandbox/common/messaging.py +++ b/src/mesh_sandbox/common/messaging.py @@ -303,6 +303,10 @@ async def reset(self): async def reset_mailbox(self, mailbox_id: str): await self.store.reset_mailbox(mailbox_id=mailbox_id) + @_IfNotReadonly() + async def create_mailbox(self, mailbox_id: str): + await self.store.create_mailbox(mailbox_id=mailbox_id) + async def get_chunk(self, message: Message, chunk_number: int) -> Optional[bytes]: return await self.store.get_chunk(message=message, chunk_number=chunk_number) diff --git a/src/mesh_sandbox/handlers/admin.py b/src/mesh_sandbox/handlers/admin.py index 72fbd4a..61dfdec 100644 --- a/src/mesh_sandbox/handlers/admin.py +++ b/src/mesh_sandbox/handlers/admin.py @@ -38,6 +38,19 @@ async def reset(self, mailbox_id: Optional[str] = None): await self.messaging.reset_mailbox(mailbox.mailbox_id) + async def create_mailbox(self, mailbox_id: str): + if self.messaging.readonly: + raise HTTPException( + status_code=status.HTTP_405_METHOD_NOT_ALLOWED, + detail="reset not supported for current store mode", + ) + + mailbox = await self.messaging.get_mailbox(mailbox_id, accessed=False) + if mailbox: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="mailbox already exists") + + await self.messaging.create_mailbox(mailbox_id) + async def create_report(self, request: CreateReportRequest, background_tasks: BackgroundTasks) -> Message: recipient = await self.messaging.get_mailbox(request.mailbox_id, accessed=False) if not recipient: diff --git a/src/mesh_sandbox/routers/admin.py b/src/mesh_sandbox/routers/admin.py index 4a5fd21..2c96952 100644 --- a/src/mesh_sandbox/routers/admin.py +++ b/src/mesh_sandbox/routers/admin.py @@ -74,6 +74,26 @@ async def reset_mailbox( return {"message": f"mailbox {mailbox_id} reset"} +@router.post( + "/admin/create/{mailbox_id}", + summary=f"Create new mailbox. {TESTING_ONLY}", + status_code=status.HTTP_200_OK, + response_model_exclude_none=True, +) +@router.post( + "/messageexchange/admin/create/{mailbox_id}", + status_code=status.HTTP_200_OK, + include_in_schema=False, + response_model_exclude_none=True, +) +async def create_mailbox( + mailbox_id: str = Depends(normalise_mailbox_id_path), + handler: AdminHandler = Depends(AdminHandler), +): + await handler.create_mailbox(mailbox_id) + return {"message": f"created new mailbox {mailbox_id}"} + + @router.post( "/messageexchange/admin/report", summary=f"Put a report messages into a particular inbox. {TESTING_ONLY}", diff --git a/src/mesh_sandbox/store/base.py b/src/mesh_sandbox/store/base.py index cfa096b..91f9aee 100644 --- a/src/mesh_sandbox/store/base.py +++ b/src/mesh_sandbox/store/base.py @@ -54,6 +54,10 @@ async def reset(self): async def reset_mailbox(self, mailbox_id: str): pass + @abstractmethod + async def create_mailbox(self, mailbox_id: str): + pass + @abstractmethod async def get_inbox_messages( self, mailbox_id: str, predicate: Optional[Callable[[Message], bool]] = None diff --git a/src/mesh_sandbox/store/canned_store.py b/src/mesh_sandbox/store/canned_store.py index aafcaab..c6f6f50 100644 --- a/src/mesh_sandbox/store/canned_store.py +++ b/src/mesh_sandbox/store/canned_store.py @@ -221,6 +221,9 @@ async def reset(self): async def reset_mailbox(self, mailbox_id: str): raise NotImplementedError + async def create_mailbox(self, mailbox_id: str): + raise NotImplementedError + async def get_inbox_messages( self, mailbox_id: str, predicate: Optional[Callable[[Message], bool]] = None ) -> list[Message]: diff --git a/src/mesh_sandbox/store/memory_store.py b/src/mesh_sandbox/store/memory_store.py index dca7d7c..4a3d0d8 100644 --- a/src/mesh_sandbox/store/memory_store.py +++ b/src/mesh_sandbox/store/memory_store.py @@ -3,6 +3,7 @@ from typing import Optional from ..common import EnvConfig +from ..models.mailbox import Mailbox from ..models.message import Message from .canned_store import CannedStore @@ -22,11 +23,28 @@ async def reset(self): super().initialise() async def reset_mailbox(self, mailbox_id: str): + all_message_ids = set() + for message in self.inboxes[mailbox_id]: + all_message_ids.add(message.message_id) + for message in self.outboxes[mailbox_id]: + all_message_ids.add(message.message_id) + for messages in self.local_ids[mailbox_id].values(): + for message in messages: + all_message_ids.add(message.message_id) + for message_id in all_message_ids: + del self.messages[message_id] + del self.chunks[message_id] self.inboxes[mailbox_id] = [] self.outboxes[mailbox_id] = [] self.local_ids[mailbox_id] = defaultdict(list) self.mailboxes[mailbox_id].inbox_count = 0 + async def create_mailbox(self, mailbox_id: str): + self.inboxes[mailbox_id] = [] + self.outboxes[mailbox_id] = [] + self.local_ids[mailbox_id] = defaultdict(list) + self.mailboxes[mailbox_id] = Mailbox(mailbox_id=mailbox_id, mailbox_name="Unknown", password="password") + async def add_to_outbox(self, message: Message): if not message.sender.mailbox_id: return