add message sent function + upd test cases

master
Ernest Litvinenko 2024-05-17 16:09:50 +03:00
parent 23d1a20459
commit 5ce8c9026c
17 changed files with 296 additions and 17 deletions

View File

@ -1,9 +1,7 @@
from fastapi import APIRouter, Depends, Response from fastapi import APIRouter, Depends, Response, Path
from core.helpers.auth.helpers import get_current_user from core.helpers.auth.helpers import get_current_user
from core.models.message.db import MPProfile from core.models.message.db import MPProfile
from core.services import chat_service from core.services import chat_service
from core.models.message.requests import CreateChatRequest from core.models.message.requests import CreateChatRequest
router = APIRouter(prefix='/chat', tags=['chat']) router = APIRouter(prefix='/chat', tags=['chat'])
@ -13,3 +11,13 @@ router = APIRouter(prefix='/chat', tags=['chat'])
async def create_chat(response: Response, chat: CreateChatRequest, user: MPProfile = Depends(get_current_user)): async def create_chat(response: Response, chat: CreateChatRequest, user: MPProfile = Depends(get_current_user)):
response.status_code = 201 response.status_code = 201
return await chat_service.create_chat(chat.name, user.id) return await chat_service.create_chat(chat.name, user.id)
@router.get("/{chat_id}")
async def chat_info(chat_id: int = Path(alias='chat_id'), user: MPProfile = Depends(get_current_user)):
return await chat_service.get_chat(chat_id, user.id)
@router.get("")
async def available_chats(user: MPProfile = Depends(get_current_user)):
return await chat_service.get_chats(user.id)

View File

@ -1,5 +1,26 @@
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, Response
from core.helpers.auth.helpers import get_current_user
from core.models.message.db import MPProfile
from core.models.message.requests import SendMessageRequest
from core.services import message_service
router = APIRouter(prefix="/message", tags=["message"]) router = APIRouter(prefix="/message", tags=["message"])
@router.post("")
async def send_message(response: Response, message: SendMessageRequest, user: MPProfile = Depends(get_current_user)):
response.status_code = 201
return (await message_service.send_message(user, message)).model_dump(exclude_none=True, by_alias=True)
async def list_messages():
pass
async def delete_message():
pass
async def edit_message():
pass

View File

@ -25,3 +25,16 @@ def incorrect_login():
message="Incorrect login") message="Incorrect login")
raise CustomExceptionHandler(status_code=401, response=response) raise CustomExceptionHandler(status_code=401, response=response)
def chat_not_found():
response = Response(status=404,
error=True,
message="Chat not found")
raise CustomExceptionHandler(status_code=404, response=response)
def not_a_member_of_chat():
response = Response(status=403,
error=True,
message="You are not a member of this chat")
raise CustomExceptionHandler(status_code=403, response=response)

View File

@ -10,6 +10,7 @@ class MPProfile(Base):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
external_id: Mapped[int] = mapped_column(unique=True) external_id: Mapped[int] = mapped_column(unique=True)
chats: Mapped[list['MPChat']] = relationship('MPChat', secondary='mp_chat_user') chats: Mapped[list['MPChat']] = relationship('MPChat', secondary='mp_chat_user')
messages: Mapped[list['MPMessage']] = relationship('MPMessage', back_populates='sender')
class MPChat(Base): class MPChat(Base):
@ -19,6 +20,7 @@ class MPChat(Base):
admin_id: Mapped[int] = mapped_column(ForeignKey('mp_profile.id')) admin_id: Mapped[int] = mapped_column(ForeignKey('mp_profile.id'))
admin: Mapped[MPProfile] = relationship() admin: Mapped[MPProfile] = relationship()
users: Mapped[list[MPProfile]] = relationship('MPProfile', secondary='mp_chat_user') users: Mapped[list[MPProfile]] = relationship('MPProfile', secondary='mp_chat_user')
messages: Mapped[list['MPMessage']] = relationship('MPMessage', back_populates='chat')
class MPMessage(Base): class MPMessage(Base):
@ -27,8 +29,8 @@ class MPMessage(Base):
sender_id: Mapped[int] = mapped_column(ForeignKey('mp_profile.id')) sender_id: Mapped[int] = mapped_column(ForeignKey('mp_profile.id'))
chat_id: Mapped[int] = mapped_column(ForeignKey('mp_chat.id')) chat_id: Mapped[int] = mapped_column(ForeignKey('mp_chat.id'))
content: Mapped[str] content: Mapped[str]
chat: Mapped[MPChat] = relationship() chat: Mapped[MPChat] = relationship("MPChat", back_populates="messages")
sender: Mapped[MPProfile] = relationship() sender: Mapped[MPProfile] = relationship("MPProfile", back_populates="messages")
class MPChatUser(Base): class MPChatUser(Base):

View File

@ -7,5 +7,4 @@ class CreateChatRequest(BaseModel):
class SendMessageRequest(BaseModel): class SendMessageRequest(BaseModel):
chat_id: int chat_id: int
sender_id: int
content: str content: str

View File

@ -1,3 +1,34 @@
from pydantic import BaseModel import datetime
from pydantic import BaseModel, ConfigDict, AliasGenerator
from pydantic.alias_generators import to_camel, to_snake
class ProfileResponse(BaseModel):
model_config = ConfigDict(alias_generator=AliasGenerator(serialization_alias=to_camel))
id: int
external_id: int
created_at: datetime.datetime
modified_at: datetime.datetime
class ChatResponse(BaseModel):
model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_snake,
serialization_alias=to_camel))
id: int
name: str | None
admin: ProfileResponse
users: list[ProfileResponse] | None = None
created_at: datetime.datetime
modified_at: datetime.datetime
class MessageResponse(BaseModel):
model_config = ConfigDict(alias_generator=AliasGenerator(validation_alias=to_snake,
serialization_alias=to_camel))
id: int
sender: ProfileResponse
content: str
chat: ChatResponse
created_at: datetime.datetime
modified_at: datetime.datetime

View File

@ -1,7 +1,8 @@
from .auth.services import Service as AuthService from .auth.services import Service as AuthService
from .chat.services import Service as ChatService from .chat.services import Service as ChatService
from .message.service import Service as MessageService
# Register services # Register services
auth_service = AuthService() auth_service = AuthService()
chat_service = ChatService() chat_service = ChatService()
message_service = MessageService()

View File

@ -1,9 +1,36 @@
from core.errors.errors import not_a_member_of_chat
from core.models.message.db import MPChat from core.models.message.db import MPChat
from core.models.message.responses import ChatResponse, ProfileResponse
from core.storage import chat_storage from core.storage import chat_storage
class Service: class Service:
async def create_chat(self, name: str, admin_id: int) -> MPChat:
async def build_chat_response(self, chat: MPChat) -> ChatResponse:
return ChatResponse(id=chat.id,
name=chat.name,
admin=ProfileResponse(id=chat.admin.id,
external_id=chat.admin.external_id,
created_at=chat.admin.created_at,
modified_at=chat.admin.modified_at),
users=[ProfileResponse(id=user.id,
external_id=user.external_id,
created_at=user.created_at,
modified_at=user.modified_at) for user in chat.users],
created_at=chat.created_at,
modified_at=chat.modified_at)
async def create_chat(self, name: str, admin_id: int) -> ChatResponse:
chat = await chat_storage.create_chat(name=name, admin_id=admin_id) chat = await chat_storage.create_chat(name=name, admin_id=admin_id)
await chat_storage.add_member(chat.id, admin_id) chat = await chat_storage.add_member(chat.id, admin_id)
return chat return await self.build_chat_response(chat)
async def get_chat(self, chat_id: int, user_id: int) -> ChatResponse:
chat = await chat_storage.get_chat(chat_id)
if user_id not in [user.id for user in chat.users]:
not_a_member_of_chat()
return await self.build_chat_response(chat)
async def get_chats(self, user_id: int) -> list[ChatResponse]:
chats = await chat_storage.get_chats(user_id)
return [await self.build_chat_response(chat) for chat in chats]

View File

View File

@ -0,0 +1,46 @@
from core.errors.errors import not_a_member_of_chat
from core.models.message.db import MPProfile, MPMessage
from core.models.message.requests import SendMessageRequest
from core.models.message.responses import MessageResponse, ProfileResponse, ChatResponse
from core.storage import message_storage, auth_storage, chat_storage
class Service:
async def build_message_response(self, msg: MPMessage) -> MessageResponse:
"""
Build a message response
"""
return MessageResponse(
id=msg.id,
sender=ProfileResponse(id=msg.sender.id, external_id=msg.sender.external_id,
created_at=msg.sender.created_at, modified_at=msg.sender.modified_at),
content=msg.content,
chat=ChatResponse(id=msg.chat.id,
name=msg.chat.name,
admin=ProfileResponse(id=msg.chat.admin.id, external_id=msg.chat.admin.external_id,
created_at=msg.chat.admin.created_at,
modified_at=msg.chat.admin.modified_at),
users=None,
created_at=msg.chat.created_at,
modified_at=msg.chat.modified_at),
created_at=msg.created_at,
modified_at=msg.modified_at
)
async def send_message(self, user: MPProfile, message: SendMessageRequest) -> MessageResponse:
"""
Send message to chat
"""
# Check chat exists
chat = await chat_storage.get_chat(message.chat_id)
# Check user is in chat
if user.id not in [x.id for x in chat.users]:
not_a_member_of_chat()
# Add message to Database
msg = await message_storage.insert_message(user.id, message.chat_id, message.content)
return await self.build_message_response(msg)

View File

@ -5,6 +5,8 @@ Register our storages
from database import Session from database import Session
from .chat.storage import Storage as ChatStorage from .chat.storage import Storage as ChatStorage
from .auth.storage import Storage as AuthStorage from .auth.storage import Storage as AuthStorage
from .message.storage import Storage as MessageStorage
chat_storage = ChatStorage(Session) chat_storage = ChatStorage(Session)
auth_storage = AuthStorage(Session) auth_storage = AuthStorage(Session)
message_storage = MessageStorage(Session)

View File

@ -34,4 +34,5 @@ class Storage(BaseStorage):
async with self.get_session() as session: async with self.get_session() as session:
session: AsyncSession session: AsyncSession
user = await session.get(MPProfile, id) user = await session.get(MPProfile, id)
await session.refresh(user, attribute_names=["chats"])
return user return user

View File

@ -1,7 +1,9 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from ..base import BaseStorage from ..base import BaseStorage
from core.models.message.db import MPChat, MPProfile from core.models.message.db import MPChat, MPProfile
from ...errors.errors import chat_not_found
class Storage(BaseStorage): class Storage(BaseStorage):
@ -16,7 +18,7 @@ class Storage(BaseStorage):
await session.refresh(chat) await session.refresh(chat)
return chat return chat
async def add_member(self, chat_id: int, user_id: int): async def add_member(self, chat_id: int, user_id: int) -> MPChat:
""" """
Storage handler for adding a member to a chat Storage handler for adding a member to a chat
""" """
@ -28,12 +30,25 @@ class Storage(BaseStorage):
chat.users.append(user) chat.users.append(user)
await session.commit() await session.commit()
await session.refresh(chat, attribute_names=["users", "admin"]) await session.refresh(chat, attribute_names=["users", "admin"])
return chat
async def get_chat(self, id: int) -> MPChat:
async def get_chat(self, id: int):
""" """
Storage handler for getting a chat Storage handler for getting a chat
""" """
async with self.get_session() as session: async with self.get_session() as session:
chat = await session.get(MPChat, id) chat = await session.get(MPChat, id)
if chat is None:
chat_not_found()
await session.refresh(chat, attribute_names=["users", "admin"])
return chat return chat
async def get_chats(self, user_id: int) -> list[MPChat]:
async with self.get_session() as session:
session: AsyncSession
stmt = select(MPChat).where(MPChat.users.any(MPProfile.id == user_id))
result = await session.execute(stmt)
chats: list[MPChat] = result.scalars().all()
for chat in chats:
await session.refresh(chat, attribute_names=["users", "admin"])
return chats

View File

View File

@ -0,0 +1,22 @@
from sqlalchemy import insert, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, subqueryload
from ..base import BaseStorage
from ...models.message.db import MPMessage, MPProfile, MPChat
class Storage(BaseStorage):
async def insert_message(self, sender_id: int, chat_id: int, content: str) -> MPMessage:
async with self.get_session() as session:
session: AsyncSession
msg = MPMessage(sender_id=sender_id, chat_id=chat_id, content=content)
session.add(msg)
await session.commit()
await session.refresh(msg)
stmt = select(MPMessage).options(joinedload(MPMessage.chat).joinedload(MPChat.admin),
joinedload(MPMessage.sender)).where(MPMessage.id == msg.id)
result = await session.execute(stmt)
data = result.scalar_one_or_none()
return data

View File

@ -12,7 +12,7 @@ server_thread = threading.Thread(target=uvicorn.run, args=(app,), daemon=True,
server_thread.start() server_thread.start()
client = Client(base_url="http://127.0.0.1:8000") client = Client(base_url="http://127.0.0.1:8000", timeout=60*60*60)
def test_create_chat(): def test_create_chat():
@ -29,3 +29,50 @@ def test_create_chat():
assert 'name' in resp.json() assert 'name' in resp.json()
assert resp.json()['name'] == 'test_chat' assert resp.json()['name'] == 'test_chat'
def test_user_chats():
resp = client.post('/api/v1/auth', json={'username': 1})
token = resp.json()['access_token']
resp = client.get('/api/v1/chat', headers={'Authorization': f'Bearer {token}'})
assert resp.status_code == 200
assert isinstance(resp.json(), list)
def test_user_forbidden_chat():
resp = client.post('/api/v1/auth', json={'username': 1})
token = resp.json()['access_token']
resp = client.get('/api/v1/chat/1', headers={'Authorization': f'Bearer {token}'})
assert resp.status_code == 403
def test_user_retrieve_chat():
resp = client.post('/api/v1/auth', json={'username': 1})
token = resp.json()['access_token']
resp = client.get('/api/v1/chat', headers={'Authorization': f'Bearer {token}'})
resp = client.get(f"/api/v1/chat/{resp.json()[0]['id']}", headers={'Authorization': f'Bearer {token}'})
assert resp.status_code == 200
def test_not_found_chat():
resp = client.post('/api/v1/auth', json={'username': 1})
token = resp.json()['access_token']
resp = client.get(f"/api/v1/chat/-1", headers={'Authorization': f'Bearer {token}'})
assert resp.status_code == 404
# Testing auth tokens
def test_create_chat_no_token():
resp = client.post(f"/api/v1/chat", json={'name': 'chat no token'})
assert resp.status_code == 422
def test_create_chat_invalid_token():
resp = client.post(f"/api/v1/chat", json={'name': 'chat invalid token'}, headers={'Authorization': 'Bearer 123'})
assert resp.status_code == 401
def test_create_chat_expired_token():
token = jwt.encode({'user_id': 1, "exp": 1715940028}, Config.secret, algorithm='HS256')
resp = client.post(f"/api/v1/chat", json={'name': 'chat expired token'}, headers={f"Authorization": f"Bearer {token}"})
assert resp.status_code == 401

44
tests/test_message.py Normal file
View File

@ -0,0 +1,44 @@
import threading
import uvicorn
from main import app
from httpx import Client
server_thread = threading.Thread(target=uvicorn.run, args=(app,), daemon=True,
kwargs={"host": "127.0.0.1", "port": 8000, "reload": False})
server_thread.start()
client = Client(base_url="http://127.0.0.1:8000", timeout=60 * 60 * 60)
def test__message_send():
# Complete authorization
resp = client.post('/api/v1/auth', json={'username': 1})
assert resp.status_code == 200
token = resp.json()['access_token']
chat_id = client.get('/api/v1/chat', headers={'Authorization': f'Bearer {token}'}).json()[0]["id"]
resp = client.post('/api/v1/message', json={
"chat_id": chat_id,
"content": "test__message_send"}, headers={'Authorization': f'Bearer {token}'})
assert resp.status_code == 201
assert resp.json()["content"] == "test__message_send"
assert resp.json()["id"] > 0
def test__message_send_without_auth():
resp = client.post('/api/v1/message', json={
"chat_id": 70,
"content": "test__message_send_without_auth"}, headers={'Authorization': f'Bearer 123'})
assert resp.status_code == 401
def test__message_send_without_chat():
# Complete authorization
resp = client.post('/api/v1/auth', json={'username': 1})
assert resp.status_code == 200
token = resp.json()['access_token']
resp = client.post('/api/v1/message', json={
"chat_id": -1,
"content": "test__message_send_without_chat"}, headers={'Authorization': f'Bearer {token}'})
assert resp.status_code == 404