diff --git a/backend/api/.vscode/settings.json b/backend/api/.vscode/settings.json new file mode 100644 index 0000000..9d6cb40 --- /dev/null +++ b/backend/api/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "." + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/backend/api/database/room/crud.py b/backend/api/database/room/crud.py index ea79cd2..2a4ae64 100644 --- a/backend/api/database/room/crud.py +++ b/backend/api/database/room/crud.py @@ -9,19 +9,20 @@ def create_room_db(*,room: RoomCreate, user: User | None = None, username: str | id_code = generate_unique_code(Room,s=db) room_obj = Room(**room.dict(exclude_unset=True), id_code=id_code) if user is not None: - member = Member(user_id=user.id, room=room_obj) + member = Member(user_id=user.id, room=room_obj, is_admin=True) db.add(member) db.commit() db.refresh(member) if username is not None: reconnect_code = generate_unique_code(Anonymous, s=db, field_name='reconnect_code') anonymous = Anonymous(username=username, reconnect_code=reconnect_code) - member = Member(anonymous=anonymous, room=room_obj) + member = Member(anonymous=anonymous, room=room_obj, is_admin=True) db.add(member) db.commit() db.refresh(member) if username is None and user is None: raise ValueError('Username or user required') + return {"room": room_obj, "member": member} def check_room(room_id: str, db: Session = Depends(get_session)): diff --git a/backend/api/database/room/models.py b/backend/api/database/room/models.py index 857f758..8c7071e 100644 --- a/backend/api/database/room/models.py +++ b/backend/api/database/room/models.py @@ -1,3 +1,4 @@ +from pydantic import root_validator from typing import List, Optional, TYPE_CHECKING from sqlmodel import SQLModel, Field, Relationship @@ -48,18 +49,43 @@ class Member(SQLModel, table = True): is_admin: bool = False - waiting: bool = True + waiting: bool = False online: bool = False - + + waiter_code: Optional[str] = Field(default= None) class RoomRead(RoomBase): id_code: str #members: List['Member'] class AnonymousRead(AnonymousBase): reconnect_code: str + class Username(SQLModel): username: str + +class MemberWithRelations(SQLModel): + is_admin: bool + user: UserRead | None = None + anonymous: AnonymousRead | None = None + class MemberRead(SQLModel): - anonymous: AnonymousRead = None - user: Username = None \ No newline at end of file + username: str + reconnect_code: str = '' + isUser: bool + isAdmin: bool + +class MemberSerializer(MemberRead): + member: MemberWithRelations + + @root_validator + def parse_member(cls, values): + member = values.get('member') + if member == None: + return values + member_obj = member.user or member.anonymous + if member_obj is None: + raise ValueError('User or anonymous required') + return {"username": member_obj.username, "reconnect_code": getattr(member_obj, "reconnect_code", ""), "isAdmin": member.is_admin, "isUser": member.user != None} + + diff --git a/backend/api/main.py b/backend/api/main.py index d711c41..1c0317f 100644 --- a/backend/api/main.py +++ b/backend/api/main.py @@ -7,7 +7,7 @@ from services.auth import get_current_user_optional, jwt_required from fastapi.openapi.utils import get_openapi from database.auth.models import User, UserRead from database.exercices.models import Exercice, ExerciceRead -from database.room.models import Room, Anonymous, Member +from database.room.models import Room, Anonymous, MemberWithRelations import database.db from fastapi_pagination import add_pagination from fastapi.responses import PlainTextResponse diff --git a/backend/api/routes/room/routes.py b/backend/api/routes/room/routes.py index 985ee77..1429af7 100644 --- a/backend/api/routes/room/routes.py +++ b/backend/api/routes/room/routes.py @@ -1,3 +1,9 @@ +from pydantic import Field +from pydantic import create_model_from_typeddict +from pydantic.error_wrappers import ValidationError +from pydantic import validate_arguments +from services.database import generate_unique_code +from sqlmodel import col from sqlmodel import select from pydantic import BaseModel from typing import Any, Callable, Dict, List, Optional @@ -8,10 +14,11 @@ from database.auth.models import User from database.db import get_session from database.room.crud import check_room, create_room_db, userInRoom from sqlmodel import Session -from database.room.models import Anonymous, Member, MemberRead, Room, RoomCreate, RoomRead +from database.room.models import Anonymous, MemberSerializer, MemberWithRelations, MemberRead, Room, RoomCreate, RoomRead, Member from services.auth import get_current_user_optional from fastapi.exceptions import HTTPException from jose import jwt, exceptions +import inspect router = APIRouter(tags=["room"]) @@ -20,17 +27,30 @@ class RoomAndMember(BaseModel): member: MemberRead +class Waiter(BaseModel): + username: str + waiter_id: str + + +def serialize_member(member: Member) -> MemberRead | Waiter: + member_obj = member.user or member.anonymous + if member.waiting == False: + return MemberRead(username=member_obj.username, reconnect_code=getattr(member_obj, "reconnect_code", ""), isUser=member.user_id != None, isAdmin=member.is_admin) + if member.waiting == True: + return Waiter(username=member_obj.username, waiter_id=member.waiter_code) + + @router.post('/room', response_model=RoomAndMember) def create_room(room: RoomCreate, username: Optional[str] = Query(default=None, max_length=20), user: User | None = Depends(get_current_user_optional), db: Session = Depends(get_session)): room_obj = create_room_db(room=room, user=user, username=username, db=db) - return room_obj + return {'room': room_obj['room'], "member": serialize_member(room_obj['member'])} class ConnectionManager: def __init__(self): self.active_connections: Dict[str, List[WebSocket]] = {} - async def add(self, group: str, ws: WebSocket): + def add(self, group: str, ws: WebSocket): if group not in self.active_connections: self.active_connections[group] = [] @@ -49,22 +69,54 @@ class ConnectionManager: await connection.send_json(message) -manager = ConnectionManager() - - def make_event_decorator(eventsDict): - def _(name: str): + def _(name: str, conditions: List[Callable | bool] = []): def add_event(func): - eventsDict[name] = func + model = validate_arguments(func).model + eventsDict[name] = {"func": func, + "conditions": conditions, "model": model} return func - return add_event return _ +class Event(BaseModel): + func: Callable + conditions: List[Callable | bool] + model: BaseModel + + +def dict_model(model: BaseModel, exclude: List[str]): + value = {} + for n, f in model: + if n not in exclude: + value[n] = f + return value + + +def dict_all(obj: Any): + if isinstance(obj, dict): + value = {} + for k, v in obj.items(): + if isinstance(v, dict): + v = dict_all(v) + value[k] = dict(v) + elif isinstance(v, BaseModel): + value[k] = dict(v) + else: + try: + value[k] = dict(v) + except: + value[k] = v + return value + return dict(obj) + + class Consumer: - events: Dict[str, Callable] = {} + events: Dict[str, Event] = {} + sendings: Dict[str, Any] = {} event = make_event_decorator(events) + sending = make_event_decorator(sendings) def __init__(self, ws: WebSocket): self.ws: WebSocket = ws @@ -73,13 +125,70 @@ class Consumer: async def connect(self): pass + async def validation_error_handler(self, e: ValidationError): + errors = e.errors() + await self.ws.send_json({"type": "error", "data": {"detail": [{ers['loc'][-1]: ers['msg']} for ers in errors]}}) + + async def send(self, payload): + type = payload.get('type', None) + print('TYPE', type, self.member.id) + if type is not None: + event_wrapper = self.sendings.get(type, None) + if event_wrapper is not None: + handler = event_wrapper.get('func') + conditions = event_wrapper.get('conditions') + + is_valid = all([(await c(self)) if inspect.iscoroutinefunction(c) else c(self) if inspect.isfunction(c) else c == True if isinstance(c, bool) else True for c in conditions]) + + if handler is not None and is_valid: + model = event_wrapper.get("model") + + data = payload.get('data') or {} + try: + validated_payload = model(self=self, **data) + except ValidationError as e: + await self.ws.send_json({"type": "error", "data": {"msg": "Oops there was an error"}}) + return + + validated_payload = dict_model(validated_payload, + exclude=["v__duplicate_kwargs", "args", 'kwargs', "self"]) + try: + parsed_payload = handler( + self, **validated_payload) + + + await self.ws.send_json({'type': type, "data": dict_all(parsed_payload)}) + return + except Exception as e: + + print('NOPE', self.member.id, e) + return + return + print('pls') + await self.ws.send_json(payload) + print('sent') + async def receive(self, data): event = data.get('type', None) if event is not None: - handler = self.events.get(event, None) - if handler is not None: - payload = data.get('data') - await handler(self, payload) + event_wrapper = self.events.get(event, None) + if event_wrapper is not None: + handler = event_wrapper.get('func') + conditions = event_wrapper.get('conditions') + + is_valid = all([(await c(self)) if inspect.iscoroutinefunction(c) else c(self) if inspect.isfunction(c) else c == True if isinstance(c, bool) else True for c in conditions]) + + if handler is not None and is_valid: + model = event_wrapper.get("model") + + payload = data.get('data') or {} + try: + validated_payload = model(self=self, **payload) + except ValidationError as e: + await self.validation_error_handler(e) + return + + await handler(**{k: v for k, v in validated_payload.dict().items() if k not in ["v__duplicate_kwargs", "args", 'kwargs']}) async def disconnect(self): pass @@ -94,6 +203,36 @@ class Consumer: await self.disconnect() +class ConsumerManager: + def __init__(self): + self.active_connections: Dict[str, List[Consumer]] = {} + + def add(self, group: str, ws: Consumer): + + if group not in self.active_connections: + self.active_connections[group] = [] + print("adding", ws, self.active_connections[group]) + if ws not in self.active_connections[group]: + print('ACTUALLY ADDING') + self.active_connections[group].append(ws) + + def remove(self, group: str, ws: Consumer): + if group in self.active_connections: + if ws in self.active_connections[group]: + self.active_connections[group].remove(ws) + + async def broadcast(self, message, group: str, exclude: list[Consumer] = []): + if group in self.active_connections: + print(self.active_connections[group], exclude) + for connection in list(set(self.active_connections[group])): + if connection not in exclude: + print('SEND TO', connection, message) + await connection.send(message) + + +manager = ConsumerManager() + + class Token(BaseModel): token: str @@ -115,12 +254,30 @@ def get_member_from_user(user_id: int, room_id: int, db: Session): return member +def get_member_from_token(token: str, room_id: int, db: Session): + user = get_user_from_token(token, db) + if user is False: + return False + if user is None: + return None + member = get_member_from_user(user.id, room_id, db) + return member + + def get_member_from_anonymous(anonymous_id: int, room_id: int, db: Session): member = db.exec(select(Member).where(Member.room_id == room_id, Member.anonymous_id == anonymous_id)).first() return member +def get_member_reconnect_code(reconnect_code: str, room_id: int, db: Session): + anonymous = get_anonymous_from_code(reconnect_code, db) + if anonymous is None: + return None + member = get_member_from_anonymous(anonymous.id, room_id, db) + return member + + def get_anonymous_from_code(reconnect_code: str, db: Session): anonymous = db.exec(select(Anonymous).where( Anonymous.reconnect_code == reconnect_code)).first() @@ -136,31 +293,117 @@ def connect_member(member: Member, db: Session): def disconnect_member(member: Member, db: Session): - member.online = False + if member.waiting == False: + member.online = False + db.add(member) + db.commit() + db.refresh(member) + return member + else: + db.delete(member) + db.commit() + return member + + +def validate_username(username: str, room: Room, db: Session = Depends(get_session)): + print('VALIDATE', username) + if len(username) > 20: + return None + members = select(Member.anonymous_id).where( + Member.room_id == room.id, Member.anonymous_id != None) + anonymous = select(Anonymous).where( + col(Anonymous.id).in_(members), Anonymous.username == username) + username_anonymous = db.exec(anonymous).first() + return None if username_anonymous is not None else username + + +def create_anonymous_member(username: str, room: Room, db: Session): + username = validate_username(username) + if username is None: + return None + reconnect_code = generate_unique_code( + Anonymous, s=db, field_name="reconnect_code") + anonymous = Anonymous(username=username, reconnect_code=reconnect_code) + member = Member(room=room, anonymous=anonymous) db.add(member) db.commit() db.refresh(member) return member -def validate_username(username: str, room: Room, db: Session): - members = select(Member).where(Member.room_id == room.id, Member.anonymous_id != None) - -def create_anonymous_member(username: str, room: Room, db: Session): - pass +def create_user_member(user: User, room: Room, db: Session): + member = get_member_from_user(user.id, room.id, db) + if member is not None: + return None + member = Member(room=room, user=user) + db.add(member) + db.commit() + db.refresh(member) + return member + + +def create_anonymous_waiter(username: str, room: Room, db: Session): + username = validate_username(username, room, db) + if username is None: + return None + reconnect_code = generate_unique_code( + Anonymous, s=db, field_name="reconnect_code") + anonymous = Anonymous(username=username, reconnect_code=reconnect_code) + + waiter_code = generate_unique_code(Member, s=db, field_name="waiter_code") + member = Member(room=room, anonymous=anonymous, + waiting=True, waiter_code=waiter_code) + db.add(member) + db.commit() + db.refresh(member) + return member + + +def create_user_waiter(user: User, room: Room, db: Session): + member = get_member_from_user(user.id, room.id, db) + if member is not None: + return None + waiter_code = generate_unique_code(Member, s=db, field_name="waiter_code") + member = Member(room=room, user=user, waiting=True, + waiter_code=waiter_code) + db.add(member) + db.commit() + db.refresh(member) + return member + + +def get_waiter(waiter_code: str, db: Session): + return db.exec(select(Member).where(Member.waiter_code == waiter_code)).first() + + +def accept_waiter(member: Member, db: Session): + member.waiting = False + member.waiter_code = None + db.add(member) + db.commit() + db.refresh(member) + return member + +def refuse_waiter(member: Member, db: Session): + db.delete(member) + db.commit() + return None + class RoomConsumer(Consumer): - def __init__(self, ws: WebSocket, room: Room, manager: ConnectionManager, db: Session): + + def __init__(self, ws: WebSocket, room: Room, manager: ConsumerManager, db: Session): self.room = room self.ws = ws self.manager = manager self.db = db self.member = None - #WS Utilities + # WS Utilities + async def connect(self): await self.ws.accept() - async def send(self, type: str, payload: Any): + async def direct_send(self, type: str, payload: Any): await self.ws.send_json({'type': type, "data": payload}) async def send_to_all_room(self, type: str, payload: Any, exclude: bool = False): @@ -173,81 +416,170 @@ class RoomConsumer(Consumer): async def send_to_members(self, type: str, payload: Any, exclude: bool = False): await self.manager.broadcast({'type': type, "data": payload}, f'{self.room.id}__member', [exclude == True and self.ws]) + async def broadcast(self, type, payload, exclude= False): + await self.manager.broadcast({"type": type, "data": payload}, self.room.id, exclude = [exclude == True and self]) + def add_to_admin(self): self.manager.add(f'{self.room.id}__admin', self.ws) def add_to_members(self): self.manager.add(f'{self.room.id}__members', self.ws) - def add_to_groups(self): - if isinstance(self.member, Member): - if self.member.is_admin == True: - self.add_to_admin() - if self.member.is_admin == False: - self.add_to_members() + def add_to_group(self): + if self.member.waiting == True: + self.manager.add(f'waiter__{self.member.waiter_code}', self) + self.manager.add(self.room.id, self) async def connect_self(self): if isinstance(self.member, Member): connect_member(self.member, self.db) - await self.send_to_all_room(type="connect", payload={}, exclude=True) - + await self.broadcast(type="connect", payload={"member": serialize_member(self.member).dict()}) + async def disconnect_self(self): if isinstance(self.member, Member): disconnect_member(self.member, self.db) - await self.send_to_all_room(type="disconnect", payload={}, exclude=True) - - #DB Utilities - - #Events + await self.broadcast(type="disconnect", payload={"member": serialize_member(self.member).dict()}) + + # DB Utilities + + # Received Events @Consumer.event('login') - async def login(self, data): - if 'token' in data: - token = data.get('token') - user = get_user_from_token(token, db=self.db) - if user == False: - await self.send() + async def login(self, token: str | None = None, reconnect_code: str | None = None): + if token is not None: + member = get_member_from_token(token, self.room.id, self.db) + if member == False: + await self.direct_send(type="error", payload={"msg": "Token expired"}) return - if user is None: - return - - member = get_member_from_user( - user_id=user.id, room_id=self.room.id, db=self.db) if member is None: + await self.direct_send(type="error", payload={"msg": "Utilisateur introuvable dans cette salle"}) + return + self.member = member + + # await self.connect_self() + self.add_to_group() + await self.direct_send(type="loggedIn", payload={"member": serialize_member(self.member).dict()}) + + elif reconnect_code is not None: + member = get_member_reconnect_code( + reconnect_code, self.room.id, db=self.db) + if member is None: + await self.direct_send(type="error", payload={"msg": "Utilisateur introuvable dans cette salle"}) return self.member = member - self.add_to_groups() - self.connect_self() - await self.send() - elif "reconnect_code" in data: - reconnect_code = data.get('reconnect_code') - anonymous = get_anonymous_from_code( - reconnect_code=reconnect_code, db=self.db) - if anonymous is None: - return - - member = get_member_from_anonymous( - anonymous_id=anonymous.id, room_id=self.room.id, db=self.db) - if member is None: - return - - self.member = member - self.add_to_groups() - self.connect_self() - await self.send(type="accepted") + # await self.connect_self() + self.add_to_group() + await self.direct_send(type="loggedIn", payload={"member": serialize_member(self.member).dict()}) + if reconnect_code is None and token is None: + await self.direct_send(type="error", payload={"msg": "Veuillez spécifier une méthode de connection"}) @Consumer.event('join') - async def join(self, data): - if "token" in data: - return - else: - return - return + async def join(self, token: str | None = None, username: str | None = None): + if self.room.public == False: + if token is not None: + user = get_user_from_token(token, self.db) + + if user is None: + await self.direct_send(type="error", payload={"msg": "Utilisateur introuvable"}) + return + if user is False: + await self.direct_send(type="error", payload={"msg": "Token expired"}) + return + + waiter = create_user_waiter(user, self.room, self.db) + + if waiter.waiting is False: + self.member = waiter + # await self.connect_self() + self.add_to_group() + await self.direct_send(type="loggedIn", payload={"member": serialize_member(self.member).dict()}) + return + + self.member = waiter + self.add_to_group() + await self.direct_send(type="waiting", payload={"waiter": serialize_member(self.member).dict()}) + await self.broadcast(type="waiter", payload={"waiter": serialize_member(self.member).dict()}) + + elif username is not None: + waiter = create_anonymous_waiter(username, self.room, self.db) + if waiter is None: + await self.direct_send(type="error", payload={"msg": "Nom d'utilisateur invalide ou indisponible"}) + return + self.member = waiter + self.add_to_group() + await self.direct_send(type="waiting", payload={"waiter": serialize_member(self.member).dict()}) + await self.broadcast(type="waiter", payload={"waiter": serialize_member(self.member).dict()}) + else: + if token is not None: + user = get_user_from_token(token, self.db) + if user is None: + return + if user is False: + return + + member = create_user_member(user, self.room, self.db) + if member is None: + return + self.member = member + self.add_to_group() + await self.direct_send() + elif username is not None: + member = create_anonymous_member(username, self.room, self.db) + if member is None: + await self.direct_send(type="error", data={"msg": "Nom d'utilisateur indisponible"}) + return + self.member = member + self.add_to_group() + await self.direct_send() + + def isAdmin(self): + return self.member is not None and self.member.is_admin == True + + @Consumer.event('accept', conditions=[isAdmin]) + async def accept(self, waiter_id: str): + waiter = get_waiter(waiter_id, self.db) + member = accept_waiter(waiter, self.db) + await self.manager.broadcast({"type": "accepted", "data": {'member': serialize_member(member).dict()}}, f"waiter__{waiter_id}") + await self.broadcast(type="joined", payload={"member": serialize_member(member).dict()}) + @Consumer.event('refuse', conditions=[isAdmin]) + async def accept(self, waiter_id: str): + waiter = get_waiter(waiter_id, self.db) + member = refuse_waiter(waiter, self.db) + await self.manager.broadcast({"type": "refused", "data": {'waiter_id': waiter_id}}, f"waiter__{waiter_id}") + #await self.broadcast(type="joined", payload={"member": serialize_member(member).dict()}) + + @Consumer.event('ping_room') + async def proom(self): + await self.broadcast(type='ping', payload={}) + + def isMember(self): + return self.member is not None and self.member.waiting == False + + # Sending Events + @Consumer.sending("joined", conditions=[isMember]) + def joined(self, member: MemberRead): + print('MEMBER', self.member, member) + if (self.member.user is not None and member.username == self.member.user.username) or (self.member.anonymous and member.reconnect_code == self.member.anonymous.reconnect_code): + raise ValueError("Nope") + if self.member.is_admin == False: + member.reconnect_code = "" + return {"member": member} + + @Consumer.sending('waiter', conditions=[isAdmin]) + def waiter(self, waiter: Waiter): + return {"waiter": waiter} + + @Consumer.sending('ping', conditions=[isMember]) + def ping(self): + return {} + async def disconnect(self): - await self.disconnect_self() - + self.manager.remove(self.room.id, self) + #await self.disconnect_self() + + @router.websocket('/ws/room/{room_id}') async def room_ws(ws: WebSocket, room: Room | None = Depends(check_room), db: Session = Depends(get_session)): if room is None: @@ -255,3 +587,32 @@ async def room_ws(ws: WebSocket, room: Room | None = Depends(check_room), db: Se status_code=status.HTTP_404_NOT_FOUND, detail='Room not found') consumer = RoomConsumer(ws=ws, room=room, manager=manager, db=db) await consumer.run() + + +class TestConsumer(Consumer): + async def connect(self): + await self.ws.accept() + + def test(self): + return True + + @Consumer.event("test", conditions=[True, test]) + async def testering(self): + await self.ws.send_json({"type": "success"}) + await self.send({"type": "test", "data": {"i": {"username": "lilian", "reconnect_code": "something", "isAdmin": False, "isUser": False}, "test": 12}}) + # await self.send({"type": "test", "data": {"i": {"username": "lilian", "reconnect_code": "something", "isAdmin": False, "isUser": False}}}) + return + + @Consumer.sending('test', conditions=[]) + def sendtest(self, i: MemberRead, test: int): + print("i", i) + print(i.reconnect_code) + print(dict(i)) + i.reconnect_code = "nope" + return {"i": i, "test": test} + + +@router.websocket('/ws/test') +async def test(ws: WebSocket): + consumer = TestConsumer(ws) + await consumer.run() diff --git a/backend/api/testing.py b/backend/api/testing.py index e69de29..1118ce0 100644 --- a/backend/api/testing.py +++ b/backend/api/testing.py @@ -0,0 +1,14 @@ +from pydantic import * + +class Model(BaseModel): + test: str = "test" + i: int + + +d = {'title': 'Sendtest', 'type': 'object', 'properties': {'i': { + 'title': 'I', 'type': 'integer'}}, 'required': ['i'], 'additionalProperties': False} + +props = {(k) for k,v in d['properties']} + +obj = create_model("model", foo=(str, "str")) +print(Model(i=12, l=12)) \ No newline at end of file diff --git a/backend/api/tests/test_room.py b/backend/api/tests/test_room.py index 4ba237b..7583213 100644 --- a/backend/api/tests/test_room.py +++ b/backend/api/tests/test_room.py @@ -3,24 +3,26 @@ from fastapi.testclient import TestClient from tests.test_auth import test_register -def test_create_room_no_auth(client: TestClient): +def test_create_room_no_auth(client: TestClient, public=False): r = client.post('/room', json={"name": "test_room", - "public": False}, params={'username': "lilian"}) + "public": False}, params={'username': "lilian", "public": public}) print(r.json()) assert "id_code" in r.json()['room'] - assert "reconnect_code" in r.json()['member']['anonymous'] - assert {"room": {**r.json()['room'], 'id_code': None}, "member": {**r.json()['member'], "anonymous": {**r.json()['member']['anonymous'], "reconnect_code": None}}} == {"room": {"id_code": None, "name": "test_room", - "public": False}, 'member': {"anonymous": {"username": "lilian", "reconnect_code": None}, "user": None}} + assert r.json()['member']['reconnect_code'] is not None + assert {"room": {**r.json()['room'], 'id_code': None}, "member": {**r.json()['member'], "reconnect_code": None}} == {"room": {"id_code": None, "name": "test_room", + "public": False}, 'member': {"username": "lilian", "reconnect_code": None, "isUser": False, "isAdmin": True}} return r.json() + def test_create_room_no_auth_invalid(client: TestClient): r = client.post('/room', json={"name": "test_room"*21, "public": False}, params={'username': "lilian"*21}) print(r.json()) assert r.json() == {'detail': {'username_error': 'ensure this value has at most 20 characters', 'name_error': 'ensure this value has at most 20 characters'}} - -def test_create_room_auth(client: TestClient, token = None): + + +def test_create_room_auth(client: TestClient, token=None): if token is None: token = test_register(client=client)['access'] r = client.post('/room', json={"name": "test_room", @@ -28,52 +30,408 @@ def test_create_room_auth(client: TestClient, token = None): print(r.json()) assert "id_code" in r.json()['room'] assert {**r.json(), "room": {**r.json()['room'], 'id_code': None}} == {"room": {"id_code": None, "name": "test_room", - "public": False}, 'member': {"user": {"username": "lilian"}, "anonymous": None}} + "public": False}, 'member': {"username": "lilian", "reconnect_code": "", "isUser": True, "isAdmin": True}} return r.json() + def test_room_not_found(client: TestClient): try: with client.websocket_connect('/ws/room/eee') as r: pass - except HTTPException as e : + except HTTPException as e: assert True except Exception: assert False - + + def test_login_no_auth(client: TestClient): room = test_create_room_no_auth(client=client) - member = room['member']['anonymous'] + member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as ws: - ws.send_json({"type": "login", "data": {"reconnect_code": member['reconnect_code']}}) + ws.send_json({"type": "login", "data": { + "reconnect_code": member['reconnect_code']}}) data = ws.receive_json() print(data) - assert data == {'type': "loggedIn", "data": {"member": {"username": member['username'], "reconnect_code": member['reconnect_code'], "isAdmin": True}}} + assert data == {'type': "loggedIn", "data": {"member": { + "username": member['username'], "reconnect_code": member['reconnect_code'], "isAdmin": True, "isUser": False}}} + + +def test_login_no_auth_not_in_room(client: TestClient): + room = test_create_room_no_auth(client=client) + member = room['member'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as ws: + ws.send_json({"type": "login", "data": { + "reconnect_code": "lol"}}) + data = ws.receive_json() + print(data) + assert data == {'type': "error", "data": { + "msg": "Utilisateur introuvable dans cette salle"}} + def test_login_auth(client: TestClient): token = test_register(client=client)['access'] room = test_create_room_auth(client=client, token=token) - member = room['member']['user'] + member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as ws: ws.send_json({"type": "login", "data": {"token": token}}) data = ws.receive_json() print(data) - assert data == {'type': "loggedIn", "data": {"member": {"username": member['username'], "isAdmin": True}}} + assert data == {'type': "loggedIn", "data": {"member": { + "username": member['username'], "isAdmin": True, "isUser": True, 'reconnect_code': ""}}} -def test_join_no_auth(client: TestClient): + +def test_login_auth_not_in_room(client: TestClient): + token = test_register(client=client, username="lilian2")['access'] + room = test_create_room_auth(client=client) + member = room['member'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as ws: + ws.send_json({"type": "login", "data": {"token": token}}) + data = ws.receive_json() + print(data) + assert data == {'type': "error", "data": { + "msg": "Utilisateur introuvable dans cette salle"}} + + +def test_join_auth(client: TestClient): + token = test_register(client, username="lilian2")['access'] room = test_create_room_no_auth(client=client) - member = room['member']['anonymous'] + member = room['member'] with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: admin.send_json({"type": "login", "data": { "reconnect_code": member['reconnect_code']}}) + admin.receive_json() with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: - member.send_json({"type":"join", "data": {"username": "member"}}) + member.send_json({"type": "join", "data": {"token": token}}) + mdata = member.receive_json() + assert "waiter_id" in mdata['data']['waiter'] + assert mdata == {"type": "waiting", "data": {"waiter": { + "username": "lilian2", "waiter_id": mdata['data']['waiter']['waiter_id']}}} + + adata = admin.receive_json() + assert adata == {'type': "waiter", 'data': { + "waiter": {"waiter_id": mdata['data']['waiter']['waiter_id'], "username": "lilian2"}}} + + admin.send_json({"type": "accept", "data": { + "waiter_id": mdata['data']['waiter']['waiter_id']}}) + mdata = member.receive_json() + assert mdata == {"type": "accepted", "data": {"member": { + "username": "lilian2", "isUser": True, "isAdmin": False, "reconnect_code": ""}}} + adata = admin.receive_json() + assert adata == {'type': "joined", 'data': { + "member": {"reconnect_code": "", "username": "lilian2", "isUser": True, "isAdmin": False}}} + admin.send_json({"type": "ping_room"}) + mdata = member.receive_json() + assert mdata == {"type": "ping", "data": {}} + +def test_join_no_auth(client: TestClient): + room = test_create_room_no_auth(client=client) + member = room['member'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": member['reconnect_code']}}) + admin.receive_json() + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as memberws: + memberws.send_json( + {"type": "join", "data": {"username": "member"}}) + mdata = memberws.receive_json() + assert "waiter_id" in mdata['data']['waiter'] + assert mdata == {"type": "waiting", "data": {"waiter": { + "username": "member", "waiter_id": mdata['data']['waiter']['waiter_id']}}} + + adata = admin.receive_json() + assert adata == {'type': "waiter", 'data': { + "waiter": {"waiter_id": mdata['data']['waiter']['waiter_id'], "username": "member"}}} + + admin.send_json({"type": "accept", "data": { + "waiter_id": mdata['data']['waiter']['waiter_id']}}) + mdata = memberws.receive_json() + new_reconnect = mdata['data']['member']['reconnect_code'] + assert 'reconnect_code' in mdata['data']['member'] + assert mdata == {"type": "accepted", "data": {"member": { + "username": "member", "reconnect_code": new_reconnect, "isUser": False, "isAdmin": False}}} + adata = admin.receive_json() + assert adata == {'type': "joined", 'data': { + "member": {"reconnect_code": new_reconnect, "username": "member", "isUser": False, "isAdmin": False}}} + admin.send_json({"type": "ping_room"}) + mdata = memberws.receive_json() + assert mdata == {"type": "ping", "data": {}} + + return {"room": room, "members": [member['reconnect_code'], new_reconnect]} + + +def test_join_no_auth_username_error(client: TestClient): + room = test_create_room_no_auth(client=client) + member = room['member'] + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: + member.send_json({"type": "join", "data": {"username": "lilian"}}) + mdata = member.receive_json() + assert mdata == {"type": "error", "data": { + "msg": "Nom d'utilisateur invalide ou indisponible"}} + + +def test_join_no_auth_username_too_long(client: TestClient): + room = test_create_room_no_auth(client=client) + member = room['member'] + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: + member.send_json({"type": "join", "data": {"username": "lilian"*21}}) + mdata = member.receive_json() + assert mdata == {"type": "error", "data": {"msg": "Nom d'utilisateur invalide ou indisponible"}} + + + + +def test_join_auth_refused(client: TestClient): + token = test_register(client, username="lilian2")['access'] + room = test_create_room_no_auth(client=client) + member = room['member'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": member['reconnect_code']}}) + admin.receive_json() + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: + member.send_json({"type": "join", "data": {"token": token}}) + mdata = member.receive_json() + assert "waiter_id" in mdata['data']['waiter'] + assert mdata == {"type": "waiting", "data": {"waiter": { + "username": "lilian2", "waiter_id": mdata['data']['waiter']['waiter_id']}}} + waiter_id = mdata['data']['waiter']['waiter_id'] + adata = admin.receive_json() + assert adata == {'type': "waiter", 'data': { + "waiter": {"waiter_id": waiter_id, "username": "lilian2"}}} + admin.send_json({"type": "refuse", "data": { + "waiter_id": waiter_id}}) + + mdata = member.receive_json() + assert mdata == {"type": "refused", "data": { + "waiter_id": waiter_id}} + + +def test_join_auth_in_room_yet(client: TestClient): + token = test_register(client, username="lilian")['access'] + room = test_create_room_auth(client=client, token=token) + member = room['member'] + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: + member.send_json({"type": "join", "data": {"token": token}}) + mdata = member.receive_json() + assert mdata == {"type": "loggedIn", "data": {"member": { + "username": "lilian2", "isAdmin": True, "isUser": True}}} + + +def test_join_auth_public(client: TestClient): + token = test_register(client, username="lilian2")['access'] + room = test_create_room_no_auth(client=client, public=True) + member = room['member'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": member['reconnect_code']}}) + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: + member.send_json({"type": "join", "data": {"token": token}}) + mdata = member.receive_json() + + assert mdata == {"type": "accepted", "data": {"member": { + "username": "member", "isUser": True, "isAdmin": False}}} + adata = admin.receive_json() + assert adata == {'type': "joined", 'data': { + "member": {"reconnect_code": "", "username": "member", "isUser": True}}} + admin.send_json({"type": "ping_room"}) + mdata = member.receive_json() + assert mdata == {"type": "ping"} + + +def test_join_no_auth_public(client: TestClient): + room = test_create_room_no_auth(client=client, public=True) + member = room['member'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": member['reconnect_code']}}) + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: + member.send_json({"type": "join", "data": {"username": "member"}}) + mdata = member.receive_json() + + assert 'reconnect_code' in mdata['data']['member'] + assert mdata == {"type": "accepted", "data": {"member": { + "username": "member", "reconnect_code": mdata['data']['reconnect_code'], "isUser": False, "isAdmin": False}}} + + adata = admin.receive_json() + assert adata == {'type': "joined", 'data': { + "member": {"reconnect_code": mdata['data']['reconnect_code'], "username": "member", "isUser": False}}} + + member.send_json({"type": "update_groups"}) + admin.send_json({"type": "ping_room"}) + mdata = member.receive_json() + assert mdata == {"type": "ping"} + + +def test_join_no_auth_unauthorized(client: TestClient): + token = test_register(client, username="lilian2")['access'] + room = test_create_room_no_auth(client=client) + member = room['member'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": member['reconnect_code']}}) + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: + member.send_json({"type": "join", "data": {"token": token}}) mdata = member.receive_json() assert "id_code" in mdata['data']['waiter'] assert mdata == {"type": "waiting", "data": {"waiter": { - "username": "member", "id_code": mdata['data']['waiter']}}} - + "username": "lilian2", "id_code": mdata['data']['waiter']}}} + adata = admin.receive_json() assert adata == {'type': "waiter", 'data': { - "waiter": {"id_code": mdata['data']['waiter'], "username": "member"}}} - - admin.send({"type": "accept", "data": {"waiter_id": mdata['data']['waiter']}}) \ No newline at end of file + "waiter": {"id_code": mdata['data']['waiter']['id_code'], "username": "member"}}} + + member.send_json({"type": "refuse", "data": { + "waiter_id": mdata['data']['waiter']['id_code']}}) + + mdata = member.receive_json() + assert mdata == {"type": "error", "data": { + "msg": "Vous n'avez pas la permission de faire ca"}} + + +def test_connect_admin(client: TestClient): + room = test_join_no_auth(client=client) + members = room['members'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": members[0]}}) + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as member: + member.send_json({'type': "login", "data": { + "reconnect_code": members[0]}}) + + adata = admin.receive_json() + assert adata == {"type": "connect", "data": {"member": { + "username": "member", "reconnect_code": members[1], "isAdmin": False, "isUser": False}}} + + +def test_connect_member(client: TestClient): + room = test_join_no_auth(client=client) + members = room['members'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as memberws: + memberws.send_json({"type": "login", "data": { + "reconnect_code": members[1]}}) + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({'type': "login", "data": { + "reconnect_code": members[0]}}) + + mdata = memberws.receive_json() + assert mdata == {"type": "connect", "data": {"member": { + "username": "member", "reconnect_code": "", "isAdmin": True, "isUser": False}}} + + +def test_disconnect(client: TestClient): + room = test_join_no_auth(client=client) + members = room['members'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as memberws: + memberws.send_json({"type": "login", "data": { + "reconnect_code": members[1]}}) + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({'type': "login", "data": { + "reconnect_code": members[0]}}) + + admin.close() + + mdata = memberws.receive_json() + assert mdata == {"type": "disconnect", "data": {"member": { + "username": "member", "reconnect_code": "", "isAdmin": True, "isUser": False}}} + + +def test_leave(client: TestClient): + room = test_join_no_auth(client=client) + members = room['members'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": members[1]}}) + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as memberws: + memberws.send_json({"type": "login", "data": { + "reconnect_code": members[1]}}) + memberws.send_json({"type": "leave"}) + data = memberws.receive_json() + assert data == {"type": "successfully_leaved"} + + adata = admin.receive_json() + assert adata == {"type": "leaved", "data": {"member": { + "username": "member", "reconnect_code": members[1], "isAdmin": False, "isUser": False}}} + + +def test_leave_not_connected(client: TestClient): + room = test_create_room_no_auth(client=client) + members = room['members'] + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as memberws: + + memberws.send_json({"type": "leave"}) + data = memberws.receive_json() + assert data == {"type": "error", "data": { + "msg": "Vous n'êtes connecté à aucune salle"}} + + +def test_leave_admin(client: TestClient): + room = test_join_no_auth(client=client) + members = room['members'] + + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": members[0]}}) + admin.send_json({"type": "leave"}) + data = admin.receive_json() + assert data == {"type": "error", "data": { + "msg": "Vous ne pouvez pas quitter une salle dont vous êtes l'administrateur"}} + + +def test_ban_anonymous(client: TestClient): + room = test_create_room_no_auth(client=client) + members = room['members'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": members[1]}}) + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as memberws: + memberws.send_json({"type": "login", "data": { + "reconnect_code": members[1]}}) + admin.send_json({"type": "ban", "data": {"member": { + "username": "member", "reconnect_code": members[1], "isUser": False, "isAdmin": False}}}) + adata = admin.receive_json() + assert adata == {"type": "leaved", "data": {"member": { + "username": "member", "reconnect_code": members[1], "isUser": True, "isAdmin": False}}} + + +def test_ban_anonymous_unauthorized(client: TestClient): + room = test_create_room_no_auth(client=client) + members = room['members'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": members[1]}}) + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as memberws: + memberws.send_json({"type": "login", "data": { + "reconnect_code": members[1]}}) + memberws.send_json({"type": "ban", "data": {"member": { + "username": "member", "reconnect_code": members[1], "isUser": False, "isAdmin": False}}}) + mdata = memberws.receive_json() + assert mdata == {"type": "error", "data": { + "msg": "Vous n'avez pas les permissions pour faire ça"}} + + +def test_ban_user(client: TestClient): + token = test_register(client=client, username="lilian2") + room = test_create_room_no_auth(client=client) + members = room['member'] + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as admin: + admin.send_json({"type": "login", "data": { + "reconnect_code": members["reconnect_code "]}}) + with client.websocket_connect(f"/ws/room/" + room['room']['id_code']) as memberws: + memberws.send_json({"type": "join", "data": {"token": token}}) + adata = admin.receive_json() + + admin.send_json({"type": "ban", "data": {"member": { + "username": "lilian2", "reconnect_code": "", "isUser": True, "isAdmin": False}}}) + adata = admin.receive_json() + assert adata == {"type": "leaved", "data": {"member": { + "username": "lilian2", "reconnect_code": "", "isUser": True, "isAdmin": False}}}