Merge app brach to frontend #1
@@ -0,0 +1,6 @@
|
|||||||
|
DISABLE_DOCS=true
|
||||||
|
|
||||||
|
MONGO_HOST=127.0.0.1
|
||||||
|
MONGO_PORT=27017
|
||||||
|
MONGO_USER=user
|
||||||
|
MONGO_PASS=pass
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
name: app-actions
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.py'
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- app
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mypy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
continue-on-error: true
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
architecture: x64
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install mypy
|
||||||
|
pip install -r app/requirements.txt
|
||||||
|
|
||||||
|
- name: Run mypy
|
||||||
|
run: mypy --ignore-missing-imports ./app
|
||||||
|
|
||||||
|
pytest:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
MONGO_HOST: ${{ secrets.MONGO_HOST }}
|
||||||
|
MONGO_PORT: ${{ secrets.MONGO_PORT }}
|
||||||
|
MONGO_USER: ${{ secrets.MONGO_USER }}
|
||||||
|
MONGO_PASS: ${{ secrets.MONGO_PASS }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.9
|
||||||
|
architecture: x64
|
||||||
|
|
||||||
|
- name: Setup MongoDB
|
||||||
|
run: docker run --name mongodb -d -p ${{ secrets.MONGO_PORT }}:27017 -e MONGO_INITDB_ROOT_USERNAME=${{ secrets.MONGO_USER }} -e MONGO_INITDB_ROOT_PASSWORD=${{ secrets.MONGO_PASS }} mongodb/mongodb-community-server
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install pytest==8.3.4 pytest-asyncio==0.25.3 httpx==0.28.1
|
||||||
|
pip install -r app/requirements.txt
|
||||||
|
- name: Setup moderated base cards
|
||||||
|
|
||||||
|
working-directory: ./app/tools
|
||||||
|
run: python3 _add_base_cards.py -a 2 -f data/base_cards.json
|
||||||
|
- name: Run pytest
|
||||||
|
run: pytest -vs
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
name: mypy-test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- '**.py'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
mypy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: 3.9
|
|
||||||
architecture: x64
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Install mypy
|
|
||||||
run: pip install mypy
|
|
||||||
- name: Run mypy
|
|
||||||
run: mypy --ignore-missing-imports ./app
|
|
||||||
+2
-25
@@ -2,6 +2,7 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
.idea/
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
@@ -170,28 +171,4 @@ cython_debug/
|
|||||||
|
|
||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
app/.env
|
||||||
|
|
||||||
|
|
||||||
# React
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|||||||
@@ -9,15 +9,23 @@ Telegram mini-app where you have to choose one of two things.
|
|||||||
git clone https://github.com/IgorVolochay/thisORthat
|
git clone https://github.com/IgorVolochay/thisORthat
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Manual setup:
|
||||||
|
|
||||||
2. The project is written in Python3.9. Make sure you have it on your system. Go to the project folder, create a virtual environment and download pip requirements:
|
2. The project is written in Python3.9. Make sure you have it on your system. Go to the project folder, create a virtual environment and download pip requirements:
|
||||||
```bash
|
```bash
|
||||||
cd ./thisORthat
|
cd ./thisORthat
|
||||||
python3.9 -m venv venv
|
python3.9 -m venv venv
|
||||||
source ./venv/bin/activate
|
source ./venv/bin/activate
|
||||||
pip3 install -r requirements.txt
|
pip3 install -r ./app/requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Installing MongoDB database. You can use the [official manual](https://www.mongodb.com/docs/manual/installation/) to install MongoDB manually, or use a [Docker image](https://hub.docker.com/r/mongodb/mongodb-community-server) to run the container:
|
3. Installing MongoDB database. You can use the [official manual](https://www.mongodb.com/docs/manual/installation/) to install MongoDB manually, or use a [Docker image](https://hub.docker.com/r/mongodb/mongodb-community-server) to run the container:
|
||||||
```bash
|
```bash
|
||||||
docker run --name mongodb -d -p 27017:27017 mongodb/mongodb-community-server
|
docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=user -e MONGO_INITDB_ROOT_PASSWORD=pass mongodb/mongodb-community-server
|
||||||
|
```
|
||||||
|
### Docker Compose setup:
|
||||||
|
|
||||||
|
2. Use docker-compose to automatically build the entire project. For correct build, it is better to use docker-compose version 1.29.2:
|
||||||
|
```bash
|
||||||
|
docker-compose up --build
|
||||||
```
|
```
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3.9.21-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["python3", "main.py"]
|
||||||
+188
-1
@@ -1,4 +1,191 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi import FastAPI, Depends, Response, status
|
||||||
|
|
||||||
|
from schemas.api_schemas import *
|
||||||
|
from schemas.base_schemas import *
|
||||||
from mongo_worker import MongoWorker
|
from mongo_worker import MongoWorker
|
||||||
|
from tools.base_moderation import moderate_text
|
||||||
|
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
disable_docs = os.getenv("DISABLE_DOCS", "true").lower() == "true"
|
||||||
|
|
||||||
|
app: FastAPI = FastAPI(title="This OR That",
|
||||||
|
summary="OpenAPI schema for \"This OR That\" project!",
|
||||||
|
version="0.1",
|
||||||
|
contact={"GitHub": "https://github.com/IgorVolochay/thisORthat"},
|
||||||
|
docs_url=None if disable_docs else "/docs",
|
||||||
|
redoc_url=None if disable_docs else "/redoc",
|
||||||
|
openapi_url=None if disable_docs else "/openapi.json")
|
||||||
|
mongo_worker = MongoWorker()
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/check_user", status_code=200)
|
||||||
|
async def check_user(user_id: NonNegativeInt,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
result = mongo.check_user(user_id)
|
||||||
|
return BaseResponse(result=result)
|
||||||
|
|
||||||
|
@app.get("/get_user", status_code=200)
|
||||||
|
async def get_user(user_id: NonNegativeInt,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
if mongo.check_user(user_id):
|
||||||
|
result = mongo.get_user(user_id)
|
||||||
|
return BaseResponse(result=result)
|
||||||
|
else:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return BaseResponse(result="User doesn't exist", error=True)
|
||||||
|
|
||||||
|
@app.post("/add_user", status_code=201)
|
||||||
|
async def add_user(new_user: AddUserBody,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
if not mongo.check_user(new_user.user_id):
|
||||||
|
result = mongo.add_user(new_user.user_id,
|
||||||
|
new_user.username,
|
||||||
|
new_user.first_name,
|
||||||
|
new_user.last_name,
|
||||||
|
new_user.photo_url)
|
||||||
|
return BaseResponse(result=result)
|
||||||
|
else:
|
||||||
|
response.status_code = status.HTTP_409_CONFLICT
|
||||||
|
return BaseResponse(result="User already exist", error=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/get_card", status_code=200)
|
||||||
|
async def get_card(card_id: NonNegativeInt,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
card = mongo.get_card(card_id)
|
||||||
|
if card:
|
||||||
|
return BaseResponse(result=card)
|
||||||
|
else:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return BaseResponse(result="There is no card with this card_id", error=True)
|
||||||
|
|
||||||
|
@app.get("/get_random_cards", status_code=200)
|
||||||
|
async def get_random_cards(user_id: NonNegativeInt,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
cards_visited = mongo.get_visited_cards(user_id)
|
||||||
|
|
||||||
|
if cards_visited.error:
|
||||||
|
response.status_code = status.HTTP_401_UNAUTHORIZED
|
||||||
|
return cards_visited
|
||||||
|
elif not cards_visited.result.cards_visited:
|
||||||
|
random_cards = mongo.get_random_cards(10, True)
|
||||||
|
if random_cards:
|
||||||
|
return BaseResponse(result=random_cards)
|
||||||
|
else:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return BaseResponse(result="No active cards", error=True)
|
||||||
|
|
||||||
|
result: list[Card] = list()
|
||||||
|
trys = 3
|
||||||
|
while len(result) < 10 and trys != 0:
|
||||||
|
random_cards = mongo.get_random_cards(10, True)
|
||||||
|
if not random_cards:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return BaseResponse(result="No active cards", error=True)
|
||||||
|
filtered_cards, filtered_cards_id = mongo.filter_cards(random_cards, cards_visited.result.cards_visited)
|
||||||
|
trys -= 1
|
||||||
|
if not filtered_cards:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
result.extend(filtered_cards)
|
||||||
|
cards_visited.result.cards_visited.update(filtered_cards_id)
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return BaseResponse(result="No active cards fo this user", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result=result)
|
||||||
|
|
||||||
|
@app.post("/add_card", status_code=201)
|
||||||
|
async def add_card(new_card: AddCardBody,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
if moderate_text(new_card.choice_A) and moderate_text(new_card.choice_B):
|
||||||
|
card = mongo.add_card_by_api(new_card.choice_A,
|
||||||
|
new_card.choice_B,
|
||||||
|
new_card.author_id)
|
||||||
|
return BaseResponse(result=card)
|
||||||
|
else:
|
||||||
|
response.status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
return BaseResponse(result="Card has not passed base moderation", error=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.patch("/select_choice", status_code=200)
|
||||||
|
async def select_choice(choice_data: SelectChoice,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
check_visited = mongo.get_visited_cards(choice_data.user_id)
|
||||||
|
if check_visited.error:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return check_visited
|
||||||
|
elif not check_visited.error and choice_data.card_id in check_visited.result.cards_visited:
|
||||||
|
response.status_code = status.HTTP_403_FORBIDDEN
|
||||||
|
return BaseResponse(result="Card already visited!", error=True)
|
||||||
|
else:
|
||||||
|
select_choice_result = mongo.select_choice(choice_data.card_id, choice_data.choice)
|
||||||
|
if select_choice_result.error:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return select_choice_result
|
||||||
|
else:
|
||||||
|
update_visited_result = mongo.update_visited_cards(choice_data.user_id, choice_data.card_id)
|
||||||
|
return BaseResponse(result="Select choice complite!")
|
||||||
|
|
||||||
|
@app.patch("/like_card", status_code=200)
|
||||||
|
async def like_card(like_data: ReactionCard,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
result = mongo.like_card(like_data.card_id, like_data.user_id)
|
||||||
|
if not result.error and result.result:
|
||||||
|
return BaseResponse(result="Added like to card")
|
||||||
|
else:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return result
|
||||||
|
|
||||||
|
@app.patch("/dislike_card", status_code=200)
|
||||||
|
async def dislike_card(dislike_data: ReactionCard,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
result = mongo.dislike_card(dislike_data.card_id, dislike_data.user_id)
|
||||||
|
if not result.error and result.result:
|
||||||
|
return BaseResponse(result="Added dislike to card")
|
||||||
|
else:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return result
|
||||||
|
|
||||||
|
@app.post("/comment", status_code=201)
|
||||||
|
async def comment(comment_info: AddCommentBody,
|
||||||
|
response: Response,
|
||||||
|
mongo: MongoWorker = Depends(lambda: mongo_worker)) -> BaseResponse:
|
||||||
|
if not moderate_text(comment_info.comment_text):
|
||||||
|
response.status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
return BaseResponse(result="Comment has not passed base moderation", error=True)
|
||||||
|
|
||||||
|
result = mongo.add_comment(comment_info.author_id, comment_info.card_id, comment_info.comment_text)
|
||||||
|
if result.error and result.result in ["User doesn't exist", "Card doesn't exist"]:
|
||||||
|
response.status_code = status.HTTP_404_NOT_FOUND
|
||||||
|
return result
|
||||||
|
elif result.error:
|
||||||
|
response.status_code = status.HTTP_400_BAD_REQUEST
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
config = uvicorn.Config("main:app", port=5000, log_level="debug")
|
||||||
|
server = uvicorn.Server(config)
|
||||||
|
await server.serve()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("Hello World!")
|
asyncio.run(main())
|
||||||
+197
-28
@@ -1,45 +1,214 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
import pymongo
|
import pymongo
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from bson import ObjectId
|
from schemas.base_schemas import *
|
||||||
|
from schemas.api_schemas import *
|
||||||
|
|
||||||
|
|
||||||
class MongoWorker:
|
class MongoWorker:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.client = pymongo.MongoClient(host="127.0.0.1", port=27017)
|
load_dotenv()
|
||||||
|
self.client = pymongo.MongoClient(host = os.getenv('MONGO_HOST'),
|
||||||
|
port = int(os.getenv('MONGO_PORT')),
|
||||||
|
username = os.getenv('MONGO_USER'),
|
||||||
|
password = os.getenv('MONGO_PASS'))
|
||||||
self.db = self.client["data"]
|
self.db = self.client["data"]
|
||||||
self.users_data = self.db["users_data"]
|
self.users_data = self.db["users"]
|
||||||
self.game_data = self.db["game_data"]
|
self.visited_data = self.db["visited"]
|
||||||
|
self.counters = self.db["counters"]
|
||||||
|
self.game_data = self.db["cards"]
|
||||||
|
self.comments_data = self.db["comments"]
|
||||||
|
|
||||||
def get_mongodb_info(self) -> None:
|
|
||||||
print(self.client.list_database_names())
|
|
||||||
print(self.db.list_collection_names())
|
|
||||||
|
|
||||||
def find_user(self, user_id: int) -> dict:
|
def check_user(self, user_id: int) -> bool:
|
||||||
result = self.users_data.find_one({"user_id": user_id})
|
if self.users_data.find_one({"user_id": user_id}):
|
||||||
return result
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
def add_user(self, user_id: int, username: str, first_name: str, last_name: str, photo_path: str) -> ObjectId:
|
def add_user(self, user_id: int, username: str, first_name: str, last_name: str, photo_url: str) -> User:
|
||||||
|
new_user = User(user_id=user_id,
|
||||||
|
username=username,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
photo_url=photo_url,
|
||||||
|
registration_date=datetime.now().isoformat())
|
||||||
try:
|
try:
|
||||||
res = self.users_data.insert_one({"user_id": user_id,
|
self.users_data.insert_one(new_user.model_dump())
|
||||||
"username": username,
|
return new_user
|
||||||
"first_name": first_name,
|
except Exception as exception:
|
||||||
"last_name": last_name,
|
return new_user
|
||||||
"photo_path": photo_path,
|
|
||||||
"activity": 0,
|
def get_user(self, user_id: int) -> User:
|
||||||
"liked_post_ids": [],
|
return User.model_validate(self.users_data.find_one({"user_id": user_id}))
|
||||||
"disliked_post_ids": [],
|
|
||||||
"comments_ids": [],
|
|
||||||
"registration_date": datetime.now().isoformat()
|
def get_and_update_counter(self, counter_name: str) -> int:
|
||||||
})
|
counter = self.counters.find_one_and_update(
|
||||||
return res.inserted_id
|
{"counter_name": counter_name},
|
||||||
|
{"$inc": {"counter": 1}},
|
||||||
|
upsert=True,
|
||||||
|
return_document=True)
|
||||||
|
return counter["counter"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_visited_cards(self, user_id: int) -> BaseResponse:
|
||||||
|
document = self.visited_data.find_one({"user_id": user_id})
|
||||||
|
if not document:
|
||||||
|
check_user = self.check_user(user_id)
|
||||||
|
if check_user:
|
||||||
|
return BaseResponse(result=Visited(user_id=user_id,
|
||||||
|
cards_visited=set()))
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="User doesn't exist", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result=Visited.model_validate(document))
|
||||||
|
|
||||||
|
def filter_cards(self, random_cards: list[Card], cards_visited: set) -> tuple[list[Card], list[int]]:
|
||||||
|
filtered_cards = [card for card in random_cards if card.card_id not in cards_visited]
|
||||||
|
filtered_cards_id = [filtered_card.card_id for filtered_card in filtered_cards]
|
||||||
|
|
||||||
|
return filtered_cards, filtered_cards_id
|
||||||
|
|
||||||
|
def update_visited_cards(self, user_id: int, visited_card_id: int) -> Visited:
|
||||||
|
update_visited = self.visited_data.find_one_and_update({"user_id": user_id},
|
||||||
|
{"$addToSet": {"cards_visited": visited_card_id}},
|
||||||
|
upsert=True,
|
||||||
|
return_document=True)
|
||||||
|
return Visited.model_validate(update_visited)
|
||||||
|
|
||||||
|
|
||||||
|
def add_card_by_api(self, choice_A: str, choice_B: str, author_id: int) -> Card:
|
||||||
|
new_card = Card(card_id=self.get_and_update_counter(counter_name="card"),
|
||||||
|
choice_A=choice_A,
|
||||||
|
choice_B=choice_B,
|
||||||
|
author_id=author_id,
|
||||||
|
creation_date=datetime.now().isoformat())
|
||||||
|
try:
|
||||||
|
self.game_data.insert_one(new_card.model_dump())
|
||||||
|
return new_card
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
print(exception)
|
print(exception)
|
||||||
|
return new_card
|
||||||
|
|
||||||
|
def add_card_by_base_model(self, new_card: Card) -> Optional[Card]:
|
||||||
|
new_card.card_id = self.get_and_update_counter(counter_name="card")
|
||||||
|
try:
|
||||||
|
self.game_data.insert_one(new_card.model_dump())
|
||||||
|
return new_card
|
||||||
|
except Exception as exception:
|
||||||
|
print(exception)
|
||||||
|
return new_card
|
||||||
|
|
||||||
|
def get_card(self, card_id: int) -> Optional[Card]:
|
||||||
|
document = self.game_data.find_one({"card_id": card_id})
|
||||||
|
if document:
|
||||||
|
return Card.model_validate(document)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_random_cards(self, amount: int, active_status: bool) -> Optional[list[Card]]:
|
||||||
|
pipeline = [{"$match": {"active_status": active_status}},
|
||||||
|
{"$sample": {"size": amount}}]
|
||||||
|
raw_items = list(self.game_data.aggregate(pipeline))
|
||||||
|
|
||||||
|
if raw_items:
|
||||||
|
validated_items = [Card.model_validate(item) for item in raw_items]
|
||||||
|
return validated_items
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def select_choice(self, card_id: int, choice: str) -> BaseResponse:
|
||||||
mongo = MongoWorker()
|
if choice == "A":
|
||||||
mongo.get_mongodb_info()
|
count_choice = "count_choice_A"
|
||||||
print(mongo.add_user(123, "VolochayIgor", "Igor", "Volochay", "path/to/img"))
|
elif choice == "B":
|
||||||
print(mongo.find_user(123))
|
count_choice = "count_choice_B"
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="Wrong choice", error=True)
|
||||||
|
|
||||||
|
result = self.game_data.find_one_and_update({"card_id": card_id},
|
||||||
|
{"$inc": {"count_total": 1, count_choice: 1}})
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return BaseResponse(result="Card doesn't exist", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result=result, error=False)
|
||||||
|
|
||||||
|
def check_user_reactions(self, user_id: int, card_id: int) -> BaseResponse:
|
||||||
|
user_info: User = self.get_user(user_id)
|
||||||
|
liked_card_ids: list = user_info.liked_card_ids
|
||||||
|
disliked_card_ids: list = user_info.disliked_card_ids
|
||||||
|
|
||||||
|
if card_id in liked_card_ids:
|
||||||
|
return BaseResponse(result="Card already liked", error=True)
|
||||||
|
elif card_id in disliked_card_ids:
|
||||||
|
return BaseResponse(result="Card already disliked", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="No reactions", error=False)
|
||||||
|
|
||||||
|
def like_card(self, card_id: int, user_id: int) -> BaseResponse:
|
||||||
|
if self.check_user(user_id):
|
||||||
|
user_reaction = self.check_user_reactions(user_id, card_id)
|
||||||
|
if user_reaction.error:
|
||||||
|
return user_reaction
|
||||||
|
update_card_info = self.game_data.find_one_and_update({"card_id": card_id},
|
||||||
|
{"$inc": {"count_likes": 1}})
|
||||||
|
if not update_card_info:
|
||||||
|
return BaseResponse(result="Card doesn't exist", error=True)
|
||||||
|
|
||||||
|
add_card_to_user = self.users_data.update_one({'user_id': user_id},
|
||||||
|
{'$push': {'liked_card_ids': card_id}})
|
||||||
|
if not add_card_to_user:
|
||||||
|
return BaseResponse(result="User doesn't exist", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result=True, error=False)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="User doesn't exist", error=True)
|
||||||
|
|
||||||
|
def dislike_card(self, card_id: int, user_id: int) -> BaseResponse:
|
||||||
|
if self.check_user(user_id):
|
||||||
|
user_reaction = self.check_user_reactions(user_id, card_id)
|
||||||
|
if user_reaction.error:
|
||||||
|
return user_reaction
|
||||||
|
update_card_info = self.game_data.find_one_and_update({"card_id": card_id},
|
||||||
|
{"$inc": {"count_dislikes": 1}})
|
||||||
|
if not update_card_info:
|
||||||
|
return BaseResponse(result="Card doesn't exist", error=True)
|
||||||
|
|
||||||
|
add_card_to_user = self.users_data.update_one({'user_id': user_id},
|
||||||
|
{'$push': {'disliked_card_ids': card_id}})
|
||||||
|
if not add_card_to_user:
|
||||||
|
return BaseResponse(result="User doesn't exist", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result=True, error=False)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="User doesn't exist", error=True)
|
||||||
|
|
||||||
|
def add_comment(self, user_id: int, card_id: int, comment_text: str) -> BaseResponse:
|
||||||
|
if self.check_user(user_id):
|
||||||
|
if self.get_card(card_id):
|
||||||
|
new_comment = Comment(comment_id=self.get_and_update_counter(counter_name="comment"),
|
||||||
|
author_id=user_id,
|
||||||
|
card_id=card_id,
|
||||||
|
commet_text=comment_text,
|
||||||
|
creation_date=datetime.now().isoformat())
|
||||||
|
result = self.comments_data.insert_one(new_comment.model_dump())
|
||||||
|
if result:
|
||||||
|
update_user_comments = self.users_data.find_one_and_update({"user_id": user_id},
|
||||||
|
{"$addToSet": {"comments_ids": new_comment.comment_id}})
|
||||||
|
if update_user_comments:
|
||||||
|
return BaseResponse(result=new_comment)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="Difficulty adding comment_id to user", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="Add comment error", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="Card doesn't exist", error=True)
|
||||||
|
else:
|
||||||
|
return BaseResponse(result="User doesn't exist", error=True)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
fastapi==0.115.7
|
||||||
|
pymongo==4.10.1
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
uvicorn==0.34.0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
from pydantic import BaseModel, NonNegativeInt
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResponse(BaseModel):
|
||||||
|
result: typing.Any
|
||||||
|
error: bool = False
|
||||||
|
|
||||||
|
class AddUserBody(BaseModel):
|
||||||
|
user_id: NonNegativeInt
|
||||||
|
username: str
|
||||||
|
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
photo_url: str
|
||||||
|
|
||||||
|
class AddCardBody(BaseModel):
|
||||||
|
choice_A: str
|
||||||
|
choice_B: str
|
||||||
|
|
||||||
|
author_id: NonNegativeInt
|
||||||
|
|
||||||
|
class SelectChoice(BaseModel):
|
||||||
|
user_id: NonNegativeInt
|
||||||
|
card_id: NonNegativeInt
|
||||||
|
|
||||||
|
choice: typing.Literal["A", "B"]
|
||||||
|
|
||||||
|
class ReactionCard(BaseModel):
|
||||||
|
user_id: NonNegativeInt
|
||||||
|
card_id: NonNegativeInt
|
||||||
|
|
||||||
|
class AddCommentBody(BaseModel):
|
||||||
|
author_id: NonNegativeInt
|
||||||
|
card_id: NonNegativeInt
|
||||||
|
|
||||||
|
comment_text: str
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
user_id: int
|
||||||
|
username: str
|
||||||
|
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
photo_url: str
|
||||||
|
|
||||||
|
activity: int = 0
|
||||||
|
liked_card_ids: list[int] = list()
|
||||||
|
disliked_card_ids: list[int] = list()
|
||||||
|
comments_ids: list[int] = list()
|
||||||
|
|
||||||
|
registration_date: str
|
||||||
|
|
||||||
|
class Visited(BaseModel):
|
||||||
|
user_id: int
|
||||||
|
cards_visited: set[int]
|
||||||
|
|
||||||
|
class Card(BaseModel):
|
||||||
|
card_id: int
|
||||||
|
|
||||||
|
choice_A: str
|
||||||
|
choice_B: str
|
||||||
|
|
||||||
|
count_choice_A: int = 0
|
||||||
|
count_choice_B: int = 0
|
||||||
|
count_total: int = 0
|
||||||
|
|
||||||
|
count_likes: int = 0
|
||||||
|
count_dislikes: int = 0
|
||||||
|
comments: list[int] = list()
|
||||||
|
|
||||||
|
author_id: int
|
||||||
|
creation_date: str
|
||||||
|
moderation_date: str = "Not moderated"
|
||||||
|
active_status: bool = False
|
||||||
|
|
||||||
|
class Comment(BaseModel):
|
||||||
|
comment_id: int
|
||||||
|
|
||||||
|
author_id: int
|
||||||
|
card_id: int
|
||||||
|
commet_text: str
|
||||||
|
|
||||||
|
creation_date: str
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient, ASGITransport
|
||||||
|
|
||||||
|
from main import app
|
||||||
|
from schemas.base_schemas import *
|
||||||
|
from schemas.api_schemas import *
|
||||||
|
|
||||||
|
|
||||||
|
EXIST_AUTHOR = random.randint(100000000, 1000000000)
|
||||||
|
NON_EXIST_CARD_ID = 1000
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- /add_card ----------
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_card_valid():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
payload = {
|
||||||
|
"choice_A": "Option A",
|
||||||
|
"choice_B": "Option B",
|
||||||
|
"author_id": EXIST_AUTHOR
|
||||||
|
}
|
||||||
|
response = await client.post("/add_card", json=payload)
|
||||||
|
print(f"\nINPUT: endpoint=/add_card | payload={payload}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code in (200, 201)
|
||||||
|
base_resp = BaseResponse.model_validate(response.json())
|
||||||
|
assert base_resp.error is False
|
||||||
|
card = Card.model_validate(base_resp.result)
|
||||||
|
assert card.choice_A == payload["choice_A"]
|
||||||
|
assert card.choice_B == payload["choice_B"]
|
||||||
|
assert card.author_id == payload["author_id"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_card_missing_field():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
payload = {
|
||||||
|
#choice_A
|
||||||
|
"choice_B": "Option B",
|
||||||
|
"author_id": EXIST_AUTHOR
|
||||||
|
}
|
||||||
|
response = await client.post("/add_card", json=payload)
|
||||||
|
print(f"\nINPUT: endpoint=/add_card | payload (missing field)={payload}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_card_wrong_type():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
payload = {
|
||||||
|
"choice_A": 123,
|
||||||
|
"choice_B": "Option B",
|
||||||
|
"author_id": EXIST_AUTHOR
|
||||||
|
}
|
||||||
|
response = await client.post("/add_card", json=payload)
|
||||||
|
print(f"\nINPUT: endpoint=/add_card | payload (wrong type)={payload}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_card_empty_strings():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
payload = {
|
||||||
|
"choice_A": "",
|
||||||
|
"choice_B": "",
|
||||||
|
"author_id": EXIST_AUTHOR
|
||||||
|
}
|
||||||
|
response = await client.post("/add_card", json=payload)
|
||||||
|
print(f"\nINPUT: endpoint=/add_card | payload (empty strings)={payload}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_card_long_strings():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
long_str = "A" * 5000 # long string
|
||||||
|
payload = {
|
||||||
|
"choice_A": long_str,
|
||||||
|
"choice_B": long_str,
|
||||||
|
"author_id": EXIST_AUTHOR
|
||||||
|
}
|
||||||
|
response = await client.post("/add_card", json=payload)
|
||||||
|
print(f"\nINPUT: endpoint=/add_card | payload with long strings (length={len(long_str)})\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_card_negative_author_id():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
payload = {
|
||||||
|
"choice_A": "Option A",
|
||||||
|
"choice_B": "Option B",
|
||||||
|
"author_id": -10 # Negative id
|
||||||
|
}
|
||||||
|
response = await client.post("/add_card", json=payload)
|
||||||
|
print(f"\nINPUT: endpoint=/add_card | payload (negative author_id)={payload}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_card_malformed_json():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
malformed_json = '{"choice_A": "Option A", "choice_B": "Option B", "author_id": 123' # broken json
|
||||||
|
response = await client.post(
|
||||||
|
"/add_card",
|
||||||
|
data=malformed_json,
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
)
|
||||||
|
print(f"\nINPUT: endpoint=/add_card | payload (malformed JSON)={malformed_json}\nOUTPUT: status={response.status_code} | json={response.json() if response.content else 'No JSON'}")
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_card_creation():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
tasks = []
|
||||||
|
num_cards = 8
|
||||||
|
for i in range(num_cards):
|
||||||
|
payload = {
|
||||||
|
"choice_A": f"Async Option A {i}",
|
||||||
|
"choice_B": f"Async Option B {i}",
|
||||||
|
"author_id": EXIST_AUTHOR
|
||||||
|
}
|
||||||
|
tasks.append(client.post("/add_card", json=payload))
|
||||||
|
responses = await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
card_ids = []
|
||||||
|
for idx, response in enumerate(responses):
|
||||||
|
print(f"\nAsync creation {idx}: status={response.status_code}, response={response.json()}")
|
||||||
|
assert response.status_code in (200, 201)
|
||||||
|
base_resp = BaseResponse.model_validate(response.json())
|
||||||
|
assert base_resp.error is False
|
||||||
|
card = Card.model_validate(base_resp.result)
|
||||||
|
card_ids.append(card.card_id)
|
||||||
|
assert card.choice_A == f"Async Option A {idx}"
|
||||||
|
assert card.choice_B == f"Async Option B {idx}"
|
||||||
|
assert card.author_id == EXIST_AUTHOR
|
||||||
|
|
||||||
|
assert len(set(card_ids)) == num_cards
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- /get_card ----------
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_card_valid():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
payload = {
|
||||||
|
"choice_A": "GetTest A",
|
||||||
|
"choice_B": "GetTest B",
|
||||||
|
"author_id": EXIST_AUTHOR
|
||||||
|
}
|
||||||
|
create_resp = await client.post("/add_card", json=payload)
|
||||||
|
base_create = BaseResponse.model_validate(create_resp.json())
|
||||||
|
card = Card.model_validate(base_create.result)
|
||||||
|
card_id = card.card_id
|
||||||
|
|
||||||
|
response = await client.get("/get_card", params={"card_id": card_id})
|
||||||
|
print(f"\nINPUT: endpoint=/get_card | params={{'card_id': {card_id}}}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
base_resp = BaseResponse.model_validate(response.json())
|
||||||
|
assert base_resp.error is False
|
||||||
|
card_from_get = Card.model_validate(base_resp.result)
|
||||||
|
assert card_from_get.card_id == card_id
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_card_nonexistent():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
response = await client.get("/get_card", params={"card_id": NON_EXIST_CARD_ID})
|
||||||
|
print(f"\nINPUT: endpoint=/get_card | params={{'card_id': {NON_EXIST_CARD_ID}}}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_card_missing_param():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
response = await client.get("/get_card")
|
||||||
|
print(f"\nINPUT: endpoint=/get_card (missing card_id param)\nOUTPUT: status={response.status_code} | json={response.json() if response.content else 'No content'}")
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_card_wrong_type():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
response = await client.get("/get_card", params={"card_id": "abc"})
|
||||||
|
print(f"\nINPUT: endpoint=/get_card | params={{'card_id': 'abc'}}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_card_negative():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
response = await client.get("/get_card", params={"card_id": -10})
|
||||||
|
print(f"\nINPUT: endpoint=/get_card | params={{'card_id': -10}}\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
assert response.status_code == 422
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from main import app
|
||||||
|
from schemas.api_schemas import *
|
||||||
|
from schemas.base_schemas import *
|
||||||
|
|
||||||
|
from httpx import AsyncClient, ASGITransport
|
||||||
|
|
||||||
|
|
||||||
|
EXIST_USER = random.randint(100000000, 1000000000)
|
||||||
|
NON_EXIST_USER = random.randint(100000000, 1000000000)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TEST ADD USERS UTILS #
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_user_non_full_data():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/add_user"
|
||||||
|
data = {
|
||||||
|
"user_id": EXIST_USER,
|
||||||
|
"username": "TestUsername",
|
||||||
|
}
|
||||||
|
raw_response = await client.post(url=end_point,json=data)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_user_negative_int_id():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/add_user"
|
||||||
|
data = {
|
||||||
|
"user_id": -1,
|
||||||
|
"username": "TestUsername",
|
||||||
|
"first_name": "FName",
|
||||||
|
"last_name": "LName",
|
||||||
|
"photo_url": "http://test.test/photo.jpg"
|
||||||
|
}
|
||||||
|
raw_response = await client.post(url=end_point,json=data)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 422
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_new_user():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/add_user"
|
||||||
|
data = {
|
||||||
|
"user_id": EXIST_USER,
|
||||||
|
"username": "TestUsername",
|
||||||
|
"first_name": "FName",
|
||||||
|
"last_name": "LName",
|
||||||
|
"photo_url": "http://test.test/photo.jpg"
|
||||||
|
}
|
||||||
|
raw_response = await client.post(url=end_point,json=data)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 201
|
||||||
|
response = BaseResponse.model_validate(raw_response.json())
|
||||||
|
assert response.error == False
|
||||||
|
assert User.model_validate(response.result)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_already_exist_user():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/add_user"
|
||||||
|
data = {
|
||||||
|
"user_id": EXIST_USER,
|
||||||
|
"username": "TestUsername",
|
||||||
|
"first_name": "FName",
|
||||||
|
"last_name": "LName",
|
||||||
|
"photo_url": "http://test.test/photo.jpg"
|
||||||
|
}
|
||||||
|
raw_response = await client.post(url=end_point,json=data)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 409
|
||||||
|
response = BaseResponse.model_validate(raw_response.json())
|
||||||
|
assert response.error == True
|
||||||
|
assert response.result == "User already exist"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TEST CHECK USERS UTILS #
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_non_exist_user():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/check_user"
|
||||||
|
params = {"user_id": NON_EXIST_USER}
|
||||||
|
raw_response = await client.get(url=end_point, params=params)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={params}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 200
|
||||||
|
response = BaseResponse.model_validate(raw_response.json())
|
||||||
|
assert response.error == False
|
||||||
|
assert response.result == False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_check_exist_user():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/check_user"
|
||||||
|
params = {"user_id": EXIST_USER}
|
||||||
|
raw_response = await client.get(url=end_point, params=params)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={params}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 200
|
||||||
|
response = BaseResponse.model_validate(raw_response.json())
|
||||||
|
assert response.error == False
|
||||||
|
assert response.result == True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# TEST GET USERS UTILS #
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_non_exist_user():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/get_user"
|
||||||
|
params = {"user_id": NON_EXIST_USER}
|
||||||
|
raw_response = await client.get(url=end_point, params=params)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={params}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 404
|
||||||
|
response = BaseResponse.model_validate(raw_response.json())
|
||||||
|
assert response.error == True
|
||||||
|
assert response.result == "User doesn't exist"
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_exist_user():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/get_user"
|
||||||
|
params = {"user_id": EXIST_USER}
|
||||||
|
raw_response = await client.get(url=end_point, params=params)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={params}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 200
|
||||||
|
response = BaseResponse.model_validate(raw_response.json())
|
||||||
|
assert response.error == False
|
||||||
|
assert User.model_validate(response.result)
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import random
|
||||||
|
import asyncio
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient, ASGITransport
|
||||||
|
|
||||||
|
from main import app
|
||||||
|
from schemas.base_schemas import *
|
||||||
|
from schemas.api_schemas import *
|
||||||
|
|
||||||
|
EXIST_USER = random.randint(100000000, 1000000000)
|
||||||
|
EXIST_AUTHOR = random.randint(100000000, 1000000000)
|
||||||
|
NO_ACTIVE_CARDS_STATUS = False
|
||||||
|
ACTIVE_CARDS_LESS_THAN_TEN = False
|
||||||
|
|
||||||
|
# ------------- /add_user ---------------
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_add_new_user():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app),
|
||||||
|
base_url='http://test') as client:
|
||||||
|
end_point = "/add_user"
|
||||||
|
data = {
|
||||||
|
"user_id": EXIST_USER,
|
||||||
|
"username": "TestUsername",
|
||||||
|
"first_name": "FName",
|
||||||
|
"last_name": "LName",
|
||||||
|
"photo_url": "http://test.test/photo.jpg"
|
||||||
|
}
|
||||||
|
raw_response = await client.post(url=end_point,json=data)
|
||||||
|
print(f"\nINPUT: endpiont={end_point} | params={data}\nOUTPUT: status={raw_response.status_code} | json={raw_response.json()}")
|
||||||
|
|
||||||
|
assert raw_response.status_code == 201
|
||||||
|
response = BaseResponse.model_validate(raw_response.json())
|
||||||
|
assert response.error == False
|
||||||
|
assert User.model_validate(response.result)
|
||||||
|
|
||||||
|
# ---------- /get_random_cards ----------
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_random_cards_valid():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
params = {"user_id": EXIST_USER}
|
||||||
|
response = await client.get("/get_random_cards", params=params)
|
||||||
|
print(f"\nINPUT: endpoint=/get_random_cards\nOUTPUT: status={response.status_code} | json={response.json()}")
|
||||||
|
if response.status_code == 404 and BaseResponse.model_validate(response.json()).result == "No active cards":
|
||||||
|
global NO_ACTIVE_CARDS_STATUS
|
||||||
|
NO_ACTIVE_CARDS_STATUS = True
|
||||||
|
pytest.skip(reason="No active cards in MongoDB")
|
||||||
|
else:
|
||||||
|
assert response.status_code == 200
|
||||||
|
result = response.json().get("result")
|
||||||
|
assert isinstance(result, list)
|
||||||
|
if len(result) == 10:
|
||||||
|
for card in result:
|
||||||
|
assert "choice_A" in card
|
||||||
|
assert "choice_B" in card
|
||||||
|
assert "author_id" in card
|
||||||
|
assert "card_id" in card
|
||||||
|
else:
|
||||||
|
global ACTIVE_CARDS_LESS_THAN_TEN
|
||||||
|
ACTIVE_CARDS_LESS_THAN_TEN = True
|
||||||
|
pytest.skip(reason="The number of active cards is less than 10 in MongoDB")
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_random_cards_randomness():
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
params = {"user_id": EXIST_USER}
|
||||||
|
response1 = await client.get("/get_random_cards", params=params)
|
||||||
|
response2 = await client.get("/get_random_cards", params=params)
|
||||||
|
result1 = response1.json().get("result")
|
||||||
|
result2 = response2.json().get("result")
|
||||||
|
print(f"\nINPUT: endpoint=/get_random_cards (двойной вызов)\nOUTPUT 1: {result1}\nOUTPUT 2: {result2}")
|
||||||
|
if len(result1) == 10 and len(result2) == 10:
|
||||||
|
assert result1 != result2
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_random_cards_parallel_requests():
|
||||||
|
if NO_ACTIVE_CARDS_STATUS:
|
||||||
|
pytest.skip(reason="No active cards in MongoDB")
|
||||||
|
elif ACTIVE_CARDS_LESS_THAN_TEN:
|
||||||
|
pytest.skip(reason="The number of active cards is less than 10 in MongoDB")
|
||||||
|
else:
|
||||||
|
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
|
||||||
|
params = {"user_id": EXIST_USER}
|
||||||
|
tasks = [client.get("/get_random_cards", params=params) for _ in range(5)]
|
||||||
|
responses = await asyncio.gather(*tasks)
|
||||||
|
for resp in responses:
|
||||||
|
print(f"\nParallel call: status={resp.status_code} | json={resp.json()}")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
result = resp.json().get("result")
|
||||||
|
assert isinstance(result, list)
|
||||||
|
assert len(result) == 10
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
sys.path.append('..')
|
||||||
|
from mongo_worker import MongoWorker
|
||||||
|
from schemas.base_schemas import Card
|
||||||
|
|
||||||
|
|
||||||
|
def create_cards(num_cards=10, author_id=1):
|
||||||
|
cards_list = []
|
||||||
|
for card_id in range(1, num_cards + 1):
|
||||||
|
print(f"Creating Card {card_id}")
|
||||||
|
card = Card(
|
||||||
|
card_id=card_id,
|
||||||
|
choice_A=input("Choice A: "),
|
||||||
|
choice_B=input("Choice B: "),
|
||||||
|
author_id=author_id,
|
||||||
|
creation_date=datetime.now().isoformat(),
|
||||||
|
moderation_date=datetime.now().isoformat(),
|
||||||
|
active_status=True
|
||||||
|
)
|
||||||
|
cards_list.append(card)
|
||||||
|
return cards_list
|
||||||
|
|
||||||
|
def read_json(json_file):
|
||||||
|
try:
|
||||||
|
with open(json_file, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return [Card(**card) for card in data]
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading JSON file: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def write_json(cards_list, json_file):
|
||||||
|
try:
|
||||||
|
with open(json_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump([card.model_dump() for card in cards_list], f, ensure_ascii=False, indent=4)
|
||||||
|
print(f"Successfully saved {len(cards_list)} cards to {json_file}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error writing to JSON file: {e}")
|
||||||
|
|
||||||
|
def add_cards_to_mongodb(cards_list):
|
||||||
|
mongo = MongoWorker()
|
||||||
|
for card in cards_list:
|
||||||
|
mongo.add_card_by_base_model(card)
|
||||||
|
print(f"Successfully added {len(cards_list)} cards to MongoDB")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('-a', '--action', type=int, required=True, choices=[0, 1, 2],
|
||||||
|
help="0 - Manual input & save to MongoDB, 1 - Manual input & save to JSON, 2 - Read JSON & save to MongoDB")
|
||||||
|
parser.add_argument('-f', '--file', type=str, default="cards.json", help="JSON file name for reading/writing")
|
||||||
|
parser.add_argument('-n', '--num', type=int, default=10, help="Number of cards to create")
|
||||||
|
parser.add_argument('-u', '--user', type=int, default=1, help="Author ID")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.action == 0:
|
||||||
|
cards = create_cards(args.num, args.user)
|
||||||
|
add_cards_to_mongodb(cards)
|
||||||
|
elif args.action == 1:
|
||||||
|
cards = create_cards(args.num, args.user)
|
||||||
|
write_json(cards, args.file)
|
||||||
|
elif args.action == 2:
|
||||||
|
cards = read_json(args.file)
|
||||||
|
if cards:
|
||||||
|
add_cards_to_mongodb(cards)
|
||||||
|
else:
|
||||||
|
print("No valid cards found in JSON file.")
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from tools.data.dirty_words import dirty_words_set
|
||||||
|
|
||||||
|
def is_not_empty(text: str) -> bool:
|
||||||
|
"""Checks that the text is not empty."""
|
||||||
|
return bool(text.strip())
|
||||||
|
|
||||||
|
def has_no_links(text: str) -> bool:
|
||||||
|
"""Checks that there are no links in the text."""
|
||||||
|
url_pattern = re.compile(r'https?://\S+|www\.\S+')
|
||||||
|
return not bool(url_pattern.search(text))
|
||||||
|
|
||||||
|
def has_no_dirty_words(text: str) -> bool:
|
||||||
|
"""Checks that there are no forbidden words in the text."""
|
||||||
|
words = set(re.findall(r'\w+', text.lower()))
|
||||||
|
return not bool(words & dirty_words_set)
|
||||||
|
|
||||||
|
def text_lenght(text: str) -> bool:
|
||||||
|
"""Check max text lenght"""
|
||||||
|
return len(text) <= 150 # TODO: update in future
|
||||||
|
|
||||||
|
def moderate_text(text: str) -> bool:
|
||||||
|
return is_not_empty(text) and has_no_links(text) and has_no_dirty_words(text) and text_lenght(text)
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"card_id": 1,
|
||||||
|
"choice_A": "Котики",
|
||||||
|
"choice_B": "Собачки",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:56:05.290110",
|
||||||
|
"moderation_date": "2025-03-04T16:56:05.290143",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 2,
|
||||||
|
"choice_A": "Чай",
|
||||||
|
"choice_B": "Кофе",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:56:11.108762",
|
||||||
|
"moderation_date": "2025-03-04T16:56:11.108793",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 3,
|
||||||
|
"choice_A": "Отпуск в горах",
|
||||||
|
"choice_B": "Отпуск на море",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:56:25.062542",
|
||||||
|
"moderation_date": "2025-03-04T16:56:25.062586",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 4,
|
||||||
|
"choice_A": "День",
|
||||||
|
"choice_B": "Ночь",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:56:34.148692",
|
||||||
|
"moderation_date": "2025-03-04T16:56:34.148725",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 5,
|
||||||
|
"choice_A": "Работать в офисе",
|
||||||
|
"choice_B": "Работать удалённо",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:57:00.723472",
|
||||||
|
"moderation_date": "2025-03-04T16:57:00.723518",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 6,
|
||||||
|
"choice_A": "Заниматься спортом",
|
||||||
|
"choice_B": "Играть в видеоигры",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:57:31.524721",
|
||||||
|
"moderation_date": "2025-03-04T16:57:31.524754",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 7,
|
||||||
|
"choice_A": "Гулять",
|
||||||
|
"choice_B": "Сидеть дома",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:57:52.022380",
|
||||||
|
"moderation_date": "2025-03-04T16:57:52.022413",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 8,
|
||||||
|
"choice_A": "Посмотреть фильм",
|
||||||
|
"choice_B": "Почитать книжку",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:58:15.032749",
|
||||||
|
"moderation_date": "2025-03-04T16:58:15.032762",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 9,
|
||||||
|
"choice_A": "Зима",
|
||||||
|
"choice_B": "Лето",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:58:24.800416",
|
||||||
|
"moderation_date": "2025-03-04T16:58:24.800450",
|
||||||
|
"active_status": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"card_id": 10,
|
||||||
|
"choice_A": "Лучший друг/подруга",
|
||||||
|
"choice_B": "Любимый человек",
|
||||||
|
"count_choice_A": 0,
|
||||||
|
"count_choice_B": 0,
|
||||||
|
"count_total": 0,
|
||||||
|
"count_likes": 0,
|
||||||
|
"count_dislikes": 0,
|
||||||
|
"comments": [],
|
||||||
|
"author_id": 1,
|
||||||
|
"creation_date": "2025-03-04T16:59:05.265795",
|
||||||
|
"moderation_date": "2025-03-04T16:59:05.265829",
|
||||||
|
"active_status": true
|
||||||
|
}
|
||||||
|
]
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mongodb:
|
||||||
|
image: mongodb/mongodb-community-server
|
||||||
|
container_name: tort-mongodb
|
||||||
|
restart: always
|
||||||
|
network_mode: bridge
|
||||||
|
environment:
|
||||||
|
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER}
|
||||||
|
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASS}
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:${MONGO_PORT}:27017"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mongosh", "--username", "${MONGO_USER}", "--password", "${MONGO_PASS}", "--eval", "db.runCommand({ ping: 1 })"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 2
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./app
|
||||||
|
image: tort-backend:latest
|
||||||
|
depends_on:
|
||||||
|
mongodb:
|
||||||
|
condition: service_healthy
|
||||||
|
container_name: tort-backend
|
||||||
|
network_mode: "host"
|
||||||
|
environment:
|
||||||
|
MONGO_HOST: ${MONGO_HOST}
|
||||||
|
MONGO_PORT: ${MONGO_PORT}
|
||||||
|
MONGO_USER: ${MONGO_USER}
|
||||||
|
MONGO_PASS: ${MONGO_PASS}
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
fastapi==0.115.7
|
|
||||||
pymongo==4.10.1
|
|
||||||
Reference in New Issue
Block a user