Skip to content
Open
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
50 changes: 35 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion src/mesh_sandbox/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed when trying to get this to work that I was having issues with authentication even though it looked like no_auth was set. I think no_auth doesn't match anything in the repos and so instead will apply full authentication with this set.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Alex ... I think the only question is if auth_mode=none should be the default .. would it be better to default to "full" .. so it will be a concious choice to ignore the authentication, rather than build a client expecting there to be no auth and get a nasty surprise when you try and move this into production?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah agreed that the default being set to full might make sense, I just went with what I thought the intended effect of no_auth was

store_mode: str = field(default="canned")
shared_key: str = field(default="Banana")
mailboxes_dir: str = field(default="/tmp/mesh_store")
Expand Down
4 changes: 4 additions & 0 deletions src/mesh_sandbox/common/messaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 13 additions & 0 deletions src/mesh_sandbox/handlers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 20 additions & 0 deletions src/mesh_sandbox/routers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
4 changes: 4 additions & 0 deletions src/mesh_sandbox/store/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/mesh_sandbox/store/canned_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
18 changes: 18 additions & 0 deletions src/mesh_sandbox/store/memory_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down