add message sent function + upd test cases
parent
23d1a20459
commit
5ce8c9026c
|
@ -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.models.message.db import MPProfile
|
||||
from core.services import chat_service
|
||||
|
||||
from core.models.message.requests import CreateChatRequest
|
||||
|
||||
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)):
|
||||
response.status_code = 201
|
||||
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)
|
||||
|
|
|
@ -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.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
|
||||
|
|
|
@ -25,3 +25,16 @@ def incorrect_login():
|
|||
message="Incorrect login")
|
||||
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)
|
|
@ -10,6 +10,7 @@ class MPProfile(Base):
|
|||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
external_id: Mapped[int] = mapped_column(unique=True)
|
||||
chats: Mapped[list['MPChat']] = relationship('MPChat', secondary='mp_chat_user')
|
||||
messages: Mapped[list['MPMessage']] = relationship('MPMessage', back_populates='sender')
|
||||
|
||||
|
||||
class MPChat(Base):
|
||||
|
@ -19,6 +20,7 @@ class MPChat(Base):
|
|||
admin_id: Mapped[int] = mapped_column(ForeignKey('mp_profile.id'))
|
||||
admin: Mapped[MPProfile] = relationship()
|
||||
users: Mapped[list[MPProfile]] = relationship('MPProfile', secondary='mp_chat_user')
|
||||
messages: Mapped[list['MPMessage']] = relationship('MPMessage', back_populates='chat')
|
||||
|
||||
|
||||
class MPMessage(Base):
|
||||
|
@ -27,8 +29,8 @@ class MPMessage(Base):
|
|||
sender_id: Mapped[int] = mapped_column(ForeignKey('mp_profile.id'))
|
||||
chat_id: Mapped[int] = mapped_column(ForeignKey('mp_chat.id'))
|
||||
content: Mapped[str]
|
||||
chat: Mapped[MPChat] = relationship()
|
||||
sender: Mapped[MPProfile] = relationship()
|
||||
chat: Mapped[MPChat] = relationship("MPChat", back_populates="messages")
|
||||
sender: Mapped[MPProfile] = relationship("MPProfile", back_populates="messages")
|
||||
|
||||
|
||||
class MPChatUser(Base):
|
||||
|
|
|
@ -7,5 +7,4 @@ class CreateChatRequest(BaseModel):
|
|||
|
||||
class SendMessageRequest(BaseModel):
|
||||
chat_id: int
|
||||
sender_id: int
|
||||
content: str
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
from .auth.services import Service as AuthService
|
||||
from .chat.services import Service as ChatService
|
||||
|
||||
from .message.service import Service as MessageService
|
||||
# Register services
|
||||
|
||||
auth_service = AuthService()
|
||||
chat_service = ChatService()
|
||||
message_service = MessageService()
|
||||
|
|
|
@ -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.responses import ChatResponse, ProfileResponse
|
||||
from core.storage import chat_storage
|
||||
|
||||
|
||||
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)
|
||||
await chat_storage.add_member(chat.id, admin_id)
|
||||
return chat
|
||||
chat = await chat_storage.add_member(chat.id, admin_id)
|
||||
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]
|
||||
|
|
|
@ -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)
|
|
@ -5,6 +5,8 @@ Register our storages
|
|||
from database import Session
|
||||
from .chat.storage import Storage as ChatStorage
|
||||
from .auth.storage import Storage as AuthStorage
|
||||
from .message.storage import Storage as MessageStorage
|
||||
|
||||
chat_storage = ChatStorage(Session)
|
||||
auth_storage = AuthStorage(Session)
|
||||
message_storage = MessageStorage(Session)
|
|
@ -34,4 +34,5 @@ class Storage(BaseStorage):
|
|||
async with self.get_session() as session:
|
||||
session: AsyncSession
|
||||
user = await session.get(MPProfile, id)
|
||||
await session.refresh(user, attribute_names=["chats"])
|
||||
return user
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from ..base import BaseStorage
|
||||
from core.models.message.db import MPChat, MPProfile
|
||||
from ...errors.errors import chat_not_found
|
||||
|
||||
|
||||
class Storage(BaseStorage):
|
||||
|
@ -16,7 +18,7 @@ class Storage(BaseStorage):
|
|||
await session.refresh(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
|
||||
"""
|
||||
|
@ -28,12 +30,25 @@ class Storage(BaseStorage):
|
|||
chat.users.append(user)
|
||||
await session.commit()
|
||||
await session.refresh(chat, attribute_names=["users", "admin"])
|
||||
return chat
|
||||
|
||||
|
||||
async def get_chat(self, id: int):
|
||||
async def get_chat(self, id: int) -> MPChat:
|
||||
"""
|
||||
Storage handler for getting a chat
|
||||
"""
|
||||
async with self.get_session() as session:
|
||||
chat = await session.get(MPChat, id)
|
||||
if chat is None:
|
||||
chat_not_found()
|
||||
await session.refresh(chat, attribute_names=["users", "admin"])
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -12,7 +12,7 @@ server_thread = threading.Thread(target=uvicorn.run, args=(app,), daemon=True,
|
|||
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():
|
||||
|
@ -29,3 +29,50 @@ def test_create_chat():
|
|||
assert 'name' in resp.json()
|
||||
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
|
|
@ -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
|
Loading…
Reference in New Issue