Added full suite of tests & added testing to CI/CD
This commit is contained in:
60
.github/workflows/deploy.yml
vendored
60
.github/workflows/deploy.yml
vendored
@@ -19,8 +19,66 @@ on:
|
||||
- cron: '0 3 * * 0'
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
# ========================================================================
|
||||
# Job to run unit tests.
|
||||
# ========================================================================
|
||||
test:
|
||||
name: Run Linters and Tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Checks out the repo under $GITHUB_WORKSPACE
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Sets up Python 3.12 environment
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
# Cache pip dependencies for faster reruns
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
|
||||
- name: Lint with Ruff
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
ruff check .
|
||||
|
||||
- name: Check formatting with Black
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
black --check .
|
||||
|
||||
- name: Run Pytest
|
||||
working-directory: ./backend
|
||||
run: |
|
||||
pytest
|
||||
|
||||
# ========================================================================
|
||||
# Job to build and deploy the Docker image to mara.
|
||||
# ========================================================================
|
||||
build-and-deploy:
|
||||
name: Build and Deploy
|
||||
runs-on: ubuntu-latest
|
||||
needs: test # Ensure tests pass before deploying
|
||||
|
||||
# Only run this job if triggered by a push to main or manual dispatch/schedule
|
||||
# This prevents it running for PRs (eventually)
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
|
||||
|
||||
steps:
|
||||
# Checks out the repo under $GITHUB_WORKSPACE
|
||||
- name: Checkout code
|
||||
|
||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"backend"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -3,12 +3,14 @@ from pydantic_settings import BaseSettings
|
||||
from pydantic import Field # Import Field for potential default values if needed
|
||||
import os
|
||||
|
||||
DOTENV_PATH = os.path.join(os.path.dirname(__file__), "../.env")
|
||||
|
||||
class Settings(BaseSettings):
|
||||
# Database settings - reads DB_URL from environment or .env
|
||||
DB_URL: str
|
||||
DB_URL: str = "postgresql://maia:maia@localhost:5432/maia"
|
||||
|
||||
# Redis settings - reads REDIS_URL from environment or .env, also used for Celery.
|
||||
REDIS_URL: str
|
||||
REDIS_URL: str ="redis://localhost:6379/0"
|
||||
|
||||
# JWT settings - reads from environment or .env
|
||||
JWT_ALGORITHM: str = "HS256"
|
||||
@@ -22,7 +24,7 @@ class Settings(BaseSettings):
|
||||
|
||||
class Config:
|
||||
# Tell pydantic-settings to load variables from a .env file
|
||||
env_file = '.env'
|
||||
env_file = DOTENV_PATH
|
||||
env_file_encoding = 'utf-8'
|
||||
extra = 'ignore'
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
# modules/calendar/api.py
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
@@ -12,7 +12,7 @@ from modules.calendar.service import create_calendar_event, get_calendar_event_b
|
||||
|
||||
router = APIRouter(prefix="/calendar", tags=["calendar"])
|
||||
|
||||
@router.post("/events", response_model=CalendarEventResponse)
|
||||
@router.post("/events", response_model=CalendarEventResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_event(
|
||||
event: CalendarEventCreate,
|
||||
user: User = Depends(get_current_user),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# modules/calendar/models.py
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON # Add JSON
|
||||
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, JSON, Boolean # Add Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from core.database import Base
|
||||
|
||||
@@ -12,6 +12,7 @@ class CalendarEvent(Base):
|
||||
start = Column(DateTime, nullable=False)
|
||||
end = Column(DateTime)
|
||||
location = Column(String)
|
||||
all_day = Column(Boolean, default=False) # Add all_day column
|
||||
tags = Column(JSON)
|
||||
color = Column(String) # hex code for color
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False) # <-- Relationship
|
||||
|
||||
@@ -11,6 +11,7 @@ class CalendarEventBase(BaseModel):
|
||||
end: Optional[datetime] = None
|
||||
location: Optional[str] = None
|
||||
color: Optional[str] = None # Assuming color exists
|
||||
all_day: Optional[bool] = None # Add all_day field
|
||||
tags: Optional[List[str]] = None # Add optional tags
|
||||
|
||||
@field_validator('tags', mode='before')
|
||||
@@ -32,6 +33,7 @@ class CalendarEventUpdate(BaseModel):
|
||||
end: Optional[datetime] = None
|
||||
location: Optional[str] = None
|
||||
color: Optional[str] = None
|
||||
all_day: Optional[bool] = None # Add all_day field
|
||||
tags: Optional[List[str]] = None # Add optional tags for update
|
||||
|
||||
@field_validator('tags', mode='before')
|
||||
|
||||
4
backend/requirements-dev.txt
Normal file
4
backend/requirements-dev.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
pytest
|
||||
pytest-cov # For checking test coverage (optional)
|
||||
ruff # Or flake8, pylint etc. for linting
|
||||
black # For code formatting checks
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
backend/tests/__pycache__/test_main.cpython-312-pytest-8.3.5.pyc
Normal file
BIN
backend/tests/__pycache__/test_main.cpython-312-pytest-8.3.5.pyc
Normal file
Binary file not shown.
BIN
backend/tests/__pycache__/test_nlp.cpython-312-pytest-8.3.5.pyc
Normal file
BIN
backend/tests/__pycache__/test_nlp.cpython-312-pytest-8.3.5.pyc
Normal file
Binary file not shown.
BIN
backend/tests/__pycache__/test_todo.cpython-312-pytest-8.3.5.pyc
Normal file
BIN
backend/tests/__pycache__/test_todo.cpython-312-pytest-8.3.5.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -8,13 +8,14 @@ from modules.auth.models import User
|
||||
from modules.auth.security import authenticate_user, create_access_token, create_refresh_token, hash_password
|
||||
from modules.auth.schemas import UserRole
|
||||
from tests.conftest import fake
|
||||
from typing import Optional # Import Optional
|
||||
|
||||
|
||||
def create_user(db: Session, is_admin: bool = False) -> User:
|
||||
def create_user(db: Session, is_admin: bool = False, username: Optional[str] = None) -> User:
|
||||
unhashed_password = fake.password()
|
||||
_user = User(
|
||||
name=fake.name(),
|
||||
username=fake.user_name(),
|
||||
username=username or fake.user_name(), # Use provided username or generate one
|
||||
hashed_password=hash_password(unhashed_password),
|
||||
uuid=uuid_pkg.uuid4(),
|
||||
role=UserRole.ADMIN if is_admin else UserRole.USER,
|
||||
|
||||
79
backend/tests/test_admin.py
Normal file
79
backend/tests/test_admin.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
from unittest.mock import patch
|
||||
|
||||
from tests.helpers import generators
|
||||
from modules.auth.models import UserRole
|
||||
|
||||
# Test admin routes require admin privileges
|
||||
|
||||
def test_read_admin_unauthorized(client: TestClient) -> None:
|
||||
"""Test accessing admin route without authentication."""
|
||||
response = client.get("/api/admin/")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_read_admin_forbidden(db: Session, client: TestClient) -> None:
|
||||
"""Test accessing admin route as a non-admin user."""
|
||||
user, password = generators.create_user(db, is_admin=False) # Use is_admin=False
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
response = client.get("/api/admin/", headers={"Authorization": f"Bearer {access_token}"})
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_read_admin_success(db: Session, client: TestClient) -> None:
|
||||
"""Test accessing admin route as an admin user."""
|
||||
admin_user, password = generators.create_user(db, is_admin=True) # Use is_admin=True
|
||||
login_rsp = generators.login(db, admin_user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
response = client.get("/api/admin/", headers={"Authorization": f"Bearer {access_token}"})
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {"message": "Admin route"}
|
||||
|
||||
@patch("modules.admin.api.cleardb.delay") # Mock the celery task
|
||||
def test_clear_db_soft(mock_cleardb_delay, db: Session, client: TestClient) -> None:
|
||||
"""Test soft clearing the database as admin."""
|
||||
admin_user, password = generators.create_user(db, is_admin=True) # Use is_admin=True
|
||||
login_rsp = generators.login(db, admin_user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
response = client.post(
|
||||
"/api/admin/cleardb",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
json={"hard": False}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {"message": "Clearing database in the background", "hard": False}
|
||||
mock_cleardb_delay.assert_called_once_with(False)
|
||||
|
||||
@patch("modules.admin.api.cleardb.delay") # Mock the celery task
|
||||
def test_clear_db_hard(mock_cleardb_delay, db: Session, client: TestClient) -> None:
|
||||
"""Test hard clearing the database as admin."""
|
||||
admin_user, password = generators.create_user(db, is_admin=True) # Use is_admin=True
|
||||
login_rsp = generators.login(db, admin_user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
response = client.post(
|
||||
"/api/admin/cleardb",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
json={"hard": True}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == {"message": "Clearing database in the background", "hard": True}
|
||||
mock_cleardb_delay.assert_called_once_with(True)
|
||||
|
||||
def test_clear_db_forbidden(db: Session, client: TestClient) -> None:
|
||||
"""Test clearing the database as a non-admin user."""
|
||||
user, password = generators.create_user(db, is_admin=False) # Use is_admin=False
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
response = client.post(
|
||||
"/api/admin/cleardb",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
json={"hard": False}
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
@@ -61,8 +61,8 @@ def test_refresh_token(db: Session, client: TestClient) -> None:
|
||||
|
||||
response = client.post(
|
||||
"/api/auth/refresh",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
|
||||
json={"refresh_token": refresh_token},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
@@ -80,8 +80,8 @@ def test_logout(db: Session, client: TestClient) -> None:
|
||||
|
||||
response = client.post(
|
||||
"/api/auth/logout",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
|
||||
json={"refresh_token": refresh_token},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
@@ -98,7 +98,8 @@ def test_logout(db: Session, client: TestClient) -> None:
|
||||
|
||||
response = client.post(
|
||||
"/api/auth/refresh",
|
||||
cookies={"refresh_token": refresh_token},
|
||||
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
|
||||
json={"refresh_token": refresh_token},
|
||||
)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
@@ -178,3 +179,52 @@ def test_delete_user(db: Session, client: TestClient) -> None:
|
||||
deleted_user = db.query(User).filter(User.username == user.username).first()
|
||||
assert deleted_user is None
|
||||
|
||||
def test_get_user_forbidden(db: Session, client: TestClient) -> None:
|
||||
"""Test getting another user's profile (should be forbidden)."""
|
||||
user1, password_user1 = generators.create_user(db, username="user1_get_forbidden")
|
||||
user2, _ = generators.create_user(db, username="user2_get_forbidden")
|
||||
|
||||
# Log in as user1
|
||||
login_rsp = generators.login(db, user1.username, password_user1)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
# Try to get user2's profile
|
||||
response = client.get(
|
||||
f"/api/user/{user2.username}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_update_user_forbidden(db: Session, client: TestClient) -> None:
|
||||
"""Test updating another user's profile (should be forbidden)."""
|
||||
user1, password_user1 = generators.create_user(db, username="user1_update_forbidden")
|
||||
user2, _ = generators.create_user(db, username="user2_update_forbidden")
|
||||
new_name = fake.name()
|
||||
|
||||
# Log in as user1
|
||||
login_rsp = generators.login(db, user1.username, password_user1)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
# Try to update user2's profile
|
||||
response = client.patch(
|
||||
f"/api/user/{user2.username}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
json={"name": new_name},
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
def test_delete_user_forbidden(db: Session, client: TestClient) -> None:
|
||||
"""Test deleting another user's profile (should be forbidden)."""
|
||||
user1, password_user1 = generators.create_user(db, username="user1_delete_forbidden")
|
||||
user2, _ = generators.create_user(db, username="user2_delete_forbidden")
|
||||
|
||||
# Log in as user1
|
||||
login_rsp = generators.login(db, user1.username, password_user1)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
# Try to delete user2's profile
|
||||
response = client.delete(
|
||||
f"/api/user/{user2.username}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
)
|
||||
assert response.status_code == status.HTTP_403_FORBIDDEN
|
||||
|
||||
@@ -1,44 +1,389 @@
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from tests.helpers import generators
|
||||
from modules.calendar.models import CalendarEvent # Assuming model exists
|
||||
from tests.conftest import fake
|
||||
|
||||
# Helper function to create an event payload
|
||||
def create_event_payload(start_offset_days=0, end_offset_days=1):
|
||||
start_time = datetime.utcnow() + timedelta(days=start_offset_days)
|
||||
end_time = datetime.utcnow() + timedelta(days=end_offset_days)
|
||||
return {
|
||||
"title": fake.sentence(nb_words=3),
|
||||
"description": fake.text(),
|
||||
"start": start_time.isoformat(), # Rename start_time to start
|
||||
"end": end_time.isoformat(), # Rename end_time to end
|
||||
"all_day": fake.boolean(),
|
||||
}
|
||||
|
||||
def test_create_event(client: TestClient, db: Session) -> None:
|
||||
user, unhashed_password = generators.create_user(db)
|
||||
rsp = generators.login(db, user.username, unhashed_password)
|
||||
access_token = rsp["access_token"]
|
||||
refresh_token = rsp["refresh_token"]
|
||||
# --- Test Create Event ---
|
||||
|
||||
response = client.post("/api/calendar/events",
|
||||
json={
|
||||
"title": "Test Event",
|
||||
"start_time": "2024-03-20T15:00:00Z"
|
||||
},
|
||||
def test_create_event_unauthorized(client: TestClient) -> None:
|
||||
"""Test creating an event without authentication."""
|
||||
payload = create_event_payload()
|
||||
response = client.post("/api/calendar/events", json=payload)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_create_event_success(db: Session, client: TestClient) -> None:
|
||||
"""Test creating a calendar event successfully."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
payload = create_event_payload()
|
||||
|
||||
response = client.post(
|
||||
"/api/calendar/events",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json=payload
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["title"] == "Test Event"
|
||||
assert response.status_code == status.HTTP_201_CREATED # Change expected status to 201
|
||||
data = response.json()
|
||||
assert data["title"] == payload["title"]
|
||||
assert data["description"] == payload["description"]
|
||||
# Remove the '+ "Z"' as the API doesn't add it
|
||||
assert data["start"] == payload["start"]
|
||||
assert data["end"] == payload["end"]
|
||||
assert data["all_day"] == payload["all_day"]
|
||||
assert "id" in data
|
||||
assert data["user_id"] == user.id
|
||||
|
||||
def test_get_events(client: TestClient, db: Session) -> None:
|
||||
user, unhashed_password = generators.create_user(db)
|
||||
rsp = generators.login(db, user.username, unhashed_password)
|
||||
access_token = rsp["access_token"]
|
||||
refresh_token = rsp["refresh_token"]
|
||||
# Verify in DB
|
||||
event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == data["id"]).first()
|
||||
assert event_in_db is not None
|
||||
assert event_in_db.user_id == user.id
|
||||
assert event_in_db.title == payload["title"]
|
||||
|
||||
# Create an event to retrieve
|
||||
client.post("/api/calendar/events",
|
||||
json={
|
||||
"title": "Test Event",
|
||||
"start_time": "2024-03-20T15:00:00Z"
|
||||
},
|
||||
# --- Test Get Events ---
|
||||
|
||||
def test_get_events_unauthorized(client: TestClient) -> None:
|
||||
"""Test getting events without authentication."""
|
||||
response = client.get("/api/calendar/events")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_get_events_success(db: Session, client: TestClient) -> None:
|
||||
"""Test getting all calendar events for a user."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
# Create a couple of events for the user
|
||||
payload1 = create_event_payload(0, 1)
|
||||
client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload1)
|
||||
payload2 = create_event_payload(2, 3)
|
||||
client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload2)
|
||||
|
||||
# Create an event for another user (should not be returned)
|
||||
other_user, other_password = generators.create_user(db)
|
||||
other_login_rsp = generators.login(db, other_user.username, other_password)
|
||||
other_access_token = other_login_rsp["access_token"]
|
||||
other_payload = create_event_payload(4, 5)
|
||||
client.post("/api/calendar/events", headers={"Authorization": f"Bearer {other_access_token}"}, json=other_payload)
|
||||
|
||||
|
||||
response = client.get(
|
||||
"/api/calendar/events",
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert len(data) == 2
|
||||
assert data[0]["title"] == payload1["title"]
|
||||
assert data[1]["title"] == payload2["title"]
|
||||
assert data[0]["user_id"] == user.id
|
||||
assert data[1]["user_id"] == user.id
|
||||
|
||||
|
||||
def test_get_events_filtered(db: Session, client: TestClient) -> None:
|
||||
"""Test getting filtered calendar events for a user."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
|
||||
# Create events
|
||||
payload1 = create_event_payload(0, 1) # Today -> Tomorrow
|
||||
client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload1)
|
||||
payload2 = create_event_payload(5, 6) # In 5 days -> In 6 days
|
||||
client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload2)
|
||||
payload3 = create_event_payload(10, 11) # In 10 days -> In 11 days
|
||||
client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload3)
|
||||
|
||||
# Filter for events starting within the next week
|
||||
start_filter = datetime.utcnow().isoformat()
|
||||
end_filter = (datetime.utcnow() + timedelta(days=7)).isoformat()
|
||||
|
||||
response = client.get(
|
||||
"/api/calendar/events",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
params={"start": start_filter, "end": end_filter}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert len(data) == 2 # Should get event 1 and 2
|
||||
assert data[0]["title"] == payload1["title"]
|
||||
assert data[1]["title"] == payload2["title"]
|
||||
|
||||
response = client.get("/api/calendar/events",
|
||||
# Filter for events starting after 8 days
|
||||
start_filter_late = (datetime.utcnow() + timedelta(days=8)).isoformat()
|
||||
response = client.get(
|
||||
"/api/calendar/events",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
params={"start": start_filter_late}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) > 0
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert len(data) == 1 # Should get event 3
|
||||
assert data[0]["title"] == payload3["title"]
|
||||
|
||||
|
||||
# --- Test Get Event By ID ---
|
||||
|
||||
def test_get_event_by_id_unauthorized(db: Session, client: TestClient) -> None:
|
||||
"""Test getting a specific event without authentication."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload)
|
||||
event_id = create_response.json()["id"]
|
||||
|
||||
response = client.get(f"/api/calendar/events/{event_id}")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_get_event_by_id_success(db: Session, client: TestClient) -> None:
|
||||
"""Test getting a specific event successfully."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload)
|
||||
event_id = create_response.json()["id"]
|
||||
|
||||
response = client.get(
|
||||
f"/api/calendar/events/{event_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == event_id
|
||||
assert data["title"] == payload["title"]
|
||||
assert data["user_id"] == user.id
|
||||
|
||||
def test_get_event_by_id_not_found(db: Session, client: TestClient) -> None:
|
||||
"""Test getting a non-existent event."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
non_existent_id = 99999
|
||||
|
||||
response = client.get(
|
||||
f"/api/calendar/events/{non_existent_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_get_event_by_id_forbidden(db: Session, client: TestClient) -> None:
|
||||
"""Test getting another user's event."""
|
||||
user1, password_user1 = generators.create_user(db)
|
||||
user2, password_user2 = generators.create_user(db)
|
||||
|
||||
# Log in as user1 and create an event
|
||||
login_rsp1 = generators.login(db, user1.username, password_user1)
|
||||
access_token1 = login_rsp1["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token1}"}, json=payload)
|
||||
event_id = create_response.json()["id"]
|
||||
|
||||
# Log in as user2 and try to get user1's event
|
||||
login_rsp2 = generators.login(db, user2.username, password_user2)
|
||||
access_token2 = login_rsp2["access_token"]
|
||||
|
||||
response = client.get(
|
||||
f"/api/calendar/events/{event_id}",
|
||||
headers={"Authorization": f"Bearer {access_token2}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND # Service layer returns 404 if user_id doesn't match
|
||||
|
||||
# --- Test Update Event ---
|
||||
|
||||
def test_update_event_unauthorized(db: Session, client: TestClient) -> None:
|
||||
"""Test updating an event without authentication."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload)
|
||||
event_id = create_response.json()["id"]
|
||||
update_payload = {"title": "Updated Title"}
|
||||
|
||||
response = client.patch(f"/api/calendar/events/{event_id}", json=update_payload)
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_update_event_success(db: Session, client: TestClient) -> None:
|
||||
"""Test updating an event successfully."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload)
|
||||
assert create_response.status_code == status.HTTP_201_CREATED # Ensure creation check uses 201
|
||||
event_id = create_response.json()["id"]
|
||||
|
||||
update_payload = {
|
||||
"title": "Updated Title",
|
||||
"description": "Updated description.",
|
||||
"all_day": not payload["all_day"] # Toggle all_day
|
||||
}
|
||||
|
||||
response = client.patch(
|
||||
f"/api/calendar/events/{event_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
json=update_payload
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == event_id
|
||||
assert data["title"] == update_payload["title"]
|
||||
assert data["description"] == update_payload["description"]
|
||||
assert data["all_day"] == update_payload["all_day"]
|
||||
assert data["start"] == payload["start"] # Check correct field name 'start'
|
||||
assert data["user_id"] == user.id
|
||||
|
||||
# Verify in DB
|
||||
event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first()
|
||||
assert event_in_db is not None
|
||||
assert event_in_db.title == update_payload["title"]
|
||||
assert event_in_db.description == update_payload["description"]
|
||||
assert event_in_db.all_day == update_payload["all_day"]
|
||||
|
||||
def test_update_event_not_found(db: Session, client: TestClient) -> None:
|
||||
"""Test updating a non-existent event."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
non_existent_id = 99999
|
||||
update_payload = {"title": "Updated Title"}
|
||||
|
||||
response = client.patch(
|
||||
f"/api/calendar/events/{non_existent_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
json=update_payload
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_update_event_forbidden(db: Session, client: TestClient) -> None:
|
||||
"""Test updating another user's event."""
|
||||
user1, password_user1 = generators.create_user(db)
|
||||
user2, password_user2 = generators.create_user(db)
|
||||
|
||||
# Log in as user1 and create an event
|
||||
login_rsp1 = generators.login(db, user1.username, password_user1)
|
||||
access_token1 = login_rsp1["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token1}"}, json=payload)
|
||||
event_id = create_response.json()["id"]
|
||||
|
||||
# Log in as user2 and try to update user1's event
|
||||
login_rsp2 = generators.login(db, user2.username, password_user2)
|
||||
access_token2 = login_rsp2["access_token"]
|
||||
update_payload = {"title": "Updated by User 2"}
|
||||
|
||||
response = client.patch(
|
||||
f"/api/calendar/events/{event_id}",
|
||||
headers={"Authorization": f"Bearer {access_token2}"},
|
||||
json=update_payload
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND # Service layer returns 404 if user_id doesn't match
|
||||
|
||||
# --- Test Delete Event ---
|
||||
|
||||
def test_delete_event_unauthorized(db: Session, client: TestClient) -> None:
|
||||
"""Test deleting an event without authentication."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload)
|
||||
event_id = create_response.json()["id"]
|
||||
|
||||
response = client.delete(f"/api/calendar/events/{event_id}")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_delete_event_success(db: Session, client: TestClient) -> None:
|
||||
"""Test deleting an event successfully."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token}"}, json=payload)
|
||||
assert create_response.status_code == status.HTTP_201_CREATED # Ensure creation check uses 201
|
||||
event_id = create_response.json()["id"]
|
||||
|
||||
# Verify event exists before delete
|
||||
event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first()
|
||||
assert event_in_db is not None
|
||||
|
||||
response = client.delete(
|
||||
f"/api/calendar/events/{event_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
# Verify event is deleted from DB
|
||||
event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first()
|
||||
assert event_in_db is None
|
||||
|
||||
# Try getting the deleted event (should be 404)
|
||||
get_response = client.get(
|
||||
f"/api/calendar/events/{event_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
assert get_response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
def test_delete_event_not_found(db: Session, client: TestClient) -> None:
|
||||
"""Test deleting a non-existent event."""
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
access_token = login_rsp["access_token"]
|
||||
non_existent_id = 99999
|
||||
|
||||
response = client.delete(
|
||||
f"/api/calendar/events/{non_existent_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"}
|
||||
)
|
||||
# The service layer raises NotFound, which should result in 404
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
|
||||
def test_delete_event_forbidden(db: Session, client: TestClient) -> None:
|
||||
"""Test deleting another user's event."""
|
||||
user1, password_user1 = generators.create_user(db)
|
||||
user2, password_user2 = generators.create_user(db)
|
||||
|
||||
# Log in as user1 and create an event
|
||||
login_rsp1 = generators.login(db, user1.username, password_user1)
|
||||
access_token1 = login_rsp1["access_token"]
|
||||
payload = create_event_payload()
|
||||
create_response = client.post("/api/calendar/events", headers={"Authorization": f"Bearer {access_token1}"}, json=payload)
|
||||
event_id = create_response.json()["id"]
|
||||
|
||||
# Log in as user2 and try to delete user1's event
|
||||
login_rsp2 = generators.login(db, user2.username, password_user2)
|
||||
access_token2 = login_rsp2["access_token"]
|
||||
|
||||
response = client.delete(
|
||||
f"/api/calendar/events/{event_id}",
|
||||
headers={"Authorization": f"Bearer {access_token2}"}
|
||||
)
|
||||
# The service layer raises NotFound if user_id doesn't match, resulting in 404
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
# Verify event still exists for user1
|
||||
event_in_db = db.query(CalendarEvent).filter(CalendarEvent.id == event_id).first()
|
||||
assert event_in_db is not None
|
||||
assert event_in_db.user_id == user1.id
|
||||
|
||||
|
||||
10
backend/tests/test_main.py
Normal file
10
backend/tests/test_main.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
# No database needed for this simple test
|
||||
|
||||
def test_health_check(client: TestClient):
|
||||
"""Test the health check endpoint."""
|
||||
response = client.get("/api/health")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"status": "ok"}
|
||||
218
backend/tests/test_nlp.py
Normal file
218
backend/tests/test_nlp.py
Normal file
@@ -0,0 +1,218 @@
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
from unittest.mock import patch, MagicMock
|
||||
from datetime import datetime
|
||||
|
||||
from tests.helpers import generators
|
||||
from modules.nlp.schemas import ProcessCommandRequest, ProcessCommandResponse
|
||||
from modules.nlp.models import MessageSender, ChatMessage # Import necessary models/enums
|
||||
|
||||
# --- Mocks ---
|
||||
# Mock the external AI call and internal service functions
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_nlp_services():
|
||||
with patch("modules.nlp.api.process_request") as mock_process, \
|
||||
patch("modules.nlp.api.ask_ai") as mock_ask, \
|
||||
patch("modules.nlp.api.save_chat_message") as mock_save, \
|
||||
patch("modules.nlp.api.get_chat_history") as mock_get_history, \
|
||||
patch("modules.nlp.api.create_calendar_event") as mock_create_event, \
|
||||
patch("modules.nlp.api.get_calendar_events") as mock_get_events, \
|
||||
patch("modules.nlp.api.update_calendar_event") as mock_update_event, \
|
||||
patch("modules.nlp.api.delete_calendar_event") as mock_delete_event, \
|
||||
patch("modules.nlp.api.todo_service.create_todo") as mock_create_todo, \
|
||||
patch("modules.nlp.api.todo_service.get_todos") as mock_get_todos, \
|
||||
patch("modules.nlp.api.todo_service.update_todo") as mock_update_todo, \
|
||||
patch("modules.nlp.api.todo_service.delete_todo") as mock_delete_todo:
|
||||
mocks = {
|
||||
"process_request": mock_process,
|
||||
"ask_ai": mock_ask,
|
||||
"save_chat_message": mock_save,
|
||||
"get_chat_history": mock_get_history,
|
||||
"create_calendar_event": mock_create_event,
|
||||
"get_calendar_events": mock_get_events,
|
||||
"update_calendar_event": mock_update_event,
|
||||
"delete_calendar_event": mock_delete_event,
|
||||
"create_todo": mock_create_todo,
|
||||
"get_todos": mock_get_todos,
|
||||
"update_todo": mock_update_todo,
|
||||
"delete_todo": mock_delete_todo,
|
||||
}
|
||||
yield mocks
|
||||
|
||||
# --- Helper Function ---
|
||||
def _login_user(db: Session, client: TestClient):
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
return user, login_rsp["access_token"], login_rsp["refresh_token"]
|
||||
|
||||
# --- Tests for /process-command ---
|
||||
|
||||
def test_process_command_ask_ai(client: TestClient, db: Session, mock_nlp_services):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
user_input = "What is the capital of France?"
|
||||
mock_nlp_services["process_request"].return_value = {
|
||||
"intent": "ask_ai",
|
||||
"params": {"request": user_input},
|
||||
"response_text": "Let me check that for you."
|
||||
}
|
||||
mock_nlp_services["ask_ai"].return_value = "The capital of France is Paris."
|
||||
|
||||
response = client.post(
|
||||
"/api/nlp/process-command",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json={"user_input": user_input}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == ProcessCommandResponse(responses=["Let me check that for you.", "The capital of France is Paris."]).model_dump()
|
||||
# Verify save calls: user message, initial AI response, final AI answer
|
||||
assert mock_nlp_services["save_chat_message"].call_count == 3
|
||||
mock_nlp_services["save_chat_message"].assert_any_call(db, user_id=user.id, sender=MessageSender.USER, text=user_input)
|
||||
mock_nlp_services["save_chat_message"].assert_any_call(db, user_id=user.id, sender=MessageSender.AI, text="Let me check that for you.")
|
||||
mock_nlp_services["save_chat_message"].assert_any_call(db, user_id=user.id, sender=MessageSender.AI, text="The capital of France is Paris.")
|
||||
mock_nlp_services["ask_ai"].assert_called_once_with(request=user_input)
|
||||
|
||||
def test_process_command_get_calendar(client: TestClient, db: Session, mock_nlp_services):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
user_input = "What are my events today?"
|
||||
mock_nlp_services["process_request"].return_value = {
|
||||
"intent": "get_calendar_events",
|
||||
"params": {"start": "2024-01-01T00:00:00Z", "end": "2024-01-01T23:59:59Z"}, # Example params
|
||||
"response_text": "Okay, fetching your events."
|
||||
}
|
||||
# Mock the actual event model returned by the service
|
||||
mock_event = MagicMock()
|
||||
mock_event.title = "Team Meeting"
|
||||
mock_event.start = datetime(2024, 1, 1, 10, 0, 0)
|
||||
mock_event.end = datetime(2024, 1, 1, 11, 0, 0)
|
||||
mock_nlp_services["get_calendar_events"].return_value = [mock_event]
|
||||
|
||||
response = client.post(
|
||||
"/api/nlp/process-command",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json={"user_input": user_input}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
expected_responses = [
|
||||
"Okay, fetching your events.",
|
||||
"Here are the events:",
|
||||
"- Team Meeting (2024-01-01 10:00 - 11:00)"
|
||||
]
|
||||
assert response.json() == ProcessCommandResponse(responses=expected_responses).model_dump()
|
||||
assert mock_nlp_services["save_chat_message"].call_count == 4 # User, Initial AI, Header, Event
|
||||
mock_nlp_services["get_calendar_events"].assert_called_once()
|
||||
|
||||
def test_process_command_add_todo(client: TestClient, db: Session, mock_nlp_services):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
user_input = "Add buy milk to my list"
|
||||
mock_nlp_services["process_request"].return_value = {
|
||||
"intent": "add_todo",
|
||||
"params": {"task": "buy milk"},
|
||||
"response_text": "Adding it now."
|
||||
}
|
||||
# Mock the actual Todo model returned by the service
|
||||
mock_todo = MagicMock()
|
||||
mock_todo.task = "buy milk"
|
||||
mock_todo.id = 1
|
||||
mock_nlp_services["create_todo"].return_value = mock_todo
|
||||
|
||||
response = client.post(
|
||||
"/api/nlp/process-command",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json={"user_input": user_input}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
expected_responses = ["Adding it now.", "Added TODO: 'buy milk' (ID: 1)."]
|
||||
assert response.json() == ProcessCommandResponse(responses=expected_responses).model_dump()
|
||||
assert mock_nlp_services["save_chat_message"].call_count == 3 # User, Initial AI, Confirmation AI
|
||||
mock_nlp_services["create_todo"].assert_called_once()
|
||||
|
||||
def test_process_command_clarification(client: TestClient, db: Session, mock_nlp_services):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
user_input = "Delete the event"
|
||||
clarification_text = "Which event do you mean? Please provide the ID."
|
||||
mock_nlp_services["process_request"].return_value = {
|
||||
"intent": "clarification_needed",
|
||||
"params": {"request": user_input},
|
||||
"response_text": clarification_text
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/nlp/process-command",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json={"user_input": user_input}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == ProcessCommandResponse(responses=[clarification_text]).model_dump()
|
||||
# Verify save calls: user message, clarification AI response
|
||||
assert mock_nlp_services["save_chat_message"].call_count == 2
|
||||
mock_nlp_services["save_chat_message"].assert_any_call(db, user_id=user.id, sender=MessageSender.USER, text=user_input)
|
||||
mock_nlp_services["save_chat_message"].assert_any_call(db, user_id=user.id, sender=MessageSender.AI, text=clarification_text)
|
||||
# Ensure no action services were called
|
||||
mock_nlp_services["delete_calendar_event"].assert_not_called()
|
||||
|
||||
def test_process_command_error_intent(client: TestClient, db: Session, mock_nlp_services):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
user_input = "Gibberish request"
|
||||
error_text = "Sorry, I didn't understand that."
|
||||
mock_nlp_services["process_request"].return_value = {
|
||||
"intent": "error",
|
||||
"params": {},
|
||||
"response_text": error_text
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/nlp/process-command",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json={"user_input": user_input}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == ProcessCommandResponse(responses=[error_text]).model_dump()
|
||||
# Verify save calls: user message, error AI response
|
||||
assert mock_nlp_services["save_chat_message"].call_count == 2
|
||||
mock_nlp_services["save_chat_message"].assert_any_call(db, user_id=user.id, sender=MessageSender.USER, text=user_input)
|
||||
mock_nlp_services["save_chat_message"].assert_any_call(db, user_id=user.id, sender=MessageSender.AI, text=error_text)
|
||||
|
||||
# --- Tests for /history ---
|
||||
|
||||
def test_get_history(client: TestClient, db: Session, mock_nlp_services):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
|
||||
# Mock the history data returned by the service
|
||||
mock_history = [
|
||||
ChatMessage(id=1, user_id=user.id, sender=MessageSender.USER, text="Hello", timestamp=datetime.now()),
|
||||
ChatMessage(id=2, user_id=user.id, sender=MessageSender.AI, text="Hi there!", timestamp=datetime.now())
|
||||
]
|
||||
mock_nlp_services["get_chat_history"].return_value = mock_history
|
||||
|
||||
response = client.get(
|
||||
"/api/nlp/history",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
# We need to compare JSON representations as datetime objects might differ slightly
|
||||
response_data = response.json()
|
||||
assert len(response_data) == 2
|
||||
assert response_data[0]["text"] == "Hello"
|
||||
assert response_data[1]["text"] == "Hi there!"
|
||||
mock_nlp_services["get_chat_history"].assert_called_once_with(db, user_id=user.id, limit=50)
|
||||
|
||||
def test_get_history_unauthorized(client: TestClient):
|
||||
response = client.get("/api/nlp/history")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
# Add more tests for other intents (update/delete calendar/todo, unknown intent, etc.)
|
||||
# Add tests for error handling within the API endpoint (e.g., missing IDs for update/delete)
|
||||
210
backend/tests/test_todo.py
Normal file
210
backend/tests/test_todo.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import date
|
||||
|
||||
from tests.helpers import generators
|
||||
from modules.todo import schemas # Import schemas
|
||||
|
||||
# Helper Function
|
||||
def _login_user(db: Session, client: TestClient):
|
||||
user, password = generators.create_user(db)
|
||||
login_rsp = generators.login(db, user.username, password)
|
||||
return user, login_rsp["access_token"], login_rsp["refresh_token"]
|
||||
|
||||
# --- Test CRUD Operations ---
|
||||
|
||||
def test_create_todo(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
today_date = date.today()
|
||||
# Format the date string to match the expected response format "YYYY-MM-DDTHH:MM:SS"
|
||||
todo_data = {
|
||||
"task": "Test TODO",
|
||||
"date": f"{today_date.isoformat()}T00:00:00",
|
||||
"remind": True
|
||||
}
|
||||
|
||||
response = client.post(
|
||||
"/api/todos/",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json=todo_data
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
data = response.json()
|
||||
assert data["task"] == todo_data["task"]
|
||||
assert data["date"] == todo_data["date"]
|
||||
assert data["remind"] == todo_data["remind"]
|
||||
assert data["complete"] is False # Default
|
||||
assert "id" in data
|
||||
assert data["owner_id"] == user.id
|
||||
|
||||
def test_read_todos(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
# Create some todos for the user
|
||||
client.post("/api/todos/", headers={"Authorization": f"Bearer {access_token}"}, cookies={"refresh_token": refresh_token}, json={"task": "Todo 1"})
|
||||
client.post("/api/todos/", headers={"Authorization": f"Bearer {access_token}"}, cookies={"refresh_token": refresh_token}, json={"task": "Todo 2"})
|
||||
|
||||
# Create a todo for another user
|
||||
other_user, other_password = generators.create_user(db)
|
||||
other_login_rsp = generators.login(db, other_user.username, other_password)
|
||||
other_access_token = other_login_rsp["access_token"]
|
||||
other_refresh_token = other_login_rsp["refresh_token"]
|
||||
client.post("/api/todos/", headers={"Authorization": f"Bearer {other_access_token}"}, cookies={"refresh_token": other_refresh_token}, json={"task": "Other User Todo"})
|
||||
|
||||
|
||||
response = client.get(
|
||||
"/api/todos/",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert len(data) == 2 # Should only get todos for the logged-in user
|
||||
assert data[0]["task"] == "Todo 1"
|
||||
assert data[1]["task"] == "Todo 2"
|
||||
|
||||
def test_read_single_todo(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
create_response = client.post(
|
||||
"/api/todos/",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json={"task": "Specific Todo"}
|
||||
)
|
||||
todo_id = create_response.json()["id"]
|
||||
|
||||
response = client.get(
|
||||
f"/api/todos/{todo_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == todo_id
|
||||
assert data["task"] == "Specific Todo"
|
||||
assert data["owner_id"] == user.id
|
||||
|
||||
def test_read_single_todo_not_found(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
response = client.get(
|
||||
"/api/todos/9999", # Non-existent ID
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_read_single_todo_forbidden(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
|
||||
# Create a todo for another user
|
||||
other_user, other_password = generators.create_user(db)
|
||||
other_login_rsp = generators.login(db, other_user.username, other_password)
|
||||
other_access_token = other_login_rsp["access_token"]
|
||||
other_refresh_token = other_login_rsp["refresh_token"]
|
||||
other_create_response = client.post("/api/todos/", headers={"Authorization": f"Bearer {other_access_token}"}, cookies={"refresh_token": other_refresh_token}, json={"task": "Other User Todo"})
|
||||
other_todo_id = other_create_response.json()["id"]
|
||||
|
||||
# Try to access the other user's todo
|
||||
response = client.get(
|
||||
f"/api/todos/{other_todo_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"}, # Using the first user's token
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND # Service raises 404 if not found for *this* user
|
||||
|
||||
def test_update_todo(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
create_response = client.post(
|
||||
"/api/todos/",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json={"task": "Update Me"}
|
||||
)
|
||||
todo_id = create_response.json()["id"]
|
||||
|
||||
update_data = {"task": "Updated Task", "complete": True}
|
||||
response = client.put(
|
||||
f"/api/todos/{todo_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json=update_data
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
data = response.json()
|
||||
assert data["id"] == todo_id
|
||||
assert data["task"] == update_data["task"]
|
||||
assert data["complete"] == update_data["complete"]
|
||||
assert data["owner_id"] == user.id
|
||||
|
||||
# Verify update by reading again
|
||||
get_response = client.get(
|
||||
f"/api/todos/{todo_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
assert get_response.json()["task"] == update_data["task"]
|
||||
assert get_response.json()["complete"] == update_data["complete"]
|
||||
|
||||
|
||||
def test_update_todo_not_found(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
update_data = {"task": "Updated Task", "complete": True}
|
||||
response = client.put(
|
||||
"/api/todos/9999", # Non-existent ID
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json=update_data
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_delete_todo(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
create_response = client.post(
|
||||
"/api/todos/",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token},
|
||||
json={"task": "Delete Me"}
|
||||
)
|
||||
todo_id = create_response.json()["id"]
|
||||
|
||||
response = client.delete(
|
||||
f"/api/todos/{todo_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK # Delete returns the deleted item
|
||||
assert response.json()["id"] == todo_id
|
||||
|
||||
# Verify deletion by trying to read
|
||||
get_response = client.get(
|
||||
f"/api/todos/{todo_id}",
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
assert get_response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
def test_delete_todo_not_found(client: TestClient, db: Session):
|
||||
user, access_token, refresh_token = _login_user(db, client)
|
||||
response = client.delete(
|
||||
"/api/todos/9999", # Non-existent ID
|
||||
headers={"Authorization": f"Bearer {access_token}"},
|
||||
cookies={"refresh_token": refresh_token}
|
||||
)
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
|
||||
# --- Test Authentication/Authorization ---
|
||||
|
||||
def test_create_todo_unauthorized(client: TestClient):
|
||||
response = client.post("/api/todos/", json={"task": "No Auth"})
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
|
||||
def test_read_todos_unauthorized(client: TestClient):
|
||||
response = client.get("/api/todos/")
|
||||
assert response.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
Reference in New Issue
Block a user