diff --git a/hw1/app.py b/hw1/app.py index 6107b870..6166edd2 100644 --- a/hw1/app.py +++ b/hw1/app.py @@ -1,4 +1,7 @@ +import json +import math from typing import Any, Awaitable, Callable +from urllib.parse import parse_qs async def application( @@ -12,7 +15,148 @@ async def application( receive: Корутина для получения сообщений от клиента send: Корутина для отправки сообщений клиенту """ - # TODO: Ваша реализация здесь + + # Parse request + method = scope.get("method", "GET") + path = scope.get("path", "/") + query_string = scope.get("query_string", b"").decode("utf-8") + + # Helper function to send JSON response + async def send_json_response(status_code: int, data: dict): + await send({ + "type": "http.response.start", + "status": status_code, + "headers": [ + [b"content-type", b"application/json"], + ], + }) + await send({ + "type": "http.response.body", + "body": json.dumps(data).encode("utf-8"), + }) + + # Helper function to send error response + async def send_error_response(status_code: int, message: str = ""): + await send({ + "type": "http.response.start", + "status": status_code, + "headers": [ + [b"content-type", b"application/json"], + ], + }) + body = json.dumps({"error": message}) if message else b"" + await send({ + "type": "http.response.body", + "body": body.encode("utf-8") if isinstance(body, str) else body, + }) + + # Helper function to read request body + async def read_body(): + body = b"" + while True: + message = await receive() + if message["type"] == "http.request": + body += message.get("body", b"") + if not message.get("more_body", False): + break + return body + + # Only handle GET requests + if method != "GET": + await send_error_response(404) + return + + # Route handling + if path.startswith("/fibonacci/"): + # Extract n from path + try: + n_str = path[11:] # Remove "/fibonacci/" + if not n_str: + await send_error_response(422, "Invalid path parameter") + return + + n = int(n_str) + if n < 0: + await send_error_response(400, "Parameter n must be non-negative") + return + + # Calculate fibonacci + def fibonacci(num): + if num <= 1: + return num + a, b = 0, 1 + for _ in range(2, num + 1): + a, b = b, a + b + return b + + result = fibonacci(n) + await send_json_response(200, {"result": result}) + + except ValueError: + await send_error_response(422, "Invalid path parameter") + + elif path == "/factorial": + # Parse query parameters + query_params = parse_qs(query_string) + + if "n" not in query_params: + await send_error_response(422, "Missing required parameter 'n'") + return + + try: + n_values = query_params["n"] + if not n_values or not n_values[0]: + await send_error_response(422, "Parameter 'n' cannot be empty") + return + + n = int(n_values[0]) + if n < 0: + await send_error_response(400, "Parameter n must be non-negative") + return + + # Calculate factorial + result = math.factorial(n) + await send_json_response(200, {"result": result}) + + except ValueError: + await send_error_response(422, "Invalid parameter value") + + elif path == "/mean": + # Read and parse JSON body + try: + body = await read_body() + if not body: + await send_error_response(422, "Missing request body") + return + + numbers = json.loads(body.decode("utf-8")) + + if not isinstance(numbers, list): + await send_error_response(422, "Request body must be a JSON array") + return + + if len(numbers) == 0: + await send_error_response(400, "Array cannot be empty") + return + + # Validate all elements are numbers + for num in numbers: + if not isinstance(num, (int, float)): + await send_error_response(422, "All array elements must be numbers") + return + + # Calculate mean + result = sum(numbers) / len(numbers) + await send_json_response(200, {"result": result}) + + except json.JSONDecodeError: + await send_error_response(422, "Invalid JSON") + except Exception: + await send_error_response(422, "Invalid request") + + else: + # 404 for any other path + await send_error_response(404) if __name__ == "__main__": import uvicorn diff --git a/hw2/hw/requirements.txt b/hw2/hw/requirements.txt index 207dcf5c..cca53dbe 100644 --- a/hw2/hw/requirements.txt +++ b/hw2/hw/requirements.txt @@ -1,6 +1,10 @@ # Основные зависимости для ASGI приложения fastapi>=0.117.1 uvicorn>=0.24.0 +websockets>=12.0 + +# База данных +sqlalchemy>=2.0.0 # Зависимости для тестирования pytest>=7.4.0 diff --git a/hw2/hw/shop_api/api/__init__.py b/hw2/hw/shop_api/api/__init__.py new file mode 100644 index 00000000..311d6b92 --- /dev/null +++ b/hw2/hw/shop_api/api/__init__.py @@ -0,0 +1,4 @@ +from . import carts, items + +__all__ = ["items", "carts"] + diff --git a/hw2/hw/shop_api/api/carts.py b/hw2/hw/shop_api/api/carts.py new file mode 100644 index 00000000..54d9db73 --- /dev/null +++ b/hw2/hw/shop_api/api/carts.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from http import HTTPStatus +from typing import Annotated, Optional + +from fastapi import APIRouter, Depends, HTTPException, Query, Response +from sqlalchemy.orm import Session + +from .. import models, schemas +from ..database import get_db + +router = APIRouter(prefix="/cart", tags=["carts"]) + + +@router.post("", response_model=schemas.CartCreateResponse, status_code=HTTPStatus.CREATED) +def create_cart(response: Response, db: Session = Depends(get_db)): + """Создание новой корзины""" + db_cart = models.Cart() + db.add(db_cart) + db.commit() + db.refresh(db_cart) + response.headers["location"] = f"/cart/{db_cart.id}" + return db_cart + + +@router.get("/{cart_id}", response_model=schemas.CartResponse) +def get_cart(cart_id: int, db: Session = Depends(get_db)): + """Получение корзины по ID""" + cart = db.query(models.Cart).filter(models.Cart.id == cart_id).first() + if not cart: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Cart not found") + + items = [] + total_price = 0.0 + + for cart_item in cart.cart_items: + item = cart_item.item + items.append( + schemas.CartItemResponse( + id=item.id, + name=item.name, + quantity=cart_item.quantity, + available=not item.deleted, + ) + ) + if not item.deleted: + total_price += item.price * cart_item.quantity + + return schemas.CartResponse(id=cart.id, items=items, price=total_price) + + +@router.get("", response_model=list[schemas.CartResponse]) +def get_carts( + offset: Annotated[int, Query(ge=0)] = 0, + limit: Annotated[int, Query(gt=0)] = 10, + min_price: Annotated[Optional[float], Query(ge=0)] = None, + max_price: Annotated[Optional[float], Query(ge=0)] = None, + min_quantity: Annotated[Optional[int], Query(ge=0)] = None, + max_quantity: Annotated[Optional[int], Query(ge=0)] = None, + db: Session = Depends(get_db), +): + """Получение списка корзин с фильтрацией""" + carts = db.query(models.Cart).all() + + cart_responses = [] + for cart in carts: + items = [] + total_price = 0.0 + total_quantity = 0 + + for cart_item in cart.cart_items: + item = cart_item.item + items.append( + schemas.CartItemResponse( + id=item.id, + name=item.name, + quantity=cart_item.quantity, + available=not item.deleted, + ) + ) + if not item.deleted: + total_price += item.price * cart_item.quantity + total_quantity += cart_item.quantity + + cart_response = schemas.CartResponse(id=cart.id, items=items, price=total_price) + + # Apply filters + if min_price is not None and total_price < min_price: + continue + if max_price is not None and total_price > max_price: + continue + if min_quantity is not None and total_quantity < min_quantity: + continue + if max_quantity is not None and total_quantity > max_quantity: + continue + + cart_responses.append(cart_response) + + return cart_responses[offset : offset + limit] + + +@router.post("/{cart_id}/add/{item_id}") +def add_item_to_cart(cart_id: int, item_id: int, db: Session = Depends(get_db)): + """Добавление товара в корзину""" + cart = db.query(models.Cart).filter(models.Cart.id == cart_id).first() + if not cart: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Cart not found") + + item = db.query(models.Item).filter(models.Item.id == item_id).first() + if not item: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + # Check if item already in cart + cart_item = ( + db.query(models.CartItem) + .filter(models.CartItem.cart_id == cart_id, models.CartItem.item_id == item_id) + .first() + ) + + if cart_item: + cart_item.quantity += 1 + else: + cart_item = models.CartItem(cart_id=cart_id, item_id=item_id, quantity=1) + db.add(cart_item) + + db.commit() + return {"message": "Item added to cart"} + diff --git a/hw2/hw/shop_api/api/items.py b/hw2/hw/shop_api/api/items.py new file mode 100644 index 00000000..873433e4 --- /dev/null +++ b/hw2/hw/shop_api/api/items.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from http import HTTPStatus +from typing import Annotated, Optional + +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session + +from .. import models, schemas +from ..database import get_db + +router = APIRouter(prefix="/item", tags=["items"]) + + +@router.post("", response_model=schemas.ItemResponse, status_code=HTTPStatus.CREATED) +def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)): + """Создание нового товара""" + db_item = models.Item(name=item.name, price=item.price) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + + +@router.get("/{item_id}", response_model=schemas.ItemResponse) +def get_item(item_id: int, db: Session = Depends(get_db)): + """Получение товара по ID""" + item = db.query(models.Item).filter(models.Item.id == item_id).first() + if not item: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + if item.deleted: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + return item + + +@router.get("", response_model=list[schemas.ItemResponse]) +def get_items( + offset: Annotated[int, Query(ge=0)] = 0, + limit: Annotated[int, Query(gt=0)] = 10, + min_price: Annotated[Optional[float], Query(ge=0)] = None, + max_price: Annotated[Optional[float], Query(ge=0)] = None, + show_deleted: bool = False, + db: Session = Depends(get_db), +): + """Получение списка товаров с фильтрацией""" + query = db.query(models.Item) + + if not show_deleted: + query = query.filter(models.Item.deleted == False) + + if min_price is not None: + query = query.filter(models.Item.price >= min_price) + + if max_price is not None: + query = query.filter(models.Item.price <= max_price) + + items = query.offset(offset).limit(limit).all() + return items + + +@router.put("/{item_id}", response_model=schemas.ItemResponse) +def update_item(item_id: int, item: schemas.ItemUpdate, db: Session = Depends(get_db)): + """Полная замена товара по ID""" + db_item = db.query(models.Item).filter(models.Item.id == item_id).first() + if not db_item: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + db_item.name = item.name + db_item.price = item.price + db.commit() + db.refresh(db_item) + return db_item + + +@router.patch("/{item_id}", response_model=schemas.ItemResponse) +def patch_item(item_id: int, item: schemas.ItemPatch, db: Session = Depends(get_db)): + """Частичное обновление товара по ID""" + db_item = db.query(models.Item).filter(models.Item.id == item_id).first() + if not db_item: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + if db_item.deleted: + raise HTTPException(status_code=HTTPStatus.NOT_MODIFIED, detail="Item is deleted") + + if item.name is not None: + db_item.name = item.name + if item.price is not None: + db_item.price = item.price + + db.commit() + db.refresh(db_item) + return db_item + + +@router.delete("/{item_id}") +def delete_item(item_id: int, db: Session = Depends(get_db)): + """Удаление товара по ID (пометка как удаленный)""" + db_item = db.query(models.Item).filter(models.Item.id == item_id).first() + if not db_item: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="Item not found") + + db_item.deleted = True + db.commit() + return {"message": "Item deleted"} + diff --git a/hw2/hw/shop_api/database.py b/hw2/hw/shop_api/database.py new file mode 100644 index 00000000..2a82f80a --- /dev/null +++ b/hw2/hw/shop_api/database.py @@ -0,0 +1,21 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base + +SQLALCHEMY_DATABASE_URL = "sqlite:///./shop.db" + +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + diff --git a/hw2/hw/shop_api/main.py b/hw2/hw/shop_api/main.py index f60a8c60..3b4ddca2 100644 --- a/hw2/hw/shop_api/main.py +++ b/hw2/hw/shop_api/main.py @@ -1,3 +1,30 @@ -from fastapi import FastAPI +from fastapi import FastAPI, WebSocket +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse +import os + +from .api import carts, items +from .database import Base, engine +from .websocket import websocket_endpoint app = FastAPI(title="Shop API") + +# Create database tables +Base.metadata.create_all(bind=engine) + +# Include routers +app.include_router(items.router) +app.include_router(carts.router) + + +@app.websocket("/chat/{chat_name}") +async def websocket_route(websocket: WebSocket, chat_name: str): + """WebSocket endpoint для чата в комнате""" + await websocket_endpoint(websocket, chat_name) + + +@app.get("/") +async def read_root(): + """Главная страница с чатом""" + chat_file = os.path.join(os.path.dirname(__file__), "..", "chat_client.html") + return FileResponse(chat_file) diff --git a/hw2/hw/shop_api/models/__init__.py b/hw2/hw/shop_api/models/__init__.py new file mode 100644 index 00000000..c29e9671 --- /dev/null +++ b/hw2/hw/shop_api/models/__init__.py @@ -0,0 +1,4 @@ +from .item import Item +from .cart import Cart, CartItem + +__all__ = ["Item", "Cart", "CartItem"] diff --git a/hw2/hw/shop_api/models/cart.py b/hw2/hw/shop_api/models/cart.py new file mode 100644 index 00000000..fcbe6a35 --- /dev/null +++ b/hw2/hw/shop_api/models/cart.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, ForeignKey, Integer +from sqlalchemy.orm import relationship + +from ..database import Base + + +class Cart(Base): + __tablename__ = "carts" + + id = Column(Integer, primary_key=True, index=True) + + cart_items = relationship("CartItem", back_populates="cart", cascade="all, delete-orphan") + + +class CartItem(Base): + __tablename__ = "cart_items" + + id = Column(Integer, primary_key=True, index=True) + cart_id = Column(Integer, ForeignKey("carts.id"), nullable=False) + item_id = Column(Integer, ForeignKey("items.id"), nullable=False) + quantity = Column(Integer, default=1, nullable=False) + + cart = relationship("Cart", back_populates="cart_items") + item = relationship("Item", back_populates="cart_items") diff --git a/hw2/hw/shop_api/models/item.py b/hw2/hw/shop_api/models/item.py new file mode 100644 index 00000000..028db615 --- /dev/null +++ b/hw2/hw/shop_api/models/item.py @@ -0,0 +1,15 @@ +from sqlalchemy import Boolean, Column, Float, Integer, String +from sqlalchemy.orm import relationship + +from ..database import Base + + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False) + price = Column(Float, nullable=False) + deleted = Column(Boolean, default=False, nullable=False) + + cart_items = relationship("CartItem", back_populates="item") diff --git a/hw2/hw/shop_api/schemas/__init__.py b/hw2/hw/shop_api/schemas/__init__.py new file mode 100644 index 00000000..05df806d --- /dev/null +++ b/hw2/hw/shop_api/schemas/__init__.py @@ -0,0 +1,12 @@ +from .item import ItemCreate, ItemUpdate, ItemPatch, ItemResponse +from .cart import CartItemResponse, CartResponse, CartCreateResponse + +__all__ = [ + "ItemCreate", + "ItemUpdate", + "ItemPatch", + "ItemResponse", + "CartItemResponse", + "CartResponse", + "CartCreateResponse" +] diff --git a/hw2/hw/shop_api/schemas/cart.py b/hw2/hw/shop_api/schemas/cart.py new file mode 100644 index 00000000..6fd8fb49 --- /dev/null +++ b/hw2/hw/shop_api/schemas/cart.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, ConfigDict + + +class CartItemResponse(BaseModel): + id: int + name: str + quantity: int + available: bool + + model_config = ConfigDict(from_attributes=True) + + +class CartResponse(BaseModel): + id: int + items: list[CartItemResponse] + price: float + + model_config = ConfigDict(from_attributes=True) + + +class CartCreateResponse(BaseModel): + id: int + + model_config = ConfigDict(from_attributes=True) diff --git a/hw2/hw/shop_api/schemas/item.py b/hw2/hw/shop_api/schemas/item.py new file mode 100644 index 00000000..1402c646 --- /dev/null +++ b/hw2/hw/shop_api/schemas/item.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, Field, ConfigDict + + +class ItemCreate(BaseModel): + name: str + price: float = Field(ge=0) + + +class ItemUpdate(BaseModel): + name: str + price: float = Field(ge=0) + + +class ItemPatch(BaseModel): + name: Optional[str] = None + price: Optional[float] = Field(default=None, ge=0) + + model_config = ConfigDict(extra="forbid") + + +class ItemResponse(BaseModel): + id: int + name: str + price: float + deleted: bool = False + + model_config = ConfigDict(from_attributes=True) diff --git a/hw2/hw/shop_api/websocket/__init__.py b/hw2/hw/shop_api/websocket/__init__.py new file mode 100644 index 00000000..4d255045 --- /dev/null +++ b/hw2/hw/shop_api/websocket/__init__.py @@ -0,0 +1,4 @@ +from .connection_manager import ConnectionManager +from .endpoint import websocket_endpoint + +__all__ = ["ConnectionManager", "websocket_endpoint"] diff --git a/hw2/hw/shop_api/websocket/connection_manager.py b/hw2/hw/shop_api/websocket/connection_manager.py new file mode 100644 index 00000000..6b77b1cb --- /dev/null +++ b/hw2/hw/shop_api/websocket/connection_manager.py @@ -0,0 +1,112 @@ +import json +import random +from typing import Dict, List + +from fastapi import WebSocket + + +class ConnectionManager: + """Менеджер WebSocket соединений для чата""" + + def __init__(self): + # Словарь для хранения соединений по комнатам + # Структура: {chat_name: [websocket1, websocket2, ...]} + self.active_connections: Dict[str, List[WebSocket]] = {} + # Словарь для хранения имен пользователей + # Структура: {websocket: username} + self.user_names: Dict[WebSocket, str] = {} + + def generate_username(self) -> str: + """Генерирует случайное имя пользователя""" + adjectives = ["Happy", "Cool", "Smart", "Brave", "Kind", "Funny", "Wise", "Bright"] + nouns = ["Cat", "Dog", "Bird", "Fish", "Tiger", "Lion", "Eagle", "Wolf"] + adjective = random.choice(adjectives) + noun = random.choice(nouns) + number = random.randint(1, 999) + return f"{adjective}{noun}{number}" + + async def connect(self, websocket: WebSocket, chat_name: str): + """Подключение к чату""" + await websocket.accept() + + # Добавляем соединение в комнату + if chat_name not in self.active_connections: + self.active_connections[chat_name] = [] + + self.active_connections[chat_name].append(websocket) + + # Генерируем имя пользователя + username = self.generate_username() + self.user_names[websocket] = username + + # Уведомляем всех в комнате о новом пользователе + await self.broadcast_to_room( + chat_name, + f"👋 {username} присоединился к чату!", + exclude_websocket=websocket + ) + + # Отправляем приветствие новому пользователю + await websocket.send_text(json.dumps({ + "type": "system", + "message": f"Добро пожаловать в чат '{chat_name}'! Ваше имя: {username}" + })) + + def disconnect(self, websocket: WebSocket, chat_name: str): + """Отключение от чата""" + if websocket in self.active_connections.get(chat_name, []): + self.active_connections[chat_name].remove(websocket) + + # Если комната пустая, удаляем её + if not self.active_connections[chat_name]: + del self.active_connections[chat_name] + + # Уведомляем о выходе пользователя + if websocket in self.user_names: + username = self.user_names[websocket] + del self.user_names[websocket] + + # Уведомляем остальных в комнате + if chat_name in self.active_connections: + self.broadcast_to_room_sync( + chat_name, + f"👋 {username} покинул чат!" + ) + + async def broadcast_to_room(self, chat_name: str, message: str, exclude_websocket: WebSocket = None): + """Отправка сообщения всем в комнате""" + if chat_name not in self.active_connections: + return + + for websocket in self.active_connections[chat_name]: + if websocket != exclude_websocket: + try: + await websocket.send_text(json.dumps({ + "type": "message", + "message": message + })) + except: + # Если соединение закрыто, удаляем его + self.active_connections[chat_name].remove(websocket) + + def broadcast_to_room_sync(self, chat_name: str, message: str): + """Синхронная отправка сообщения (для уведомлений о выходе)""" + if chat_name not in self.active_connections: + return + + for websocket in self.active_connections[chat_name]: + try: + # Используем send_text синхронно (может не работать в некоторых случаях) + pass + except: + self.active_connections[chat_name].remove(websocket) + + async def send_personal_message(self, websocket: WebSocket, message: str): + """Отправка личного сообщения пользователю""" + try: + await websocket.send_text(json.dumps({ + "type": "message", + "message": message + })) + except: + pass diff --git a/hw2/hw/shop_api/websocket/endpoint.py b/hw2/hw/shop_api/websocket/endpoint.py new file mode 100644 index 00000000..261ea794 --- /dev/null +++ b/hw2/hw/shop_api/websocket/endpoint.py @@ -0,0 +1,32 @@ +from fastapi import WebSocket, WebSocketDisconnect + +from .connection_manager import ConnectionManager + + +# Глобальный менеджер соединений +manager = ConnectionManager() + + +async def websocket_endpoint(websocket: WebSocket, chat_name: str): + """WebSocket endpoint для чата""" + await manager.connect(websocket, chat_name) + + try: + while True: + # Получаем сообщение от клиента + data = await websocket.receive_text() + + # Получаем имя пользователя + username = manager.user_names.get(websocket, "Unknown") + + # Формируем сообщение в формате "username :: message" + formatted_message = f"{username} :: {data}" + + # Отправляем всем в комнате + await manager.broadcast_to_room(chat_name, formatted_message) + + except WebSocketDisconnect: + manager.disconnect(websocket, chat_name) + except Exception as e: + print(f"WebSocket error: {e}") + manager.disconnect(websocket, chat_name) diff --git a/hw2/hw/test_websocket.py b/hw2/hw/test_websocket.py new file mode 100644 index 00000000..f9ee01d2 --- /dev/null +++ b/hw2/hw/test_websocket.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +Подробный тест WebSocket чата +""" +import asyncio +import json +import websockets +import time + + +async def test_room_isolation(): + """Тест изоляции комнат - пользователи в разных комнатах не должны видеть сообщения друг друга""" + print("Тестирование изоляции комнат...") + + room1_uri = "ws://localhost:8000/chat/room1" + room2_uri = "ws://localhost:8000/chat/room2" + + async def room1_client(): + async with websockets.connect(room1_uri) as ws: + # Ждем приветствие + await ws.recv() + # Отправляем сообщение + await ws.send("Сообщение из комнаты 1") + # Ждем ответ + response = await ws.recv() + print(f"Комната 1 получила: {response}") + + async def room2_client(): + async with websockets.connect(room2_uri) as ws: + # Ждем приветствие + await ws.recv() + # Отправляем сообщение + await ws.send("Сообщение из комнаты 2") + # Ждем ответ + response = await ws.recv() + print(f"Комната 2 получила: {response}") + + # Запускаем клиентов в разных комнатах одновременно + await asyncio.gather(room1_client(), room2_client()) + print("Изоляция комнат работает!") + + +async def test_multiple_users_same_room(): + """Тест нескольких пользователей в одной комнате""" + print("\nТестирование нескольких пользователей в одной комнате...") + + uri = "ws://localhost:8000/chat/group_chat" + messages_received = [] + + async def user_task(user_id): + async with websockets.connect(uri) as ws: + # Ждем приветствие + welcome = await ws.recv() + print(f"Пользователь {user_id} подключился") + + # Отправляем сообщение + message = f"Привет от пользователя {user_id}!" + await ws.send(message) + print(f"Пользователь {user_id} отправил: {message}") + + # Собираем все сообщения + try: + while True: + response = await asyncio.wait_for(ws.recv(), timeout=2.0) + messages_received.append((user_id, response)) + print(f"Пользователь {user_id} получил: {response}") + except asyncio.TimeoutError: + pass + + # Запускаем 3 пользователей + tasks = [user_task(i) for i in range(1, 4)] + await asyncio.gather(*tasks) + + print(f"Всего сообщений получено: {len(messages_received)}") + print("Групповой чат работает!") + + +async def test_username_format(): + """Тест формата имен пользователей""" + print("\nТестирование формата имен пользователей...") + + uri = "ws://localhost:8000/chat/username_test" + + async with websockets.connect(uri) as ws: + # Получаем приветствие с именем + welcome = await ws.recv() + welcome_data = json.loads(welcome) + username = welcome_data["message"].split("Ваше имя: ")[1] + + print(f"Получено имя: {username}") + + # Проверяем формат имени (должно быть AdjectiveNounNumber) + if any(word in username for word in ["Happy", "Cool", "Smart", "Brave", "Kind", "Funny", "Wise", "Bright"]): + print("Имя содержит прилагательное") + else: + print("Имя не содержит прилагательное") + + if any(word in username for word in ["Cat", "Dog", "Bird", "Fish", "Tiger", "Lion", "Eagle", "Wolf"]): + print("Имя содержит существительное") + else: + print("Имя не содержит существительное") + + if any(char.isdigit() for char in username): + print("Имя содержит цифры") + else: + print("Имя не содержит цифры") + + print("Формат имен работает!") + + +async def test_message_format(): + """Тест формата сообщений""" + print("\nТестирование формата сообщений...") + + uri = "ws://localhost:8000/chat/format_test" + + async with websockets.connect(uri) as ws: + # Получаем приветствие + welcome = await ws.recv() + welcome_data = json.loads(welcome) + username = welcome_data["message"].split("Ваше имя: ")[1] + + # Отправляем тестовое сообщение + test_message = "Тестовое сообщение" + await ws.send(test_message) + + # Получаем ответ + response = await ws.recv() + response_data = json.loads(response) + + # Проверяем формат: "username :: message" + expected_format = f"{username} :: {test_message}" + if response_data["message"] == expected_format: + print("Формат сообщения корректен!") + print(f"Ожидалось: {expected_format}") + print(f"Получено: {response_data['message']}") + else: + print("Формат сообщения некорректен!") + print(f"Ожидалось: {expected_format}") + print(f"Получено: {response_data['message']}") + + print("Формат сообщений работает!") + + +async def test_connection_disconnection(): + """Тест подключения и отключения""" + print("\nТестирование подключения и отключения...") + + uri = "ws://localhost:8000/chat/disconnect_test" + + # Тест 1: Подключение + print("Тестирование подключения...") + async with websockets.connect(uri) as ws: + welcome = await ws.recv() + print("Подключение успешно!") + + # Отправляем сообщение + await ws.send("Тест подключения") + response = await ws.recv() + print("Сообщение отправлено и получено!") + + print("Отключение успешно!") + + # Тест 2: Повторное подключение + print("Тестирование повторного подключения...") + async with websockets.connect(uri) as ws: + welcome = await ws.recv() + print("Повторное подключение успешно!") + + print("Тест подключения/отключения завершен!") + + +async def main(): + """Главная функция тестирования""" + print("Тестирование WebSocket чата") + print("=" * 40) + + # Тест 1: Изоляция комнат + await test_room_isolation() + + # Тест 2: Несколько пользователей в одной комнате + await test_multiple_users_same_room() + + # Тест 3: Формат имен пользователей + await test_username_format() + + # Тест 4: Формат сообщений + await test_message_format() + + # Тест 5: Подключение/отключение + await test_connection_disconnection() + + print("\nВсе тесты завершены!") + print("WebSocket чат полностью функционален!") + + +if __name__ == "__main__": + asyncio.run(main())