ismailelghazi commited on
Commit
21f81f9
·
1 Parent(s): 6b42151

Upload Docker app

Browse files
Files changed (47) hide show
  1. Dockerfile +23 -0
  2. README.md +95 -10
  3. app/__init__.py +1 -0
  4. app/__pycache__/__init__.cpython-311.pyc +0 -0
  5. app/__pycache__/__init__.cpython-313.pyc +0 -0
  6. app/__pycache__/auth.cpython-311.pyc +0 -0
  7. app/__pycache__/auth.cpython-313.pyc +0 -0
  8. app/__pycache__/database.cpython-311.pyc +0 -0
  9. app/__pycache__/database.cpython-313.pyc +0 -0
  10. app/__pycache__/main.cpython-311.pyc +0 -0
  11. app/__pycache__/main.cpython-313.pyc +0 -0
  12. app/__pycache__/models.cpython-311.pyc +0 -0
  13. app/__pycache__/models.cpython-313.pyc +0 -0
  14. app/__pycache__/schemas.cpython-311.pyc +0 -0
  15. app/__pycache__/schemas.cpython-313.pyc +0 -0
  16. app/auth.py +25 -0
  17. app/database.py +33 -0
  18. app/main.py +41 -0
  19. app/models.py +9 -0
  20. app/routers/__init__.py +1 -0
  21. app/routers/__pycache__/__init__.cpython-311.pyc +0 -0
  22. app/routers/__pycache__/__init__.cpython-313.pyc +0 -0
  23. app/routers/__pycache__/auth_router.cpython-311.pyc +0 -0
  24. app/routers/__pycache__/auth_router.cpython-313.pyc +0 -0
  25. app/routers/__pycache__/translate_router.cpython-311.pyc +0 -0
  26. app/routers/__pycache__/translate_router.cpython-313.pyc +0 -0
  27. app/routers/auth_router.py +49 -0
  28. app/routers/translate_router.py +23 -0
  29. app/schemas.py +23 -0
  30. app/utils/__init__.py +1 -0
  31. app/utils/__pycache__/__init__.cpython-311.pyc +0 -0
  32. app/utils/__pycache__/__init__.cpython-313.pyc +0 -0
  33. app/utils/__pycache__/hashing.cpython-311.pyc +0 -0
  34. app/utils/__pycache__/hashing.cpython-313.pyc +0 -0
  35. app/utils/__pycache__/hf_client.cpython-311.pyc +0 -0
  36. app/utils/__pycache__/hf_client.cpython-313.pyc +0 -0
  37. app/utils/__pycache__/jwt_handler.cpython-311.pyc +0 -0
  38. app/utils/__pycache__/jwt_handler.cpython-313.pyc +0 -0
  39. app/utils/hashing.py +12 -0
  40. app/utils/hf_client.py +37 -0
  41. app/utils/jwt_handler.py +21 -0
  42. requirements.txt +12 -0
  43. test.db +0 -0
  44. tests/__init__.py +1 -0
  45. tests/__pycache__/__init__.cpython-311.pyc +0 -0
  46. tests/__pycache__/test_main.cpython-311-pytest-8.4.2.pyc +0 -0
  47. tests/test_main.py +106 -0
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.11-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies (needed for psycopg2)
8
+ RUN apt-get update && apt-get install -y libpq-dev gcc && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy the requirements file into the container at /app
11
+ COPY requirements.txt .
12
+
13
+ # Install any needed packages specified in requirements.txt
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy the rest of the application code
17
+ COPY . .
18
+
19
+ # Make port 8000 available to the world outside this container
20
+ EXPOSE 8000
21
+
22
+ # Run app.main:app when the container launches
23
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
README.md CHANGED
@@ -1,10 +1,95 @@
1
- ---
2
- title: Test
3
- emoji:
4
- colorFrom: green
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # TalAIt Translation Backend
2
+
3
+ A secure, private translation backend for TalAIt using FastAPI, PostgreSQL, and Hugging Face Inference API.
4
+
5
+ ## Features
6
+
7
+ - **Authentication**: JWT stored in HTTP-only cookies.
8
+ - **Translation**: English <-> French translation using Helsinki-NLP models.
9
+ - **Security**: Password hashing with Bcrypt, secure cookie handling.
10
+ - **Database**: PostgreSQL for user storage.
11
+ - **Dockerized**: Easy deployment with Docker Compose.
12
+
13
+ ## Architecture
14
+
15
+ 1. **FastAPI**: Handles HTTP requests and routing.
16
+ 2. **PostgreSQL**: Stores user credentials (hashed).
17
+ 3. **Hugging Face API**: Performs the actual translation.
18
+ 4. **JWT**: Manages session state via secure cookies.
19
+
20
+ ### Authentication Flow
21
+
22
+ 1. **Register**: User sends username/password -> Backend hashes password -> Stores in DB.
23
+ 2. **Login**: User sends credentials -> Backend verifies -> Generates JWT -> Sets `access_token` HTTP-only cookie.
24
+ 3. **Protected Routes**: Browser automatically sends cookie -> Backend validates JWT -> Grants access.
25
+ 4. **Logout**: Backend clears the cookie.
26
+
27
+ ## Setup & Running
28
+
29
+ ### Prerequisites
30
+
31
+ - Docker & Docker Compose
32
+ - Hugging Face API Token
33
+
34
+ ### Environment Variables
35
+
36
+ Create a `.env` file in the `backend` directory (or set them in `docker-compose.yml`):
37
+
38
+ ```env
39
+ POSTGRES_USER=postgres
40
+ POSTGRES_PASSWORD=password
41
+ POSTGRES_DB=talait
42
+ HF_TOKEN=your_hf_token_here
43
+ JWT_SECRET=your_jwt_secret
44
+ ```
45
+
46
+ ### Running Locally
47
+
48
+ 1. Navigate to the `backend` directory:
49
+ ```bash
50
+ cd backend
51
+ ```
52
+
53
+ 2. Install dependencies:
54
+ ```bash
55
+ pip install -r requirements.txt
56
+ ```
57
+
58
+ 3. Start the server using the provided script (Windows):
59
+ ```bash
60
+ .\start_backend.bat
61
+ ```
62
+ Or manually:
63
+ ```bash
64
+ uvicorn app.main:app --reload
65
+ ```
66
+ **Note:** Make sure to run `uvicorn` from the `backend` directory, not `backend/app`.
67
+
68
+ The API will be available at `http://localhost:8000`.
69
+ Docs will be at `http://localhost:8000/docs`.
70
+
71
+ ### Running with Docker
72
+
73
+ 1. Navigate to the `backend` directory:
74
+ ```bash
75
+ cd backend
76
+ ```
77
+
78
+ 2. Build and start the services:
79
+ ```bash
80
+ docker-compose up --build
81
+ ```
82
+
83
+ The API will be available at `http://localhost:8000`.
84
+ Adminer (DB GUI) will be at `http://localhost:8080`.
85
+
86
+ ## API Endpoints
87
+
88
+ - `POST /register`: Create a new user.
89
+ - `POST /login`: Authenticate and receive cookie.
90
+ - `POST /logout`: Clear authentication cookie.
91
+ - `POST /translate`: Translate text (Requires Auth).
92
+
93
+ ## Hugging Face Limits
94
+
95
+ The free Hugging Face Inference API has rate limits. If you encounter 503 errors, it means the model is loading. If you hit rate limits, consider upgrading to a paid plan or hosting the models yourself.
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # init
app/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (174 Bytes). View file
 
app/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (162 Bytes). View file
 
app/__pycache__/auth.cpython-311.pyc ADDED
Binary file (1.85 kB). View file
 
app/__pycache__/auth.cpython-313.pyc ADDED
Binary file (1.1 kB). View file
 
app/__pycache__/database.cpython-311.pyc ADDED
Binary file (1.82 kB). View file
 
app/__pycache__/database.cpython-313.pyc ADDED
Binary file (1.62 kB). View file
 
app/__pycache__/main.cpython-311.pyc ADDED
Binary file (1.87 kB). View file
 
app/__pycache__/main.cpython-313.pyc ADDED
Binary file (2.1 kB). View file
 
app/__pycache__/models.cpython-311.pyc ADDED
Binary file (837 Bytes). View file
 
app/__pycache__/models.cpython-313.pyc ADDED
Binary file (734 Bytes). View file
 
app/__pycache__/schemas.cpython-311.pyc ADDED
Binary file (1.82 kB). View file
 
app/__pycache__/schemas.cpython-313.pyc ADDED
Binary file (1.61 kB). View file
 
app/auth.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import Request, HTTPException, status, Depends
2
+ from sqlalchemy.orm import Session
3
+ from .utils import jwt_handler
4
+ from .database import get_db
5
+ from . import models
6
+
7
+ def get_current_user(request: Request, db: Session = Depends(get_db)):
8
+ token = request.cookies.get("access_token")
9
+ if not token:
10
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
11
+
12
+ # Remove "Bearer " prefix if present
13
+ if token.startswith("Bearer "):
14
+ token = token.split(" ")[1]
15
+
16
+ payload = jwt_handler.verify_token(token)
17
+ if not payload:
18
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token")
19
+
20
+ username = payload.get("sub")
21
+ user = db.query(models.User).filter(models.User.username == username).first()
22
+ if not user:
23
+ raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
24
+
25
+ return user
app/database.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine
2
+ from sqlalchemy.ext.declarative import declarative_base
3
+ from sqlalchemy.orm import sessionmaker
4
+ import os
5
+ import urllib.parse
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+ POSTGRES_USER = os.getenv("POSTGRES_USER", "postgres")
11
+ POSTGRES_PASSWORD = os.getenv("POSTGRES_PASSWORD", "password")
12
+ POSTGRES_SERVER = os.getenv("POSTGRES_SERVER", "localhost")
13
+ POSTGRES_DB = os.getenv("POSTGRES_DB", "talait")
14
+
15
+ encoded_user = urllib.parse.quote_plus(POSTGRES_USER)
16
+ encoded_password = urllib.parse.quote_plus(POSTGRES_PASSWORD)
17
+
18
+ DATABASE_URL = os.getenv("DATABASE_URL", f"postgresql://{encoded_user}:{encoded_password}@{POSTGRES_SERVER}/{POSTGRES_DB}")
19
+
20
+ engine = create_engine(
21
+ DATABASE_URL,
22
+ connect_args={"client_encoding": "utf8"}
23
+ )
24
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
25
+
26
+ Base = declarative_base()
27
+
28
+ def get_db():
29
+ db = SessionLocal()
30
+ try:
31
+ yield db
32
+ finally:
33
+ db.close()
app/main.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+ load_dotenv()
3
+
4
+ from fastapi import FastAPI, Request
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi.responses import JSONResponse
7
+ from contextlib import asynccontextmanager
8
+ from .database import engine, Base
9
+ from .routers import auth_router, translate_router
10
+
11
+ @asynccontextmanager
12
+ async def lifespan(app: FastAPI):
13
+ # Create tables
14
+ Base.metadata.create_all(bind=engine)
15
+ yield
16
+
17
+ app = FastAPI(title="TalAIt Backend", lifespan=lifespan)
18
+
19
+
20
+
21
+ # CORS Configuration
22
+ origins = [
23
+ "http://localhost",
24
+ "http://localhost:3000",
25
+ "http://localhost:8080",
26
+ ]
27
+
28
+ app.add_middleware(
29
+ CORSMiddleware,
30
+ allow_origins=origins,
31
+ allow_credentials=True,
32
+ allow_methods=["*"],
33
+ allow_headers=["*"],
34
+ )
35
+
36
+ app.include_router(auth_router.router)
37
+ app.include_router(translate_router.router)
38
+
39
+ @app.get("/")
40
+ def root():
41
+ return {"message": "Welcome to TalAIt Translation API"}
app/models.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String
2
+ from .database import Base
3
+
4
+ class User(Base):
5
+ __tablename__ = "users"
6
+
7
+ id = Column(Integer, primary_key=True, index=True)
8
+ username = Column(String, unique=True, index=True, nullable=False)
9
+ password_hash = Column(String, nullable=False)
app/routers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # init
app/routers/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (182 Bytes). View file
 
app/routers/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (170 Bytes). View file
 
app/routers/__pycache__/auth_router.cpython-311.pyc ADDED
Binary file (3.89 kB). View file
 
app/routers/__pycache__/auth_router.cpython-313.pyc ADDED
Binary file (3.27 kB). View file
 
app/routers/__pycache__/translate_router.cpython-311.pyc ADDED
Binary file (1.89 kB). View file
 
app/routers/__pycache__/translate_router.cpython-313.pyc ADDED
Binary file (1.66 kB). View file
 
app/routers/auth_router.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Response
2
+ from sqlalchemy.orm import Session
3
+ from .. import schemas, models, database, auth
4
+ from ..utils import hashing, jwt_handler
5
+
6
+ router = APIRouter(tags=["Authentication"])
7
+
8
+ @router.post("/register", response_model=schemas.UserResponse)
9
+ def register(user: schemas.UserCreate, db: Session = Depends(database.get_db)):
10
+ user_exists = db.query(models.User).filter(models.User.username == user.username).first()
11
+ if user_exists:
12
+ raise HTTPException(status_code=400, detail="Username already registered")
13
+
14
+ new_user = models.User(username=user.username, password_hash=hashing.Hash.bcrypt(user.password))
15
+ db.add(new_user)
16
+ db.commit()
17
+ db.refresh(new_user)
18
+ return new_user
19
+
20
+ @router.post("/login")
21
+ def login(response: Response, user: schemas.UserCreate, db: Session = Depends(database.get_db)):
22
+ db_user = db.query(models.User).filter(models.User.username == user.username).first()
23
+ if not db_user:
24
+ raise HTTPException(status_code=404, detail="Invalid Credentials")
25
+
26
+ if not hashing.Hash.verify(user.password, db_user.password_hash):
27
+ raise HTTPException(status_code=404, detail="Invalid Credentials")
28
+
29
+ access_token = jwt_handler.create_access_token(data={"sub": db_user.username})
30
+
31
+ # Set HTTP-only cookie
32
+ response.set_cookie(
33
+ key="access_token",
34
+ value=f"Bearer {access_token}",
35
+ httponly=True,
36
+ secure=False, # Set to False for localhost (HTTP)
37
+ samesite="lax"
38
+ )
39
+
40
+ return {"message": "logged in"}
41
+
42
+ @router.post("/logout")
43
+ def logout(response: Response):
44
+ response.delete_cookie("access_token")
45
+ return {"message": "logged out"}
46
+
47
+ @router.get("/me", response_model=schemas.UserResponse)
48
+ def read_users_me(current_user: models.User = Depends(auth.get_current_user)):
49
+ return current_user
app/routers/translate_router.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, Request, status
2
+ from .. import schemas, auth, models
3
+ from ..utils import hf_client
4
+
5
+ router = APIRouter(tags=["Translation"])
6
+
7
+ @router.post("/translate", response_model=schemas.TranslationResponse)
8
+ def translate(request: schemas.TranslationRequest, user: models.User = Depends(auth.get_current_user)):
9
+ if not request.text:
10
+ raise HTTPException(status_code=400, detail="Text cannot be empty")
11
+
12
+ try:
13
+ translation = hf_client.translate_text(request.text, request.direction)
14
+ except ValueError as e:
15
+ raise HTTPException(status_code=400, detail=str(e))
16
+
17
+ if isinstance(translation, dict) and translation.get("status") == 503:
18
+ raise HTTPException(status_code=503, detail=translation.get("error"))
19
+
20
+ if translation is None:
21
+ raise HTTPException(status_code=502, detail="Translation service unavailable")
22
+
23
+ return {"translation": translation}
app/schemas.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ class UserCreate(BaseModel):
4
+ username: str
5
+ password: str
6
+
7
+ class UserResponse(BaseModel):
8
+ id: int
9
+ username: str
10
+
11
+ class Config:
12
+ from_attributes = True
13
+
14
+ class Token(BaseModel):
15
+ access_token: str
16
+ token_type: str
17
+
18
+ class TranslationRequest(BaseModel):
19
+ text: str
20
+ direction: str # "fr-en" or "en-fr"
21
+
22
+ class TranslationResponse(BaseModel):
23
+ translation: str
app/utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # init
app/utils/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (180 Bytes). View file
 
app/utils/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (168 Bytes). View file
 
app/utils/__pycache__/hashing.cpython-311.pyc ADDED
Binary file (1.12 kB). View file
 
app/utils/__pycache__/hashing.cpython-313.pyc ADDED
Binary file (997 Bytes). View file
 
app/utils/__pycache__/hf_client.cpython-311.pyc ADDED
Binary file (1.98 kB). View file
 
app/utils/__pycache__/hf_client.cpython-313.pyc ADDED
Binary file (1.8 kB). View file
 
app/utils/__pycache__/jwt_handler.cpython-311.pyc ADDED
Binary file (1.5 kB). View file
 
app/utils/__pycache__/jwt_handler.cpython-313.pyc ADDED
Binary file (1.34 kB). View file
 
app/utils/hashing.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from passlib.context import CryptContext
2
+
3
+ pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
4
+
5
+ class Hash:
6
+ @staticmethod
7
+ def bcrypt(password: str):
8
+ return pwd_context.hash(password)
9
+
10
+ @staticmethod
11
+ def verify(plain_password, hashed_password):
12
+ return pwd_context.verify(plain_password, hashed_password)
app/utils/hf_client.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import os
3
+
4
+ HF_TOKEN = os.getenv("HF_TOKEN")
5
+ API_URL_TEMPLATE = "https://router.huggingface.co/hf-inference/models/{model}"
6
+
7
+ MODELS = {
8
+ "fr-en": "Helsinki-NLP/opus-mt-fr-en",
9
+ "en-fr": "Helsinki-NLP/opus-mt-en-fr"
10
+ }
11
+
12
+ def translate_text(text: str, direction: str):
13
+ if direction not in MODELS:
14
+ raise ValueError("Invalid direction")
15
+
16
+ model = MODELS[direction]
17
+ api_url = API_URL_TEMPLATE.format(model=model)
18
+ headers = {"Authorization": f"Bearer {HF_TOKEN}"}
19
+ payload = {"inputs": text}
20
+
21
+ try:
22
+ response = requests.post(api_url, headers=headers, json=payload, timeout=10)
23
+
24
+ if response.status_code == 503:
25
+ # Model loading
26
+ return {"error": "Model is loading, please try again shortly", "status": 503}
27
+
28
+ response.raise_for_status()
29
+ result = response.json()
30
+
31
+ if isinstance(result, list) and len(result) > 0 and 'translation_text' in result[0]:
32
+ return result[0]['translation_text']
33
+ else:
34
+ return None
35
+ except requests.exceptions.RequestException as e:
36
+ print(f"HF API Error: {e}")
37
+ return None
app/utils/jwt_handler.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from jose import jwt, JWTError
3
+ import os
4
+
5
+ SECRET_KEY = os.getenv("JWT_SECRET", "secret")
6
+ ALGORITHM = "HS256"
7
+ ACCESS_TOKEN_EXPIRE_MINUTES = 30
8
+
9
+ def create_access_token(data: dict):
10
+ to_encode = data.copy()
11
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
12
+ to_encode.update({"exp": expire})
13
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
14
+ return encoded_jwt
15
+
16
+ def verify_token(token: str):
17
+ try:
18
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
19
+ return payload
20
+ except JWTError:
21
+ return None
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ sqlalchemy
4
+ psycopg2-binary
5
+ python-jose[cryptography]
6
+ passlib[argon2]
7
+ argon2-cffi
8
+ python-multipart
9
+ requests
10
+ python-dotenv
11
+ httpx
12
+ pytest
test.db ADDED
Binary file (16.4 kB). View file
 
tests/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # init
tests/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (176 Bytes). View file
 
tests/__pycache__/test_main.cpython-311-pytest-8.4.2.pyc ADDED
Binary file (9.81 kB). View file
 
tests/test_main.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ # Set env vars before importing app modules to ensure they use the test DB
3
+ os.environ["DATABASE_URL"] = "sqlite:///./test.db"
4
+ os.environ["POSTGRES_USER"] = "user"
5
+ os.environ["POSTGRES_PASSWORD"] = "password"
6
+ os.environ["POSTGRES_DB"] = "dbname"
7
+ os.environ["JWT_SECRET"] = "testsecret"
8
+ os.environ["HF_TOKEN"] = "testtoken"
9
+
10
+ from fastapi.testclient import TestClient
11
+ from sqlalchemy import create_engine, StaticPool
12
+ from sqlalchemy.orm import sessionmaker
13
+ from app.main import app
14
+ from app.database import Base, get_db
15
+ from app.utils import hf_client
16
+ from unittest.mock import MagicMock
17
+
18
+ # Use SQLite for testing
19
+ SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
20
+
21
+ engine = create_engine(
22
+ SQLALCHEMY_DATABASE_URL,
23
+ connect_args={"check_same_thread": False},
24
+ poolclass=StaticPool,
25
+ )
26
+ TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
27
+
28
+ # Patch the app's engine to use our test engine
29
+ from app import database
30
+ database.engine = engine
31
+
32
+ Base.metadata.create_all(bind=engine)
33
+
34
+ def override_get_db():
35
+ try:
36
+ db = TestingSessionLocal()
37
+ yield db
38
+ finally:
39
+ db.close()
40
+
41
+ app.dependency_overrides[get_db] = override_get_db
42
+
43
+ client = TestClient(app)
44
+
45
+ def test_register():
46
+ response = client.post(
47
+ "/register",
48
+ json={"username": "testuser", "password": "testpassword"},
49
+ )
50
+ assert response.status_code == 200
51
+ assert response.json()["username"] == "testuser"
52
+
53
+ def test_login():
54
+ client.post(
55
+ "/register",
56
+ json={"username": "testuser2", "password": "testpassword"},
57
+ )
58
+ response = client.post(
59
+ "/login",
60
+ json={"username": "testuser2", "password": "testpassword"},
61
+ )
62
+ assert response.status_code == 200
63
+ assert "access_token" in response.cookies
64
+
65
+ def test_login_wrong_password():
66
+ client.post(
67
+ "/register",
68
+ json={"username": "testuser3", "password": "testpassword"},
69
+ )
70
+ response = client.post(
71
+ "/login",
72
+ json={"username": "testuser3", "password": "wrongpassword"},
73
+ )
74
+ assert response.status_code == 404
75
+
76
+ def test_translate_no_cookie():
77
+ response = client.post(
78
+ "/translate",
79
+ json={"text": "Hello", "direction": "en-fr"},
80
+ )
81
+ assert response.status_code == 401
82
+
83
+ def test_translate_success(monkeypatch):
84
+ # Mock HF API
85
+ def mock_translate(*args, **kwargs):
86
+ return "Bonjour"
87
+
88
+ monkeypatch.setattr(hf_client, "translate_text", mock_translate)
89
+
90
+ # Login first
91
+ client.post(
92
+ "/register",
93
+ json={"username": "testuser4", "password": "testpassword"},
94
+ )
95
+ login_response = client.post(
96
+ "/login",
97
+ json={"username": "testuser4", "password": "testpassword"},
98
+ )
99
+
100
+ response = client.post(
101
+ "/translate",
102
+ json={"text": "Hello", "direction": "en-fr"},
103
+ cookies=login_response.cookies
104
+ )
105
+ assert response.status_code == 200
106
+ assert response.json()["translation"] == "Bonjour"