m-ahmad-official commited on
Commit
6bed18e
·
1 Parent(s): b4a1f32

Initial commit: Full-stack todo backend for Hugging Face Spaces

Browse files
Files changed (48) hide show
  1. .env.example +7 -0
  2. .gitignore +32 -0
  3. Dockerfile +18 -0
  4. alembic.ini +40 -0
  5. alembic/env.py +52 -0
  6. backend/check_tables.py +33 -0
  7. backend/verify_neon_db.py +77 -0
  8. check_neon_tables.py +70 -0
  9. init_db.py +25 -0
  10. pyproject.toml +12 -0
  11. requirements.txt +17 -0
  12. run_quickstart_validation.py +240 -0
  13. src/__init__.py +0 -0
  14. src/api/__init__.py +0 -0
  15. src/api/v1/__init__.py +0 -0
  16. src/api/v1/auth.py +161 -0
  17. src/api/v1/tasks.py +227 -0
  18. src/auth/__init__.py +0 -0
  19. src/auth/deps.py +100 -0
  20. src/auth/middleware.py +101 -0
  21. src/auth/security.py +235 -0
  22. src/auth/user_service.py +56 -0
  23. src/core/__init__.py +0 -0
  24. src/core/config.py +20 -0
  25. src/core/database.py +22 -0
  26. src/core/logging.py +100 -0
  27. src/main.py +41 -0
  28. src/models/__init__.py +3 -0
  29. src/models/task.py +59 -0
  30. src/models/user.py +35 -0
  31. src/services/task_service.py +230 -0
  32. src/utils/__init__.py +0 -0
  33. src/utils/code_cleanup.py +187 -0
  34. src/utils/performance.py +203 -0
  35. src/utils/validators.py +62 -0
  36. test_implementation.py +50 -0
  37. tests/contract/test_data_isolation.py +182 -0
  38. tests/contract/test_jwt_validation.py +80 -0
  39. tests/contract/test_token_expiry.py +171 -0
  40. tests/contract/test_unauthorized_access.py +144 -0
  41. tests/integration/test_401_responses.py +135 -0
  42. tests/integration/test_authenticated_access.py +129 -0
  43. tests/integration/test_cross_user_access.py +210 -0
  44. tests/integration/test_expired_tokens.py +191 -0
  45. tests/integration/test_responsive_design.py +210 -0
  46. tests/unit/test_auth/test_auth_functions.py +231 -0
  47. tests/unit/test_auth/test_authentication_functions.py +196 -0
  48. tests/unit/test_models/test_task.py +84 -0
.env.example ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ DATABASE_URL='postgresql://neondb_owner:npg_OobETvcr52mH@ep-purple-rain-ahogvd6j-pooler.c-3.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require'
2
+ SECRET_KEY=your-secret-key-here
3
+ DEBUG=True
4
+ BETTER_AUTH_SECRET=your-better-auth-secret-key
5
+ BETTER_AUTH_PUBLIC_KEY=your-public-key-for-verification
6
+ JWT_ALGORITHM=RS256
7
+ JWT_EXPIRATION_DELTA=604800 # 7 days in seconds
.gitignore ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ env/
7
+ venv/
8
+ ENV/
9
+ .venv/
10
+ .env
11
+ *.env
12
+ *.swp
13
+ .DS_Store
14
+ Thumbs.db
15
+ .vscode/
16
+ .idea/
17
+ *.log
18
+ *.sqlite
19
+ *.db
20
+ dist/
21
+ build/
22
+ *.egg-info/
23
+ .coverage
24
+ htmlcov/
25
+ .pytest_cache/
26
+ .hypothesis/
27
+ *.so
28
+ *.egg
29
+ .pytest_cache/
30
+ .tox/
31
+ nox/
32
+ site-packages/
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /src
4
+
5
+ # Copy requirements first for cache efficiency
6
+ COPY requirements.txt .
7
+
8
+ #Install dependencies
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # Copy the backend application code
12
+ COPY src ./src
13
+
14
+ # Expose the port expected by HF Spaces (7860) and uvicorn
15
+ EXPOSE 7860
16
+
17
+ # Run the application
18
+ CMD ["python", "-m", "src.main", "--host", "0.0.0.0", "--port", "7860"]
alembic.ini ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [alembic]
2
+ script_location = alembic
3
+ sqlalchemy.url = postgresql+asyncpg://username:password@localhost:5432/todo_db
4
+
5
+ [post_write_hooks]
6
+ hooks = black, isort
7
+
8
+ [loggers]
9
+ keys = root,sqlalchemy,alembic
10
+
11
+ [handlers]
12
+ keys = console
13
+
14
+ [formatters]
15
+ keys = generic
16
+
17
+ [logger_root]
18
+ level = WARN
19
+ handlers = console
20
+ qualname =
21
+
22
+ [logger_sqlalchemy]
23
+ level = WARN
24
+ handlers =
25
+ qualname = sqlalchemy.engine
26
+
27
+ [logger_alembic]
28
+ level = INFO
29
+ handlers =
30
+ qualname = alembic
31
+
32
+ [handler_console]
33
+ class = StreamHandler
34
+ args = (sys.stderr,)
35
+ level = NOTSET
36
+ formatter = generic
37
+
38
+ [formatter_generic]
39
+ format = %(levelname)-5.5s [%(name)s] %(message)s
40
+ datefmt = %H:%M:%S
alembic/env.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from logging.config import fileConfig
2
+ from sqlalchemy import engine_from_config
3
+ from sqlalchemy import pool
4
+ from alembic import context
5
+
6
+ # this is the Alembic Config object
7
+ config = context.config
8
+
9
+ # Interpret the config file for Python logging.
10
+ if config.config_file_name is not None:
11
+ fileConfig(config.config_file_name)
12
+
13
+ # add your model's MetaData object here for 'autogenerate' support
14
+ from src.models.task import Task # Import your models here
15
+ from sqlmodel import SQLModel
16
+ target_metadata = SQLModel.metadata
17
+
18
+ def run_migrations_offline() -> None:
19
+ """Run migrations in 'offline' mode."""
20
+ url = config.get_main_option("sqlalchemy.url")
21
+ context.configure(
22
+ url=url,
23
+ target_metadata=target_metadata,
24
+ literal_binds=True,
25
+ dialect_opts={"paramstyle": "named"},
26
+ )
27
+
28
+ with context.begin_transaction():
29
+ context.run_migrations()
30
+
31
+
32
+ def run_migrations_online() -> None:
33
+ """Run migrations in 'online' mode."""
34
+ connectable = engine_from_config(
35
+ config.get_section(config.config_ini_section),
36
+ prefix="sqlalchemy.",
37
+ poolclass=pool.NullPool,
38
+ )
39
+
40
+ with connectable.connect() as connection:
41
+ context.configure(
42
+ connection=connection, target_metadata=target_metadata
43
+ )
44
+
45
+ with context.begin_transaction():
46
+ context.run_migrations()
47
+
48
+
49
+ if context.is_offline_mode():
50
+ run_migrations_offline()
51
+ else:
52
+ run_migrations_online()
backend/check_tables.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Check if the tables were created in the database
3
+ """
4
+ from sqlalchemy import inspect
5
+ from src.core.database import engine
6
+
7
+
8
+ def check_tables():
9
+ """
10
+ Check what tables exist in the database
11
+ """
12
+ print("Checking database tables...")
13
+
14
+ # Create an inspector
15
+ inspector = inspect(engine)
16
+
17
+ # Get table names
18
+ table_names = inspector.get_table_names()
19
+
20
+ print(f"Tables found in database: {table_names}")
21
+
22
+ if table_names:
23
+ for table_name in table_names:
24
+ print(f"\nColumns in '{table_name}' table:")
25
+ columns = inspector.get_columns(table_name)
26
+ for col in columns:
27
+ print(f" - {col['name']} ({col['type']}) {col['nullable'] and 'NULL' or 'NOT NULL'}")
28
+ else:
29
+ print("❌ No tables found in the database")
30
+
31
+
32
+ if __name__ == "__main__":
33
+ check_tables()
backend/verify_neon_db.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Verify that the Neon database tables are properly set up
4
+ """
5
+ import os
6
+ from sqlalchemy import create_engine, inspect, text
7
+ from sqlalchemy.exc import OperationalError
8
+
9
+
10
+ def verify_neon_database():
11
+ """
12
+ Verify the Neon database connection and table structure
13
+ """
14
+ # Get the database URL from environment or use the default
15
+ database_url = os.getenv("DATABASE_URL", "postgresql://neondb_owner:npg_OobETvcr52mH@ep-purple-rain-ahogvd6j-pooler.c-3.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require")
16
+
17
+ print(f"Connecting to database: {database_url.replace('@', '[AT]').replace(':', '[COLON]')}") # Mask credentials
18
+
19
+ try:
20
+ # Create engine
21
+ engine = create_engine(database_url)
22
+
23
+ # Test connection
24
+ with engine.connect() as conn:
25
+ result = conn.execute(text("SELECT version();"))
26
+ version = result.fetchone()
27
+ print(f"✅ Successfully connected to database. Version: {version[0][:50]}...")
28
+
29
+ # Create an inspector
30
+ inspector = inspect(engine)
31
+
32
+ # Get table names
33
+ table_names = inspector.get_table_names()
34
+ print(f"📊 Tables in database: {table_names}")
35
+
36
+ if 'task' in table_names:
37
+ print("\n📋 Task table structure:")
38
+
39
+ # Get column information for the task table
40
+ columns = inspector.get_columns('task')
41
+ for col in columns:
42
+ nullable_text = "NULL" if col['nullable'] else "NOT NULL"
43
+ print(f" • {col['name']:<15} {str(col['type']):<25} {nullable_text}")
44
+
45
+ print("\n✅ Task table exists with correct structure!")
46
+ print("🎉 Your Neon database is properly set up for the Todo API!")
47
+ else:
48
+ print("\n❌ Task table not found in database")
49
+ print("Attempting to create tables...")
50
+
51
+ # Import SQLModel and create tables
52
+ import sys
53
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
54
+
55
+ from sqlmodel import SQLModel
56
+ from backend.src.models.task import Task
57
+
58
+ # Create all tables
59
+ SQLModel.metadata.create_all(engine)
60
+ print("✅ Tables created successfully")
61
+
62
+ # Check again
63
+ table_names = inspector.get_table_names()
64
+ print(f"📊 Tables after creation: {table_names}")
65
+
66
+ except OperationalError as e:
67
+ print(f"❌ Database connection error: {e}")
68
+ print("\n💡 This might be due to:")
69
+ print(" - Incorrect database credentials in .env")
70
+ print(" - Network connectivity issues")
71
+ print(" - Database not properly configured in Neon")
72
+ except Exception as e:
73
+ print(f"❌ Unexpected error: {e}")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ verify_neon_database()
check_neon_tables.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Check if the tables were created in the Neon database
3
+ """
4
+ import os
5
+ import sys
6
+
7
+ # Add the current directory and backend directory to the Python path
8
+ sys.path.insert(0, os.path.dirname(__file__))
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
10
+
11
+ from sqlalchemy import create_engine, inspect
12
+ from sqlmodel import SQLModel
13
+
14
+ # Import after setting up the path
15
+ from backend.src.models.task import Task
16
+
17
+
18
+ def check_tables():
19
+ """
20
+ Check what tables exist in the database
21
+ """
22
+ print("Checking database tables...")
23
+
24
+ # Get the database URL from environment or use the default
25
+ database_url = os.getenv("DATABASE_URL", "postgresql://neondb_owner:npg_OobETvcr52mH@ep-purple-rain-ahogvd6j-pooler.c-3.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require")
26
+
27
+ print(f"Connecting to database: {database_url}")
28
+
29
+ # Create engine
30
+ engine = create_engine(database_url)
31
+
32
+ # Create an inspector
33
+ inspector = inspect(engine)
34
+
35
+ # Get table names
36
+ table_names = inspector.get_table_names()
37
+
38
+ print(f"Tables found in database: {table_names}")
39
+
40
+ if table_names:
41
+ for table_name in table_names:
42
+ print(f"\nColumns in '{table_name}' table:")
43
+ columns = inspector.get_columns(table_name)
44
+ for col in columns:
45
+ print(f" - {col['name']} ({col['type']}) {col['nullable'] and 'NULL' or 'NOT NULL'}")
46
+ else:
47
+ print("❌ No tables found in the database")
48
+
49
+ # Try to create the tables directly using SQLModel metadata
50
+ print("\nTrying to create tables using SQLModel...")
51
+ try:
52
+ SQLModel.metadata.create_all(engine)
53
+ print("✅ Attempted to create tables via SQLModel")
54
+
55
+ # Check again after attempting to create
56
+ table_names = inspector.get_table_names()
57
+ print(f"Tables found after creation attempt: {table_names}")
58
+
59
+ if table_names:
60
+ print("\n✅ Success! Tables have been created in your Neon database.")
61
+ print("You can now use the Todo API to perform CRUD operations on tasks.")
62
+ else:
63
+ print("\n⚠️ Tables may not have been created. Please check your Neon database connection.")
64
+
65
+ except Exception as e:
66
+ print(f"❌ Error creating tables: {e}")
67
+
68
+
69
+ if __name__ == "__main__":
70
+ check_tables()
init_db.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Initialize the database tables for the Todo API
3
+ """
4
+ from sqlmodel import SQLModel
5
+ from src.core.database import engine
6
+ from src.models.task import Task
7
+ from src.models.user import User
8
+
9
+
10
+ def create_tables():
11
+ """
12
+ Create all database tables
13
+ """
14
+ print("Creating database tables...")
15
+
16
+ # Create all tables defined in SQLModel metadata
17
+ SQLModel.metadata.create_all(engine)
18
+
19
+ print("✅ Database tables created successfully!")
20
+ print("- Task table created")
21
+ print("- User table created")
22
+
23
+
24
+ if __name__ == "__main__":
25
+ create_tables()
pyproject.toml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.black]
2
+ line-length = 88
3
+ target-version = ['py311']
4
+ include = '\.pyi?$'
5
+
6
+ [tool.isort]
7
+ profile = "black"
8
+ multi_line_output = 3
9
+
10
+ [tool.flake8]
11
+ max-line-length = 88
12
+ extend-ignore = ['E203', 'W503']
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ sqlmodel
3
+ pydantic
4
+ pydantic-settings
5
+ uvicorn
6
+ asyncpg
7
+ psycopg2-binary
8
+ alembic
9
+ pytest
10
+ httpx
11
+ python-dotenv
12
+ PyJWT
13
+ python-jose[cryptography]
14
+ cryptography
15
+ passlib
16
+ bcrypt
17
+ requests
run_quickstart_validation.py ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Quickstart validation script for the Todo API backend
4
+ This script validates that all core functionality works as expected
5
+ """
6
+
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ import requests
11
+ import json
12
+ from datetime import datetime, timedelta
13
+ from jose import jwt
14
+ import sys
15
+ import os
16
+ sys.path.append(os.path.join(os.path.dirname(__file__)))
17
+
18
+ from src.core.config import settings
19
+
20
+
21
+ def validate_project_structure():
22
+ """Validate that all required project files and directories exist"""
23
+ print("🔍 Validating project structure...")
24
+
25
+ required_files = [
26
+ "src/main.py",
27
+ "src/models/task.py",
28
+ "src/services/task_service.py",
29
+ "src/api/v1/tasks.py",
30
+ "src/auth/security.py",
31
+ "src/auth/deps.py",
32
+ "src/core/config.py",
33
+ "src/core/database.py",
34
+ "requirements.txt"
35
+ ]
36
+
37
+ missing_files = []
38
+ for file in required_files:
39
+ full_path = f"./{file}"
40
+ try:
41
+ with open(full_path, 'r'):
42
+ pass
43
+ except FileNotFoundError:
44
+ missing_files.append(full_path)
45
+
46
+ if missing_files:
47
+ print(f"❌ Missing required files: {missing_files}")
48
+ return False
49
+
50
+ print("✅ All required files exist")
51
+ return True
52
+
53
+
54
+ def validate_dependencies():
55
+ """Validate that all required dependencies are available"""
56
+ print("🔍 Validating dependencies...")
57
+
58
+ try:
59
+ import fastapi
60
+ import sqlmodel
61
+ import jose
62
+ import pydantic
63
+ print("✅ All required dependencies are available")
64
+ return True
65
+ except ImportError as e:
66
+ print(f"❌ Missing dependency: {e}")
67
+ return False
68
+
69
+
70
+ def validate_config():
71
+ """Validate that configuration is properly set up"""
72
+ print("🔍 Validating configuration...")
73
+
74
+ try:
75
+ # Check that settings object has required attributes
76
+ assert hasattr(settings, 'DATABASE_URL'), "DATABASE_URL not configured"
77
+ assert hasattr(settings, 'SECRET_KEY'), "SECRET_KEY not configured"
78
+ assert hasattr(settings, 'JWT_ALGORITHM'), "JWT_ALGORITHM not configured"
79
+ assert hasattr(settings, 'JWT_EXPIRATION_DELTA'), "JWT_EXPIRATION_DELTA not configured"
80
+
81
+ print("✅ Configuration is properly set up")
82
+ return True
83
+ except AssertionError as e:
84
+ print(f"❌ Configuration error: {e}")
85
+ return False
86
+
87
+
88
+ def validate_token_functionality():
89
+ """Validate JWT token creation and verification functionality"""
90
+ print("🔍 Validating token functionality...")
91
+
92
+ try:
93
+ # Create a test token
94
+ test_data = {"user_id": "test_user_123", "role": "user"}
95
+ from src.auth.security import create_access_token, verify_token
96
+
97
+ token = create_access_token(data=test_data)
98
+ assert token is not None, "Token creation failed"
99
+ assert isinstance(token, str), "Token should be a string"
100
+ assert len(token) > 0, "Token should not be empty"
101
+
102
+ # Verify the token
103
+ payload = verify_token(token)
104
+ assert payload is not None, "Token verification failed"
105
+ assert payload["user_id"] == "test_user_123", "User ID mismatch in payload"
106
+ assert payload["role"] == "user", "Role mismatch in payload"
107
+ assert "exp" in payload, "Expiration not in payload"
108
+
109
+ print("✅ Token functionality works correctly")
110
+ return True
111
+ except Exception as e:
112
+ print(f"❌ Token functionality error: {e}")
113
+ return False
114
+
115
+
116
+ def validate_models():
117
+ """Validate that the data models are properly defined"""
118
+ print("🔍 Validating data models...")
119
+
120
+ try:
121
+ from src.models.task import Task, TaskCreate, TaskUpdate, TaskResponse
122
+
123
+ # Test creating a task model instance
124
+ task_create = TaskCreate(
125
+ title="Test task",
126
+ description="Test description",
127
+ user_id="test_user_123"
128
+ )
129
+
130
+ assert task_create.title == "Test task"
131
+ assert task_create.user_id == "test_user_123"
132
+
133
+ print("✅ Data models are properly defined")
134
+ return True
135
+ except Exception as e:
136
+ print(f"❌ Data model error: {e}")
137
+ return False
138
+
139
+
140
+ def validate_services():
141
+ """Validate that the service layer is properly implemented"""
142
+ print("🔍 Validating service layer...")
143
+
144
+ try:
145
+ from src.services.task_service import TaskService
146
+
147
+ # Just check that the service class exists and has required methods
148
+ assert hasattr(TaskService, 'create_task'), "create_task method missing"
149
+ assert hasattr(TaskService, 'get_tasks_by_user_id'), "get_tasks_by_user_id method missing"
150
+ assert hasattr(TaskService, 'update_task'), "update_task method missing"
151
+ assert hasattr(TaskService, 'delete_task'), "delete_task method missing"
152
+ assert hasattr(TaskService, 'toggle_task_completion'), "toggle_task_completion method missing"
153
+
154
+ print("✅ Service layer is properly implemented")
155
+ return True
156
+ except Exception as e:
157
+ print(f"❌ Service layer error: {e}")
158
+ return False
159
+
160
+
161
+ def validate_api_endpoints():
162
+ """Validate that API endpoints are properly defined"""
163
+ print("🔍 Validating API endpoints...")
164
+
165
+ try:
166
+ from src.api.v1.tasks import router
167
+
168
+ # Check that the router is properly defined
169
+ assert router is not None, "API router not defined"
170
+
171
+ print("✅ API endpoints are properly defined")
172
+ return True
173
+ except Exception as e:
174
+ print(f"❌ API endpoint error: {e}")
175
+ return False
176
+
177
+
178
+ def validate_logging():
179
+ """Validate that logging functionality works"""
180
+ print("🔍 Validating logging functionality...")
181
+
182
+ try:
183
+ from src.core.logging import log_operation, log_error, log_authentication_event, log_authorization_decision, log_token_validation_result
184
+
185
+ # Test logging functions
186
+ log_operation("QUICKSTART_TEST_OPERATION", user_id="test_user")
187
+ log_authentication_event("QUICKSTART_TEST", user_id="test_user")
188
+ log_authorization_decision("read", "test_user", "task_123", True)
189
+ log_token_validation_result("QUICKSTART_VALID", user_id="test_user")
190
+
191
+ print("✅ Logging functionality works")
192
+ return True
193
+ except Exception as e:
194
+ print(f"❌ Logging error: {e}")
195
+ return False
196
+
197
+
198
+ def run_complete_validation():
199
+ """Run all validation checks"""
200
+ print("🚀 Starting quickstart validation for Todo API Backend...\n")
201
+
202
+ all_checks = [
203
+ ("Project Structure", validate_project_structure),
204
+ ("Dependencies", validate_dependencies),
205
+ ("Configuration", validate_config),
206
+ ("Token Functionality", validate_token_functionality),
207
+ ("Data Models", validate_models),
208
+ ("Service Layer", validate_services),
209
+ ("API Endpoints", validate_api_endpoints),
210
+ ("Logging", validate_logging)
211
+ ]
212
+
213
+ results = []
214
+ for check_name, check_func in all_checks:
215
+ print(f"\n📋 {check_name} check:")
216
+ result = check_func()
217
+ results.append((check_name, result))
218
+
219
+ print(f"\n🏁 Validation Summary:")
220
+ total_checks = len(results)
221
+ passed_checks = sum(1 for _, result in results if result)
222
+ failed_checks = total_checks - passed_checks
223
+
224
+ for check_name, result in results:
225
+ status = "✅ PASS" if result else "❌ FAIL"
226
+ print(f" {status}: {check_name}")
227
+
228
+ print(f"\n📊 Total: {total_checks}, Passed: {passed_checks}, Failed: {failed_checks}")
229
+
230
+ if failed_checks == 0:
231
+ print("\n🎉 All validation checks passed! The Todo API backend is ready for use.")
232
+ return True
233
+ else:
234
+ print(f"\n⚠️ {failed_checks} validation checks failed. Please review the issues above.")
235
+ return False
236
+
237
+
238
+ if __name__ == "__main__":
239
+ success = run_complete_validation()
240
+ sys.exit(0 if success else 1)
src/__init__.py ADDED
File without changes
src/api/__init__.py ADDED
File without changes
src/api/v1/__init__.py ADDED
File without changes
src/api/v1/auth.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status, Form
2
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
3
+ from sqlmodel import Session
4
+ from src.core.database import get_session
5
+ from src.auth.security import verify_token, create_access_token
6
+ from src.auth.deps import is_token_expired
7
+ from src.core.config import settings
8
+ from src.auth.user_service import authenticate_user, create_user
9
+ from src.models.user import UserCreate
10
+ from fastapi.responses import JSONResponse
11
+
12
+ router = APIRouter()
13
+ security = HTTPBearer()
14
+
15
+
16
+ @router.post("/token/refresh", summary="Refresh expired JWT token")
17
+ async def refresh_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
18
+ """
19
+ Refresh an expired JWT token by generating a new one based on the user's identity.
20
+ This endpoint allows clients to renew their access tokens without re-authenticating.
21
+ """
22
+ token = credentials.credentials
23
+
24
+ # Verify the token (this will succeed for expired tokens if we just want to extract user data)
25
+ # Note: In a real implementation, you'd have a separate refresh token mechanism
26
+ payload = verify_token(token)
27
+
28
+ if payload is None:
29
+ raise HTTPException(
30
+ status_code=status.HTTP_401_UNAUTHORIZED,
31
+ detail="Invalid token",
32
+ headers={"WWW-Authenticate": "Bearer"},
33
+ )
34
+
35
+ # Check if the token is expired
36
+ if not is_token_expired(payload):
37
+ # If the token is not expired, we might want to reject the refresh request
38
+ # Or we could allow refreshing slightly before expiry
39
+ pass # For now, allow refresh regardless of current expiry status
40
+
41
+ # Create a new token with the same user data
42
+ user_data = {key: value for key, value in payload.items() if key != "exp"}
43
+ new_token = create_access_token(data=user_data)
44
+
45
+ return {
46
+ "access_token": new_token,
47
+ "token_type": "bearer",
48
+ "expires_in": int(settings.JWT_EXPIRATION_DELTA),
49
+ }
50
+
51
+
52
+ @router.get("/token/validate", summary="Validate JWT token")
53
+ async def validate_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
54
+ """
55
+ Validate a JWT token without using it for any specific operation.
56
+ Returns user information if token is valid.
57
+ """
58
+ token = credentials.credentials
59
+
60
+ payload = verify_token(token)
61
+ if payload is None:
62
+ raise HTTPException(
63
+ status_code=status.HTTP_401_UNAUTHORIZED,
64
+ detail="Invalid or expired token",
65
+ headers={"WWW-Authenticate": "Bearer"},
66
+ )
67
+
68
+ # Check if token is expired
69
+ exp_time = payload.get("exp")
70
+ if exp_time:
71
+ import time
72
+
73
+ current_time = time.time()
74
+ if current_time >= exp_time:
75
+ raise HTTPException(
76
+ status_code=status.HTTP_401_UNAUTHORIZED,
77
+ detail="Token has expired",
78
+ headers={"WWW-Authenticate": "Bearer"},
79
+ )
80
+
81
+ return {
82
+ "valid": True,
83
+ "user_id": payload.get("user_id"),
84
+ "role": payload.get("role", "user"),
85
+ "exp": payload.get("exp"),
86
+ }
87
+
88
+
89
+ @router.post("/token/revoke", summary="Revoke JWT token (placeholder)")
90
+ async def revoke_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
91
+ """
92
+ Revoke a JWT token (this is a placeholder implementation).
93
+ In a real system, this would add the token to a blacklist/jti registry.
94
+ """
95
+ # In a real implementation, you would add the token to a blacklist
96
+ # For now, we just return a success message
97
+ return {
98
+ "revoked": True,
99
+ "message": "Token revoked successfully (in a real implementation, this would be added to a blacklist)",
100
+ }
101
+
102
+
103
+ @router.post("/login", summary="Authenticate user and return JWT token")
104
+ async def login(
105
+ email: str = Form(...),
106
+ password: str = Form(...),
107
+ session: Session = Depends(get_session),
108
+ ):
109
+ """
110
+ Authenticate a user with email and password.
111
+ Returns a JWT token upon successful authentication.
112
+ """
113
+ user = authenticate_user(session, email, password)
114
+
115
+ if not user:
116
+ raise HTTPException(
117
+ status_code=status.HTTP_401_UNAUTHORIZED,
118
+ detail="Incorrect email or password",
119
+ headers={"WWW-Authenticate": "Bearer"},
120
+ )
121
+
122
+ # Create access token - ensure user_id is a string for JWT
123
+ access_token = create_access_token(
124
+ data={"user_id": str(user.id), "role": getattr(user, "role", "user")}
125
+ )
126
+
127
+ return {
128
+ "access_token": access_token,
129
+ "token_type": "bearer",
130
+ "user": {"id": user.id, "email": user.email, "name": user.name},
131
+ }
132
+
133
+
134
+ @router.post("/register", summary="Register a new user")
135
+ async def register(
136
+ email: str = Form(...),
137
+ password: str = Form(...),
138
+ name: str = Form(...),
139
+ session: Session = Depends(get_session),
140
+ ):
141
+ try:
142
+ user_create = UserCreate(email=email, password=password, name=name)
143
+ user = create_user(session, user_create)
144
+
145
+ access_token = create_access_token(
146
+ data={"user_id": str(user.id), "role": getattr(user, "role", "user")}
147
+ )
148
+
149
+ return {
150
+ "access_token": access_token,
151
+ "token_type": "bearer",
152
+ "user": {"id": user.id, "email": user.email, "name": user.name},
153
+ }
154
+ except HTTPException:
155
+ # Re-raise HTTP exceptions (like 409 for duplicate email)
156
+ raise
157
+ except Exception as e:
158
+ raise HTTPException(
159
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
160
+ detail=f"Registration failed: {str(e)}",
161
+ )
src/api/v1/tasks.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List
2
+ from fastapi import APIRouter, Depends, HTTPException, status
3
+ from sqlmodel import Session
4
+ from src.services.task_service import TaskService
5
+ from src.models.task import Task, TaskCreate, TaskUpdate, TaskResponse
6
+ from src.core.database import get_session
7
+ from src.core.logging import log_operation
8
+ from src.auth.deps import get_current_user_id
9
+ from src.auth.security import authorize_user_for_task
10
+
11
+ router = APIRouter()
12
+
13
+
14
+ @router.post("/", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
15
+ def create_task(
16
+ task_create: TaskCreate,
17
+ current_user_id: str = Depends(get_current_user_id),
18
+ session: Session = Depends(get_session)
19
+ ) -> TaskResponse:
20
+ """
21
+ Create a new task for the authenticated user
22
+ """
23
+ try:
24
+ from src.utils.validators import validate_task_create
25
+
26
+ # Override user_id with authenticated user's ID to ensure security
27
+ task_create.user_id = current_user_id
28
+
29
+ # Now validate the task with the proper user_id
30
+ validate_task_create(task_create)
31
+
32
+ # Create the task
33
+ db_task = TaskService.create_task(session, task_create)
34
+
35
+ log_operation("TASK_CREATED_SUCCESSFULLY", user_id=current_user_id, task_id=db_task.id)
36
+ return TaskResponse.model_validate(db_task)
37
+ except HTTPException:
38
+ raise
39
+ except Exception as e:
40
+ log_operation("CREATE_TASK_ERROR", user_id=current_user_id)
41
+ raise HTTPException(
42
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
43
+ detail=f"An error occurred while creating the task: {str(e)}"
44
+ )
45
+
46
+
47
+ @router.get("/user/{user_id}", response_model=List[TaskResponse])
48
+ def get_tasks_for_user(
49
+ user_id: str,
50
+ current_user_id: str = Depends(get_current_user_id),
51
+ session: Session = Depends(get_session)
52
+ ) -> List[TaskResponse]:
53
+ """
54
+ Get all tasks for the authenticated user
55
+ """
56
+ try:
57
+ # Verify that the requested user_id matches the authenticated user's ID
58
+ if user_id != current_user_id:
59
+ log_operation(f"UNAUTHORIZED_ACCESS_ATTEMPT_tasks_for_user_{user_id}", user_id=current_user_id)
60
+ raise HTTPException(
61
+ status_code=status.HTTP_403_FORBIDDEN,
62
+ detail="You can only access your own tasks"
63
+ )
64
+
65
+ # Validate that user_id is provided
66
+ if not user_id or len(user_id.strip()) == 0:
67
+ raise HTTPException(
68
+ status_code=status.HTTP_400_BAD_REQUEST,
69
+ detail="user_id is required"
70
+ )
71
+
72
+ # Get tasks for the user
73
+ tasks = TaskService.get_tasks_by_user_id(session, user_id)
74
+
75
+ log_operation(f"GET_TASKS_SUCCESS ({len(tasks)} tasks)", user_id=user_id)
76
+
77
+ # Return as response models using SQLModel's serialization
78
+ return [TaskResponse.model_validate(task, from_attributes=True) for task in tasks]
79
+ except HTTPException:
80
+ raise
81
+ except Exception as e:
82
+ log_operation("GET_TASKS_ERROR", user_id=user_id)
83
+ raise HTTPException(
84
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
85
+ detail=f"An error occurred while retrieving tasks: {str(e)}"
86
+ )
87
+
88
+
89
+ @router.put("/{task_id}", response_model=TaskResponse)
90
+ def update_task(
91
+ task_id: int,
92
+ task_update: TaskUpdate,
93
+ current_user_id: str = Depends(get_current_user_id),
94
+ session: Session = Depends(get_session)
95
+ ) -> TaskResponse:
96
+ """
97
+ Update a task by ID (only if the task belongs to the authenticated user)
98
+ """
99
+ try:
100
+ from src.utils.validators import validate_task_update
101
+ validate_task_update(task_update)
102
+
103
+ # Get the task from the database
104
+ existing_task = TaskService.get_task_by_id(session, task_id)
105
+ if not existing_task:
106
+ raise HTTPException(
107
+ status_code=status.HTTP_404_NOT_FOUND,
108
+ detail=f"Task with id {task_id} not found"
109
+ )
110
+
111
+ # Verify that the authenticated user owns this task
112
+ if existing_task.user_id != current_user_id:
113
+ log_operation("UNAUTHORIZED_ACCESS_ATTEMPT", user_id=current_user_id, task_id=task_id)
114
+ raise HTTPException(
115
+ status_code=status.HTTP_403_FORBIDDEN,
116
+ detail="You can only update your own tasks"
117
+ )
118
+
119
+ # Update the task
120
+ updated_task = TaskService.update_task(session, task_id, task_update)
121
+ if not updated_task:
122
+ raise HTTPException(
123
+ status_code=status.HTTP_404_NOT_FOUND,
124
+ detail=f"Task with id {task_id} not found"
125
+ )
126
+
127
+ log_operation("TASK_UPDATED_SUCCESSFULLY", user_id=current_user_id, task_id=task_id)
128
+ return TaskResponse.model_validate(updated_task)
129
+ except HTTPException:
130
+ raise
131
+ except Exception as e:
132
+ log_operation("UPDATE_TASK_ERROR", user_id=current_user_id)
133
+ raise HTTPException(
134
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
135
+ detail=f"An error occurred while updating the task: {str(e)}"
136
+ )
137
+
138
+
139
+ @router.patch("/{task_id}/toggle", response_model=TaskResponse)
140
+ def toggle_task_completion(
141
+ task_id: int,
142
+ current_user_id: str = Depends(get_current_user_id),
143
+ session: Session = Depends(get_session)
144
+ ) -> TaskResponse:
145
+ """
146
+ Toggle the completion status of a task (only if the task belongs to the authenticated user)
147
+ """
148
+ try:
149
+ # Get the task from the database
150
+ existing_task = TaskService.get_task_by_id(session, task_id)
151
+ if not existing_task:
152
+ raise HTTPException(
153
+ status_code=status.HTTP_404_NOT_FOUND,
154
+ detail=f"Task with id {task_id} not found"
155
+ )
156
+
157
+ # Verify that the authenticated user owns this task
158
+ if existing_task.user_id != current_user_id:
159
+ log_operation("UNAUTHORIZED_ACCESS_ATTEMPT", user_id=current_user_id, task_id=task_id)
160
+ raise HTTPException(
161
+ status_code=status.HTTP_403_FORBIDDEN,
162
+ detail="You can only toggle completion status of your own tasks"
163
+ )
164
+
165
+ # Toggle the task completion status
166
+ toggled_task = TaskService.toggle_task_completion(session, task_id)
167
+ if not toggled_task:
168
+ raise HTTPException(
169
+ status_code=status.HTTP_404_NOT_FOUND,
170
+ detail=f"Task with id {task_id} not found"
171
+ )
172
+
173
+ log_operation("TASK_COMPLETION_TOGGLED_SUCCESSFULLY", user_id=current_user_id, task_id=task_id)
174
+ return TaskResponse.model_validate(toggled_task)
175
+ except HTTPException:
176
+ raise
177
+ except Exception as e:
178
+ log_operation("TOGGLE_TASK_ERROR", user_id=current_user_id)
179
+ raise HTTPException(
180
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
181
+ detail=f"An error occurred while toggling the task: {str(e)}"
182
+ )
183
+
184
+
185
+ @router.delete("/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
186
+ def delete_task(
187
+ task_id: int,
188
+ current_user_id: str = Depends(get_current_user_id),
189
+ session: Session = Depends(get_session)
190
+ ):
191
+ """
192
+ Delete a task by ID (only if the task belongs to the authenticated user)
193
+ """
194
+ try:
195
+ # Get the task from the database
196
+ existing_task = TaskService.get_task_by_id(session, task_id)
197
+ if not existing_task:
198
+ raise HTTPException(
199
+ status_code=status.HTTP_404_NOT_FOUND,
200
+ detail=f"Task with id {task_id} not found"
201
+ )
202
+
203
+ # Verify that the authenticated user owns this task
204
+ if existing_task.user_id != current_user_id:
205
+ log_operation("UNAUTHORIZED_ACCESS_ATTEMPT", user_id=current_user_id, task_id=task_id)
206
+ raise HTTPException(
207
+ status_code=status.HTTP_403_FORBIDDEN,
208
+ detail="You can only delete your own tasks"
209
+ )
210
+
211
+ # Delete the task
212
+ success = TaskService.delete_task(session, task_id)
213
+ if not success:
214
+ raise HTTPException(
215
+ status_code=status.HTTP_404_NOT_FOUND,
216
+ detail=f"Task with id {task_id} not found"
217
+ )
218
+
219
+ log_operation("TASK_DELETED_SUCCESSFULLY", user_id=current_user_id, task_id=task_id)
220
+ except HTTPException:
221
+ raise
222
+ except Exception as e:
223
+ log_operation("DELETE_TASK_ERROR", user_id=current_user_id)
224
+ raise HTTPException(
225
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
226
+ detail=f"An error occurred while deleting the task: {str(e)}"
227
+ )
src/auth/__init__.py ADDED
File without changes
src/auth/deps.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import timedelta
2
+ from typing import Optional
3
+ from fastapi import HTTPException, status, Depends
4
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
5
+ from jose import JWTError, jwt
6
+ from src.core.config import settings
7
+ from src.auth.security import verify_token, create_access_token
8
+
9
+
10
+ # HTTP Bearer scheme for token authentication
11
+ security = HTTPBearer()
12
+
13
+
14
+ def get_current_user_id(credentials: HTTPAuthorizationCredentials = Depends(security)):
15
+ """
16
+ Get the current user ID from the JWT token in the Authorization header
17
+ """
18
+ token = credentials.credentials
19
+
20
+ payload = verify_token(token)
21
+ if payload is None:
22
+ raise HTTPException(
23
+ status_code=status.HTTP_401_UNAUTHORIZED,
24
+ detail="Invalid authentication credentials",
25
+ headers={"WWW-Authenticate": "Bearer"},
26
+ )
27
+
28
+ user_id: str = payload.get("user_id")
29
+ if user_id is None:
30
+ raise HTTPException(
31
+ status_code=status.HTTP_401_UNAUTHORIZED,
32
+ detail="Could not validate credentials",
33
+ headers={"WWW-Authenticate": "Bearer"},
34
+ )
35
+
36
+ return user_id
37
+
38
+
39
+ def get_current_user_payload(credentials: HTTPAuthorizationCredentials = Depends(security)):
40
+ """
41
+ Get the full user payload from the JWT token in the Authorization header
42
+ """
43
+ token = credentials.credentials
44
+
45
+ payload = verify_token(token)
46
+ if payload is None:
47
+ raise HTTPException(
48
+ status_code=status.HTTP_401_UNAUTHORIZED,
49
+ detail="Invalid authentication credentials",
50
+ headers={"WWW-Authenticate": "Bearer"},
51
+ )
52
+
53
+ return payload
54
+
55
+
56
+ def require_authenticated_user(current_user_id: str = Depends(get_current_user_id)):
57
+ """
58
+ Require an authenticated user for endpoints that need authentication
59
+ but don't necessarily need the user ID
60
+ """
61
+ return current_user_id
62
+
63
+
64
+ def verify_admin_access(current_user_payload: dict = Depends(get_current_user_payload)):
65
+ """
66
+ Verify that the current user has admin access
67
+ """
68
+ role = current_user_payload.get("role", "user")
69
+ if role != "admin":
70
+ raise HTTPException(
71
+ status_code=status.HTTP_403_FORBIDDEN,
72
+ detail="Admin access required"
73
+ )
74
+ return current_user_payload
75
+
76
+
77
+ def refresh_access_token(current_user_payload: dict) -> str:
78
+ """
79
+ Generate a new access token based on the current user's payload
80
+ This function can be used to refresh an expired token
81
+ """
82
+ # Remove the expiration time from the payload to create a new token
83
+ user_data = {key: value for key, value in current_user_payload.items() if key != "exp"}
84
+
85
+ # Create a new token with fresh expiration
86
+ new_token = create_access_token(data=user_data)
87
+ return new_token
88
+
89
+
90
+ def is_token_expired(payload: dict) -> bool:
91
+ """
92
+ Check if the token in the payload is expired
93
+ """
94
+ exp_time = payload.get("exp")
95
+ if exp_time is None:
96
+ return True
97
+
98
+ import time
99
+ current_time = time.time()
100
+ return current_time >= exp_time
src/auth/middleware.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from fastapi import Request, HTTPException, status
3
+ from fastapi.security.http import HTTPBearer
4
+ from jose import JWTError, jwt
5
+ from starlette.middleware.base import BaseHTTPMiddleware
6
+ from starlette.responses import Response
7
+ from starlette.requests import Request as StarletteRequest
8
+ from datetime import datetime
9
+ from src.core.config import settings
10
+ from src.auth.security import verify_token
11
+
12
+
13
+ class JWTMiddleware(BaseHTTPMiddleware):
14
+ """
15
+ Middleware to verify JWT tokens for protected routes
16
+ """
17
+ def __init__(self, app):
18
+ super().__init__(app)
19
+ self.http_bearer = HTTPBearer(auto_error=False)
20
+
21
+ async def dispatch(self, request: Request, call_next):
22
+ # Skip authentication for public routes (you can customize this list)
23
+ public_routes = [
24
+ "/", # Root endpoint (public)
25
+ "/docs", "/redoc", "/openapi.json", # Swagger/OpenAPI docs
26
+ "/health", # Health check endpoint
27
+ "/api/v1/login", # Login endpoint (public)
28
+ "/api/v1/register", # Registration endpoint (public)
29
+ # Add other public routes as needed
30
+ ]
31
+
32
+ # Check if the current path is a public route
33
+ is_public_route = any(request.url.path.startswith(route) for route in public_routes)
34
+
35
+ # Also skip authentication for OPTIONS requests (preflight CORS requests)
36
+ if request.method == "OPTIONS" or is_public_route:
37
+ response = await call_next(request)
38
+ return response
39
+
40
+ # Extract the authorization header
41
+ auth_header = request.headers.get("Authorization")
42
+
43
+ if auth_header is None:
44
+ raise HTTPException(
45
+ status_code=status.HTTP_401_UNAUTHORIZED,
46
+ detail="Authorization header is missing",
47
+ headers={"WWW-Authenticate": "Bearer"},
48
+ )
49
+
50
+ # Verify the format of the authorization header
51
+ try:
52
+ scheme, token = auth_header.split(" ")
53
+ if scheme.lower() != "bearer":
54
+ raise HTTPException(
55
+ status_code=status.HTTP_401_UNAUTHORIZED,
56
+ detail="Authorization scheme must be Bearer",
57
+ headers={"WWW-Authenticate": "Bearer"},
58
+ )
59
+ except ValueError:
60
+ raise HTTPException(
61
+ status_code=status.HTTP_401_UNAUTHORIZED,
62
+ detail="Invalid authorization header format",
63
+ headers={"WWW-Authenticate": "Bearer"},
64
+ )
65
+
66
+ # Verify the token
67
+ payload = verify_token(token)
68
+ if payload is None:
69
+ raise HTTPException(
70
+ status_code=status.HTTP_401_UNAUTHORIZED,
71
+ detail="Invalid or expired token",
72
+ headers={"WWW-Authenticate": "Bearer"},
73
+ )
74
+
75
+ # Check if token is expired (double-checking expiration)
76
+ exp_time = payload.get("exp")
77
+ if exp_time:
78
+ current_time = datetime.utcnow().timestamp()
79
+ if current_time >= exp_time:
80
+ raise HTTPException(
81
+ status_code=status.HTTP_401_UNAUTHORIZED,
82
+ detail="Token has expired",
83
+ headers={"WWW-Authenticate": "Bearer"},
84
+ )
85
+
86
+ # Add user info to request state for use in endpoints
87
+ request.state.user_id = payload.get("user_id")
88
+ request.state.user_role = payload.get("role", "user")
89
+ request.state.token_payload = payload # Include full payload for potential refresh logic
90
+
91
+ response = await call_next(request)
92
+ return response
93
+
94
+
95
+ # Function to create and configure the middleware
96
+ def get_jwt_middleware():
97
+ return JWTMiddleware
98
+
99
+
100
+ # Global instance of the middleware (if needed)
101
+ jwt_middleware = get_jwt_middleware()
src/auth/security.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timedelta
2
+ from typing import Optional
3
+ import os
4
+ from jose import JWTError, jwt
5
+ from fastapi import HTTPException, status, Depends
6
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
7
+ from sqlmodel import Session
8
+ from src.core.database import get_session
9
+ from src.models.task import Task
10
+ from src.core.config import settings
11
+ from src.core.logging import log_operation, log_token_validation_result, log_token_lifecycle_event
12
+
13
+
14
+ # JWT token creation and validation functions
15
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
16
+ """
17
+ Create a JWT access token with the provided data
18
+ """
19
+ to_encode = data.copy()
20
+
21
+ # Validate input data for security
22
+ if "user_id" in to_encode:
23
+ user_id = to_encode["user_id"]
24
+ if not isinstance(user_id, str) or len(user_id) == 0 or len(user_id) > 255:
25
+ raise ValueError("Invalid user_id: must be a non-empty string with max 255 characters")
26
+
27
+ if "role" in to_encode:
28
+ role = to_encode["role"]
29
+ if role not in ["user", "admin"]:
30
+ # In a real application, you might want to be more flexible with roles
31
+ # For now, we'll only allow "user" and "admin" roles
32
+ log_security_event("INVALID_ROLE_ASSIGNED", user_id=to_encode.get("user_id", "unknown"), severity="ERROR")
33
+ raise ValueError(f"Invalid role: {role}. Only 'user' and 'admin' roles are allowed.")
34
+
35
+ if expires_delta:
36
+ expire = datetime.utcnow() + expires_delta
37
+ else:
38
+ expire = datetime.utcnow() + timedelta(seconds=int(settings.JWT_EXPIRATION_DELTA))
39
+
40
+ to_encode.update({"exp": expire})
41
+
42
+ # Add additional security claims
43
+ to_encode.update({
44
+ "iat": datetime.utcnow(), # Issued at
45
+ "nbf": datetime.utcnow(), # Not before (token valid immediately)
46
+ })
47
+
48
+ encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
49
+
50
+ # Log token creation
51
+ user_id = data.get("user_id", "unknown")
52
+ log_operation("TOKEN_CREATED", user_id=user_id)
53
+ log_token_validation_result("CREATED", user_id=user_id)
54
+
55
+ return encoded_jwt
56
+
57
+
58
+ def verify_token(token: str) -> Optional[dict]:
59
+ """
60
+ Verify a JWT token and return the payload if valid
61
+ """
62
+ try:
63
+ # Additional security validation
64
+ if not token or len(token) == 0:
65
+ log_security_event("EMPTY_TOKEN_RECEIVED", severity="ERROR")
66
+ return None
67
+
68
+ # Check token format (should have 3 parts separated by dots)
69
+ parts = token.split('.')
70
+ if len(parts) != 3:
71
+ log_security_event("MALFORMED_TOKEN_RECEIVED", severity="ERROR")
72
+ return None
73
+
74
+ # Verify the token
75
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
76
+
77
+ # Additional security checks
78
+ user_id = payload.get("user_id", "unknown")
79
+
80
+ # Check that the token has not expired (double-check)
81
+ exp_time = payload.get("exp")
82
+ if exp_time:
83
+ current_time = datetime.utcnow().timestamp()
84
+ if current_time >= exp_time:
85
+ log_token_validation_result("EXPIRED", user_id=user_id, reason="Token expiry time reached")
86
+ return None
87
+
88
+ # Log successful validation
89
+ log_token_lifecycle_event("VALID", user_id=user_id)
90
+
91
+ return payload
92
+ except JWTError as e:
93
+ log_token_validation_result("INVALID", user_id="unknown", reason=f"JWT Error: {str(e)}")
94
+ log_security_event("TOKEN_VERIFICATION_FAILED", severity="ERROR", details=str(e))
95
+ return None
96
+ except Exception as e:
97
+ log_token_validation_result("INVALID", user_id="unknown", reason=f"Unexpected error: {str(e)}")
98
+ log_security_event("TOKEN_VERIFICATION_ERROR", severity="ERROR", details=str(e))
99
+ return None
100
+
101
+
102
+ def validate_jwt_token(token: str) -> Optional[dict]:
103
+ """
104
+ Validate a JWT token and return the payload if valid.
105
+ This function specifically implements the token validation functionality
106
+ """
107
+ try:
108
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
109
+
110
+ # Check if token is expired
111
+ exp_time = payload.get("exp")
112
+ if exp_time:
113
+ current_time = datetime.utcnow().timestamp()
114
+ if current_time >= exp_time:
115
+ user_id = payload.get("user_id", "unknown")
116
+ log_token_validation_result("EXPIRED", user_id=user_id, reason="Token expiry time reached")
117
+ return None
118
+
119
+ user_id = payload.get("user_id", "unknown")
120
+ log_token_validation_result("VALID", user_id=user_id)
121
+ return payload
122
+ except JWTError as e:
123
+ log_token_validation_result("INVALID", reason=str(e))
124
+ return None
125
+
126
+
127
+ def get_current_user_payload(
128
+ credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)),
129
+ db: Session = Depends(get_session)
130
+ ):
131
+ """
132
+ Get the current user's payload from the JWT token
133
+ """
134
+ if credentials is None:
135
+ log_token_validation_result("MISSING", reason="No authorization token provided")
136
+ raise HTTPException(
137
+ status_code=status.HTTP_401_UNAUTHORIZED,
138
+ detail="No authorization token provided",
139
+ headers={"WWW-Authenticate": "Bearer"},
140
+ )
141
+
142
+ token = credentials.credentials
143
+ payload = validate_jwt_token(token)
144
+
145
+ if payload is None:
146
+ user_id = "unknown"
147
+ if credentials:
148
+ # Try to extract user_id from the invalid token for logging purposes
149
+ try:
150
+ temp_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM], options={"verify_exp": False})
151
+ user_id = temp_payload.get("user_id", "unknown")
152
+ except:
153
+ pass
154
+
155
+ log_token_validation_result("FAILED", user_id=user_id, reason="Invalid or expired token")
156
+ raise HTTPException(
157
+ status_code=status.HTTP_401_UNAUTHORIZED,
158
+ detail="Invalid or expired token",
159
+ headers={"WWW-Authenticate": "Bearer"},
160
+ )
161
+
162
+ user_id: str = payload.get("user_id")
163
+ if user_id is None:
164
+ log_token_validation_result("FAILED", reason="No user_id in token payload")
165
+ raise HTTPException(
166
+ status_code=status.HTTP_401_UNAUTHORIZED,
167
+ detail="Could not validate credentials",
168
+ headers={"WWW-Authenticate": "Bearer"},
169
+ )
170
+
171
+ return payload
172
+
173
+
174
+ def get_current_user_id(
175
+ current_user_payload: dict = Depends(get_current_user_payload)
176
+ ):
177
+ """
178
+ Extract the user ID from the current user's payload
179
+ """
180
+ user_id = current_user_payload.get("user_id")
181
+ if user_id is None:
182
+ raise HTTPException(
183
+ status_code=status.HTTP_401_UNAUTHORIZED,
184
+ detail="Could not validate credentials",
185
+ headers={"WWW-Authenticate": "Bearer"},
186
+ )
187
+ return user_id
188
+
189
+
190
+ def authorize_user_for_task(
191
+ task: Task,
192
+ current_user_id: str = Depends(get_current_user_id)
193
+ ):
194
+ """
195
+ Verify that the current user has access to the specified task
196
+ """
197
+ if task.user_id != current_user_id:
198
+ log_operation("AUTHORIZATION_DENIED", user_id=current_user_id, task_id=task.id)
199
+ raise HTTPException(
200
+ status_code=status.HTTP_403_FORBIDDEN,
201
+ detail="Not authorized to access this task"
202
+ )
203
+
204
+ log_operation("AUTHORIZATION_GRANTED", user_id=current_user_id, task_id=task.id)
205
+ return task
206
+
207
+
208
+ def validate_token_not_expired(payload: dict) -> bool:
209
+ """
210
+ Validate that the token has not expired
211
+ """
212
+ exp_time = payload.get("exp")
213
+ if exp_time is None:
214
+ return False
215
+
216
+ current_time = datetime.utcnow().timestamp()
217
+ is_valid = current_time < exp_time
218
+
219
+ user_id = payload.get("user_id", "unknown")
220
+ if not is_valid:
221
+ log_token_validation_result("EXPIRED_CHECK", user_id=user_id, reason="Token expiry validation failed")
222
+ else:
223
+ log_token_validation_result("NOT_EXPIRED", user_id=user_id)
224
+
225
+ return is_valid
226
+
227
+
228
+ def get_user_id_from_token_payload(payload: dict) -> Optional[str]:
229
+ """
230
+ Extract the user ID from the token payload
231
+ """
232
+ user_id = payload.get("user_id")
233
+ if user_id:
234
+ log_operation("USER_ID_EXTRACTED", user_id=user_id)
235
+ return user_id
src/auth/user_service.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from sqlmodel import Session, select
3
+ from fastapi import HTTPException, status
4
+ from passlib.context import CryptContext
5
+ from datetime import datetime, timedelta
6
+ from jose import JWTError, jwt
7
+ from src.models.user import User, UserCreate, UserPublic
8
+ from src.core.config import settings
9
+
10
+ # Password hashing context
11
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
12
+
13
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
14
+ """Verify a plaintext password against a hashed password."""
15
+ return pwd_context.verify(plain_password, hashed_password)
16
+
17
+ def get_password_hash(password: str) -> str:
18
+ """Hash a plaintext password."""
19
+ return pwd_context.hash(password)
20
+
21
+ def authenticate_user(session: Session, email: str, password: str) -> Optional[User]:
22
+ """Authenticate a user by email and password."""
23
+ statement = select(User).where(User.email == email)
24
+ user = session.exec(statement).first()
25
+
26
+ if not user or not verify_password(password, user.hashed_password):
27
+ return None
28
+
29
+ return user
30
+
31
+ def create_user(session: Session, user_create: UserCreate) -> User:
32
+ """Create a new user with hashed password."""
33
+ # Check if user already exists
34
+ statement = select(User).where(User.email == user_create.email)
35
+ existing_user = session.exec(statement).first()
36
+ if existing_user:
37
+ raise HTTPException(
38
+ status_code=status.HTTP_409_CONFLICT,
39
+ detail="User with this email already exists"
40
+ )
41
+
42
+ # Hash the password
43
+ hashed_password = get_password_hash(user_create.password)
44
+
45
+ # Create the user
46
+ db_user = User(
47
+ email=user_create.email,
48
+ name=user_create.name,
49
+ hashed_password=hashed_password
50
+ )
51
+
52
+ session.add(db_user)
53
+ session.commit()
54
+ session.refresh(db_user)
55
+
56
+ return db_user
src/core/__init__.py ADDED
File without changes
src/core/config.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Optional
3
+ from pydantic_settings import BaseSettings
4
+
5
+
6
+ class Settings(BaseSettings):
7
+ DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:///./todo_app.db")
8
+ SECRET_KEY: str = os.getenv("SECRET_KEY", "dev-secret-key-change-in-production")
9
+ DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
10
+
11
+ # JWT settings
12
+ BETTER_AUTH_SECRET: str = os.getenv("BETTER_AUTH_SECRET", "dev-better-auth-secret-change-in-production")
13
+ BETTER_AUTH_PUBLIC_KEY: str = os.getenv("BETTER_AUTH_PUBLIC_KEY", "")
14
+ JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256") # Changed from RS256 to HS256 for simpler implementation
15
+ JWT_EXPIRATION_DELTA: int = int(os.getenv("JWT_EXPIRATION_DELTA", "604800")) # 7 days in seconds
16
+
17
+ model_config = {"env_file": ".env"}
18
+
19
+
20
+ settings = Settings()
src/core/database.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import create_engine, Session, SQLModel
2
+ from src.core.config import settings
3
+ from src.models.task import Task # Import models to register them with SQLModel metadata
4
+ from src.models.user import User # Import models to register them with SQLModel metadata
5
+
6
+ # Create the database engine
7
+ engine = create_engine(
8
+ settings.DATABASE_URL,
9
+ echo=settings.DEBUG,
10
+ connect_args=(
11
+ {"check_same_thread": False} if "sqlite" in settings.DATABASE_URL else {}
12
+ ),
13
+ )
14
+
15
+
16
+ def get_session():
17
+ with Session(engine) as session:
18
+ yield session
19
+
20
+
21
+ def create_db_and_tables():
22
+ SQLModel.metadata.create_all(engine)
src/core/logging.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from datetime import datetime
3
+
4
+ # Configure logging
5
+ logging.basicConfig(level=logging.INFO)
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def log_operation(operation: str, user_id: str = None, task_id: int = None):
10
+ """
11
+ Log an operation with user and task context
12
+ """
13
+ timestamp = datetime.now().isoformat()
14
+ context = f"[{timestamp}] Operation: {operation}"
15
+ if user_id:
16
+ context += f", User: {user_id}"
17
+ if task_id:
18
+ context += f", Task: {task_id}"
19
+
20
+ logger.info(context)
21
+
22
+
23
+ def log_error(error: Exception, operation: str):
24
+ """
25
+ Log an error with operation context
26
+ """
27
+ timestamp = datetime.now().isoformat()
28
+ logger.error(f"[{timestamp}] Error in '{operation}': {str(error)}")
29
+
30
+
31
+ def log_authentication_event(event: str, user_id: str = None, ip_address: str = None):
32
+ """
33
+ Log authentication-related events
34
+ """
35
+ timestamp = datetime.now().isoformat()
36
+ context = f"[{timestamp}] Auth Event: {event}"
37
+ if user_id:
38
+ context += f", User: {user_id}"
39
+ if ip_address:
40
+ context += f", IP: {ip_address}"
41
+
42
+ logger.info(context)
43
+
44
+
45
+ def log_authorization_decision(action: str, user_id: str, resource: str, granted: bool):
46
+ """
47
+ Log authorization decisions
48
+ """
49
+ timestamp = datetime.now().isoformat()
50
+ decision = "GRANTED" if granted else "DENIED"
51
+ context = f"[{timestamp}] Authorization {decision}: User {user_id} attempted to {action} {resource}"
52
+
53
+ logger.info(context)
54
+
55
+
56
+ def log_token_validation_result(token_status: str, user_id: str = None, reason: str = None):
57
+ """
58
+ Log JWT token validation results
59
+ """
60
+ timestamp = datetime.now().isoformat()
61
+ context = f"[{timestamp}] Token Validation: {token_status}"
62
+ if user_id:
63
+ context += f", User: {user_id}"
64
+ if reason:
65
+ context += f", Reason: {reason}"
66
+
67
+ logger.info(context)
68
+
69
+
70
+ def log_token_lifecycle_event(event: str, user_id: str = None, token_id: str = None, details: str = None):
71
+ """
72
+ Log token lifecycle events (creation, refresh, expiry, etc.)
73
+ """
74
+ timestamp = datetime.now().isoformat()
75
+ context = f"[{timestamp}] Token Lifecycle: {event}"
76
+ if user_id:
77
+ context += f", User: {user_id}"
78
+ if token_id:
79
+ context += f", Token: {token_id}"
80
+ if details:
81
+ context += f", Details: {details}"
82
+
83
+ logger.info(context)
84
+
85
+
86
+ def log_security_event(event: str, user_id: str = None, ip_address: str = None, severity: str = "INFO"):
87
+ """
88
+ Log security-related events
89
+ """
90
+ timestamp = datetime.now().isoformat()
91
+ context = f"[{timestamp}] Security Event [{severity}]: {event}"
92
+ if user_id:
93
+ context += f", User: {user_id}"
94
+ if ip_address:
95
+ context += f", IP: {ip_address}"
96
+
97
+ if severity.upper() == "ERROR" or severity.upper() == "CRITICAL":
98
+ logger.error(context)
99
+ else:
100
+ logger.info(context)
src/main.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from src.api.v1 import tasks
3
+ from src.api.v1.auth import router as auth_router
4
+ from src.auth.middleware import JWTMiddleware
5
+ from fastapi.middleware.cors import CORSMiddleware
6
+ from src.core.database import create_db_and_tables
7
+
8
+
9
+ app = FastAPI(title="Todo API", version="1.0.0")
10
+
11
+
12
+ @app.on_event("startup")
13
+ def on_startup():
14
+ create_db_and_tables()
15
+
16
+
17
+ # Add JWT authentication middleware
18
+ app.add_middleware(JWTMiddleware)
19
+
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["http://localhost:3000"], # frontend
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ # Include API routes
29
+ app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"])
30
+ app.include_router(auth_router, prefix="/api/v1", tags=["auth"])
31
+
32
+
33
+ @app.get("/")
34
+ def read_root():
35
+ return {"Hello": "World"}
36
+
37
+
38
+ if __name__ == "__main__":
39
+ import uvicorn
40
+
41
+ uvicorn.run(app, host="0.0.0.0", port=7860)
src/models/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .task import Task, TaskCreate, TaskUpdate, TaskResponse
2
+
3
+ __all__ = ["Task", "TaskCreate", "TaskUpdate", "TaskResponse"]
src/models/task.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from sqlmodel import SQLModel, Field
4
+ from sqlalchemy import select
5
+ from sqlmodel import Session
6
+ from pydantic import ConfigDict
7
+
8
+
9
+ class TaskBase(SQLModel):
10
+ title: str = Field(min_length=1, max_length=255)
11
+ description: Optional[str] = Field(default=None, max_length=1000)
12
+ completed: bool = Field(default=False)
13
+ user_id: str = Field(max_length=255)
14
+
15
+
16
+ class Task(TaskBase, table=True):
17
+ id: Optional[int] = Field(default=None, primary_key=True)
18
+ created_at: datetime = Field(default_factory=datetime.utcnow)
19
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
20
+
21
+ model_config = ConfigDict(from_attributes=True)
22
+
23
+ @classmethod
24
+ def get_by_user_id(cls, session: Session, user_id: str):
25
+ """
26
+ Class method to get all tasks for a specific user
27
+ """
28
+ statement = select(cls).where(cls.user_id == user_id)
29
+ result = session.exec(statement)
30
+ tasks = result.all()
31
+ return tasks
32
+
33
+ @classmethod
34
+ def get_by_id_and_user_id(cls, session: Session, task_id: int, user_id: str):
35
+ """
36
+ Class method to get a specific task for a specific user (for data isolation)
37
+ """
38
+ statement = select(cls).where(cls.id == task_id, cls.user_id == user_id)
39
+ return session.exec(statement).first()
40
+
41
+
42
+ class TaskCreate(TaskBase):
43
+ user_id: Optional[str] = Field(default=None, max_length=255) # Override to make optional for creation
44
+
45
+
46
+ class TaskUpdate(SQLModel):
47
+ title: Optional[str] = Field(default=None, min_length=1, max_length=255)
48
+ description: Optional[str] = Field(default=None, max_length=1000)
49
+ completed: Optional[bool] = None
50
+
51
+
52
+ from pydantic import ConfigDict
53
+
54
+ class TaskResponse(TaskBase):
55
+ id: int
56
+ created_at: datetime
57
+ updated_at: datetime
58
+
59
+ model_config = ConfigDict(from_attributes=True)
src/models/user.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field
2
+ from typing import Optional
3
+ from sqlalchemy import Column, String
4
+
5
+
6
+ class UserBase(SQLModel):
7
+ email: str = Field(sa_column=Column(String, unique=True, index=True))
8
+ name: Optional[str] = Field(default=None)
9
+
10
+
11
+ class User(UserBase, table=True):
12
+ id: Optional[int] = Field(default=None, primary_key=True)
13
+ hashed_password: str
14
+
15
+ class Config:
16
+ from_attributes = True
17
+
18
+ __table_args__ = {"extend_existing": True}
19
+
20
+
21
+ class UserCreate(UserBase):
22
+ email: str
23
+ password: str
24
+ name: str
25
+
26
+
27
+ class UserPublic(UserBase):
28
+ id: int
29
+ email: str
30
+ name: Optional[str] = None
31
+
32
+
33
+ class UserUpdate(SQLModel):
34
+ name: Optional[str] = None
35
+ email: Optional[str] = None
src/services/task_service.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Optional
2
+ from sqlmodel import Session, select
3
+ from src.models.task import Task, TaskCreate, TaskUpdate
4
+ from src.core.logging import log_operation, log_error, log_authorization_decision
5
+
6
+
7
+ class TaskService:
8
+ @staticmethod
9
+ def create_task(session: Session, task_create: TaskCreate) -> Task:
10
+ """
11
+ Create a new task in the database
12
+ """
13
+ try:
14
+ log_operation("CREATE_TASK", user_id=str(task_create.user_id))
15
+
16
+ db_task = Task(**task_create.dict())
17
+ session.add(db_task)
18
+ session.commit()
19
+ session.refresh(db_task)
20
+
21
+ log_operation("TASK_CREATED", user_id=str(task_create.user_id), task_id=db_task.id)
22
+ return db_task
23
+ except Exception as e:
24
+ log_error(e, "CREATE_TASK")
25
+ session.rollback()
26
+ raise
27
+
28
+ @staticmethod
29
+ def get_task_by_id(session: Session, task_id: int) -> Optional[Task]:
30
+ """
31
+ Retrieve a task by its ID
32
+ """
33
+ try:
34
+ log_operation("GET_TASK_BY_ID", task_id=task_id)
35
+
36
+ statement = select(Task).where(Task.id == task_id)
37
+ task = session.exec(statement).first()
38
+
39
+ if task:
40
+ log_operation("TASK_FOUND", task_id=task_id, user_id=task.user_id)
41
+ else:
42
+ log_operation("TASK_NOT_FOUND", task_id=task_id)
43
+
44
+ return task
45
+ except Exception as e:
46
+ log_error(e, "GET_TASK_BY_ID")
47
+ raise
48
+
49
+ @staticmethod
50
+ def get_tasks_by_user_id(session: Session, user_id: str) -> List[Task]:
51
+ """
52
+ Retrieve all tasks for a specific user
53
+ """
54
+ try:
55
+ log_operation("GET_TASKS_BY_USER", user_id=user_id)
56
+
57
+ # Using the enhanced model method
58
+ tasks = Task.get_by_user_id(session, user_id)
59
+
60
+ # Ensure we're returning Task objects and not Row objects
61
+ # If the result contains Row objects, extract the Task from them
62
+ processed_tasks = []
63
+ for task in tasks:
64
+ if hasattr(task, '__iter__') and not isinstance(task, str) and hasattr(task, '__getitem__'):
65
+ # This looks like a Row/tuple object, extract the first element if it's a Task
66
+ try:
67
+ if len(task) > 0:
68
+ item = task[0]
69
+ if isinstance(item, Task):
70
+ processed_tasks.append(item)
71
+ else:
72
+ processed_tasks.append(task)
73
+ else:
74
+ processed_tasks.append(task)
75
+ except:
76
+ # If there's any issue with unpacking, just add the original
77
+ processed_tasks.append(task)
78
+ else:
79
+ processed_tasks.append(task)
80
+
81
+ log_operation(f"FOUND_{len(processed_tasks)}_TASKS_FOR_USER", user_id=user_id)
82
+ return processed_tasks
83
+ except Exception as e:
84
+ log_error(e, "GET_TASKS_BY_USER")
85
+ raise
86
+
87
+ @staticmethod
88
+ def get_task_by_id_and_user_id(session: Session, task_id: int, user_id: str) -> Optional[Task]:
89
+ """
90
+ Retrieve a task by ID for a specific user (enforcing data isolation)
91
+ """
92
+ try:
93
+ log_operation("GET_TASK_BY_ID_AND_USER", user_id=user_id, task_id=task_id)
94
+
95
+ # Using the enhanced model method for data isolation
96
+ task = Task.get_by_id_and_user_id(session, task_id, user_id)
97
+
98
+ if task:
99
+ log_operation("TASK_FOUND_FOR_USER", user_id=user_id, task_id=task_id)
100
+ else:
101
+ log_operation("TASK_NOT_FOUND_FOR_USER", user_id=user_id, task_id=task_id)
102
+
103
+ return task
104
+ except Exception as e:
105
+ log_error(e, "GET_TASK_BY_ID_AND_USER")
106
+ raise
107
+
108
+ @staticmethod
109
+ def update_task(session: Session, task_id: int, task_update: TaskUpdate, current_user_id: str = None) -> Optional[Task]:
110
+ """
111
+ Update an existing task, with user ownership validation if current_user_id is provided
112
+ """
113
+ try:
114
+ # Get the existing task
115
+ statement = select(Task).where(Task.id == task_id)
116
+ db_task = session.exec(statement).first()
117
+
118
+ if not db_task:
119
+ log_operation("TASK_UPDATE_FAILED_NOT_FOUND", task_id=task_id)
120
+ return None
121
+
122
+ # If current user is provided, validate ownership
123
+ if current_user_id and db_task.user_id != current_user_id:
124
+ log_authorization_decision("update", current_user_id, f"task-{task_id}", False)
125
+ raise PermissionError(f"User {current_user_id} does not own task {task_id}")
126
+
127
+ # Log successful authorization if user was validated
128
+ if current_user_id:
129
+ log_authorization_decision("update", current_user_id, f"task-{task_id}", True)
130
+
131
+ # Apply updates
132
+ update_data = task_update.dict(exclude_unset=True)
133
+ for field, value in update_data.items():
134
+ setattr(db_task, field, value)
135
+
136
+ session.add(db_task)
137
+ session.commit()
138
+ session.refresh(db_task)
139
+
140
+ log_operation("TASK_UPDATED", user_id=db_task.user_id, task_id=task_id)
141
+ return db_task
142
+ except Exception as e:
143
+ log_error(e, "UPDATE_TASK")
144
+ session.rollback()
145
+ raise
146
+
147
+ @staticmethod
148
+ def delete_task(session: Session, task_id: int, current_user_id: str = None) -> bool:
149
+ """
150
+ Delete a task by its ID, with user ownership validation if current_user_id is provided
151
+ """
152
+ try:
153
+ statement = select(Task).where(Task.id == task_id)
154
+ db_task = session.exec(statement).first()
155
+
156
+ if not db_task:
157
+ log_operation("TASK_DELETE_FAILED_NOT_FOUND", task_id=task_id)
158
+ return False
159
+
160
+ # If current user is provided, validate ownership
161
+ if current_user_id and db_task.user_id != current_user_id:
162
+ log_authorization_decision("delete", current_user_id, f"task-{task_id}", False)
163
+ raise PermissionError(f"User {current_user_id} does not own task {task_id}")
164
+
165
+ # Log successful authorization if user was validated
166
+ if current_user_id:
167
+ log_authorization_decision("delete", current_user_id, f"task-{task_id}", True)
168
+
169
+ session.delete(db_task)
170
+ session.commit()
171
+
172
+ log_operation("TASK_DELETED", user_id=db_task.user_id, task_id=task_id)
173
+ return True
174
+ except Exception as e:
175
+ log_error(e, "DELETE_TASK")
176
+ session.rollback()
177
+ raise
178
+
179
+ @staticmethod
180
+ def toggle_task_completion(session: Session, task_id: int, current_user_id: str = None) -> Optional[Task]:
181
+ """
182
+ Toggle the completion status of a task, with user ownership validation if current_user_id is provided
183
+ """
184
+ try:
185
+ statement = select(Task).where(Task.id == task_id)
186
+ db_task = session.exec(statement).first()
187
+
188
+ if not db_task:
189
+ log_operation("TASK_TOGGLE_FAILED_NOT_FOUND", task_id=task_id)
190
+ return None
191
+
192
+ # If current user is provided, validate ownership
193
+ if current_user_id and db_task.user_id != current_user_id:
194
+ log_authorization_decision("toggle", current_user_id, f"task-{task_id}", False)
195
+ raise PermissionError(f"User {current_user_id} does not own task {task_id}")
196
+
197
+ # Log successful authorization if user was validated
198
+ if current_user_id:
199
+ log_authorization_decision("toggle", current_user_id, f"task-{task_id}", True)
200
+
201
+ # Toggle completion status
202
+ db_task.completed = not db_task.completed
203
+
204
+ session.add(db_task)
205
+ session.commit()
206
+ session.refresh(db_task)
207
+
208
+ log_operation("TASK_COMPLETION_TOGGLED", user_id=db_task.user_id, task_id=task_id)
209
+ return db_task
210
+ except Exception as e:
211
+ log_error(e, "TOGGLE_TASK_COMPLETION")
212
+ session.rollback()
213
+ raise
214
+
215
+ @staticmethod
216
+ def verify_task_ownership(session: Session, task_id: int, user_id: str) -> bool:
217
+ """
218
+ Verify that a specific user owns a specific task
219
+ """
220
+ try:
221
+ statement = select(Task).where(Task.id == task_id)
222
+ task = session.exec(statement).first()
223
+
224
+ if not task:
225
+ return False
226
+
227
+ return task.user_id == user_id
228
+ except Exception as e:
229
+ log_error(e, "VERIFY_TASK_OWNERSHIP")
230
+ raise
src/utils/__init__.py ADDED
File without changes
src/utils/code_cleanup.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility script for code cleanup and refactoring across all modules
3
+ This script identifies common issues and applies standard formatting
4
+ """
5
+
6
+ import os
7
+ import re
8
+ from pathlib import Path
9
+
10
+
11
+ def find_python_files(root_dir: str) -> list:
12
+ """Find all Python files in the specified directory"""
13
+ python_files = []
14
+ for root, dirs, files in os.walk(root_dir):
15
+ for file in files:
16
+ if file.endswith('.py') and not file.startswith('.'):
17
+ python_files.append(os.path.join(root, file))
18
+ return python_files
19
+
20
+
21
+ def find_typescript_files(root_dir: str) -> list:
22
+ """Find all TypeScript/TSX files in the specified directory"""
23
+ ts_files = []
24
+ for root, dirs, files in os.walk(root_dir):
25
+ for file in files:
26
+ if file.endswith(('.ts', '.tsx')) and not file.startswith('.'):
27
+ ts_files.append(os.path.join(root, file))
28
+ return ts_files
29
+
30
+
31
+ def standardize_imports(file_path: str):
32
+ """Standardize import statements in the file"""
33
+ with open(file_path, 'r', encoding='utf-8') as f:
34
+ content = f.read()
35
+
36
+ # Look for common import issues and fix them
37
+ # Sort imports alphabetically and separate stdlib, third-party, and local imports
38
+ lines = content.split('\n')
39
+ new_lines = []
40
+
41
+ stdlib_imports = []
42
+ third_party_imports = []
43
+ local_imports = []
44
+ other_lines = []
45
+
46
+ for line in lines:
47
+ if line.startswith('import ') or line.startswith('from '):
48
+ # Identify import type by checking if common modules are in the line
49
+ is_stdlib = any(keyword in line for keyword in [' os.', ' os\n', ' os ', ' sys.', ' sys\n', ' sys ', ' pathlib.', ' pathlib\n', ' pathlib ', ' typing.', ' typing\n', ' typing '])
50
+ is_third_party = any(keyword in line for keyword in [' fastapi', ' sqlmodel', ' jose', ' pydantic'])
51
+ is_local = any(keyword in line for keyword in [' src.', ' backend.'])
52
+
53
+ if is_stdlib:
54
+ stdlib_imports.append(line)
55
+ elif is_third_party:
56
+ third_party_imports.append(line)
57
+ elif is_local:
58
+ local_imports.append(line)
59
+ else:
60
+ third_party_imports.append(line)
61
+ else:
62
+ other_lines.append(line)
63
+
64
+ # Combine in order: stdlib, third-party, local with proper spacing
65
+ all_imports = []
66
+ if stdlib_imports:
67
+ all_imports.extend(sorted(set(stdlib_imports)))
68
+ all_imports.append('') # Empty line after stdlib imports
69
+ if third_party_imports:
70
+ all_imports.extend(sorted(set(third_party_imports)))
71
+ all_imports.append('') # Empty line after third-party imports
72
+ if local_imports:
73
+ all_imports.extend(sorted(set(local_imports)))
74
+ all_imports.append('') # Empty line after local imports
75
+
76
+ new_content = '\n'.join(all_imports + other_lines)
77
+
78
+ with open(file_path, 'w', encoding='utf-8') as f:
79
+ f.write(new_content)
80
+
81
+
82
+ def remove_unused_imports(file_path: str):
83
+ """Remove unused imports from the file"""
84
+ with open(file_path, 'r', encoding='utf-8') as f:
85
+ content = f.read()
86
+
87
+ # This is a simplified version - in practice, you'd use a tool like unimport
88
+ # For now, just ensure imports are properly formatted
89
+ lines = content.split('\n')
90
+ new_lines = []
91
+ in_import_block = False
92
+
93
+ for line in lines:
94
+ if line.startswith('import ') or line.startswith('from '):
95
+ if not in_import_block:
96
+ in_import_block = True
97
+ new_lines.append(line)
98
+ elif line.strip() and not line.startswith(('import ', 'from ')):
99
+ in_import_block = False
100
+ new_lines.append(line)
101
+ else:
102
+ new_lines.append(line)
103
+ else:
104
+ in_import_block = False
105
+ new_lines.append(line)
106
+
107
+ new_content = '\n'.join(new_lines)
108
+ with open(file_path, 'w', encoding='utf-8') as f:
109
+ f.write(new_content)
110
+
111
+
112
+ def format_strings_consistently(file_path: str):
113
+ """Standardize string formatting in the file"""
114
+ with open(file_path, 'r', encoding='utf-8') as f:
115
+ content = f.read()
116
+
117
+ # Standardize f-string usage where appropriate
118
+ # Standardize quote usage (prefer double quotes for consistency)
119
+ # This is a simplified version - in practice, you'd use black or similar
120
+
121
+ # Fix common string formatting issues
122
+ content = re.sub(r"f'([^']*)'", r'f"\1"', content) # Convert f-string single quotes to double
123
+ content = re.sub(r"'([^']*)'", r'"\1"', content) # Convert single quotes to double where safe
124
+
125
+ with open(file_path, 'w', encoding='utf-8') as f:
126
+ f.write(content)
127
+
128
+
129
+ def cleanup_whitespace(file_path: str):
130
+ """Remove trailing whitespace and ensure consistent line endings"""
131
+ with open(file_path, 'r', encoding='utf-8') as f:
132
+ lines = f.readlines()
133
+
134
+ # Remove trailing whitespace and ensure newline at end
135
+ cleaned_lines = [line.rstrip() + '\n' for line in lines]
136
+ if cleaned_lines and not cleaned_lines[-1].endswith('\n'):
137
+ cleaned_lines[-1] += '\n'
138
+
139
+ with open(file_path, 'w', encoding='utf-8') as f:
140
+ f.writelines(cleaned_lines)
141
+
142
+
143
+ def apply_standard_cleanups(root_dir: str):
144
+ """Apply all standard cleanup operations to files in the directory"""
145
+ print(f"Starting code cleanup in: {root_dir}")
146
+
147
+ # Process Python files
148
+ python_files = find_python_files(root_dir)
149
+ print(f"Found {len(python_files)} Python files to process")
150
+
151
+ for file_path in python_files:
152
+ print(f"Processing: {file_path}")
153
+ try:
154
+ cleanup_whitespace(file_path)
155
+ remove_unused_imports(file_path)
156
+ standardize_imports(file_path)
157
+ format_strings_consistently(file_path)
158
+ except Exception as e:
159
+ print(f"Error processing {file_path}: {str(e)}")
160
+ except Exception as e:
161
+ print(f"Error processing {file_path}: {str(e)}")
162
+
163
+ # Process TypeScript files
164
+ ts_files = find_typescript_files(root_dir)
165
+ print(f"Found {len(ts_files)} TypeScript/TSX files to process")
166
+
167
+ for file_path in ts_files:
168
+ print(f"Processing: {file_path}")
169
+ try:
170
+ cleanup_whitespace(file_path)
171
+ except Exception as e:
172
+ print(f"Error processing {file_path}: {str(e)}")
173
+
174
+ print("Code cleanup completed!")
175
+
176
+
177
+ if __name__ == "__main__":
178
+ import sys
179
+ if len(sys.argv) > 1:
180
+ root_directory = sys.argv[1]
181
+ else:
182
+ root_directory = input("Enter the root directory to clean up: ").strip()
183
+
184
+ if os.path.exists(root_directory):
185
+ apply_standard_cleanups(root_directory)
186
+ else:
187
+ print(f"Directory {root_directory} does not exist!")
src/utils/performance.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Performance optimization utilities for API calls and UI rendering
3
+ """
4
+ import time
5
+ import functools
6
+ from typing import Callable, Any
7
+ from src.core.logging import log_operation
8
+
9
+
10
+ def measure_execution_time(func: Callable) -> Callable:
11
+ """
12
+ Decorator to measure and log the execution time of functions
13
+ """
14
+ @functools.wraps(func)
15
+ def wrapper(*args, **kwargs):
16
+ start_time = time.time()
17
+ result = func(*args, **kwargs)
18
+ end_time = time.time()
19
+
20
+ execution_time_ms = (end_time - start_time) * 1000
21
+
22
+ # Log the execution time
23
+ log_operation(
24
+ f"EXECUTION_TIME_{func.__name__.upper()}",
25
+ task_id=int(execution_time_ms) if execution_time_ms < 1000000 else None # Only if it fits as task_id
26
+ )
27
+
28
+ print(f"{func.__name__} executed in {execution_time_ms:.2f} ms")
29
+
30
+ # Log warning if execution time is too high
31
+ if execution_time_ms > 200: # Threshold of 200ms
32
+ log_operation(
33
+ f"SLOW_EXECUTION_{func.__name__.upper()}",
34
+ details=f"Execution took {execution_time_ms:.2f} ms"
35
+ )
36
+
37
+ return result
38
+
39
+ return wrapper
40
+
41
+
42
+ def cache_result(expiration_time: int = 300):
43
+ """
44
+ Decorator to cache function results for a specified time (in seconds)
45
+ """
46
+ def decorator(func: Callable) -> Callable:
47
+ cache = {}
48
+
49
+ @functools.wraps(func)
50
+ def wrapper(*args, **kwargs):
51
+ # Create a cache key based on function name and arguments
52
+ cache_key = f"{func.__name__}_{hash(str(args) + str(kwargs))}"
53
+
54
+ current_time = time.time()
55
+
56
+ # Check if result is cached and not expired
57
+ if cache_key in cache:
58
+ result, timestamp = cache[cache_key]
59
+ if current_time - timestamp < expiration_time:
60
+ return result
61
+
62
+ # Execute the function and cache the result
63
+ result = func(*args, **kwargs)
64
+ cache[cache_key] = (result, current_time)
65
+
66
+ return result
67
+
68
+ return wrapper
69
+ return decorator
70
+
71
+
72
+ def batch_process(items: list, batch_size: int = 10):
73
+ """
74
+ Process items in batches to optimize performance
75
+ """
76
+ for i in range(0, len(items), batch_size):
77
+ yield items[i:i + batch_size]
78
+
79
+
80
+ def optimize_database_queries():
81
+ """
82
+ Utility function to apply database query optimizations
83
+ """
84
+ # This would typically configure database connection pooling, query optimization settings
85
+ # For now, we'll just return a success message
86
+ log_operation("DATABASE_OPTIMIZATIONS_APPLIED")
87
+ return {
88
+ "connection_pooling": "enabled",
89
+ "query_batching": "available",
90
+ "caching": "configured"
91
+ }
92
+
93
+
94
+ def throttle_requests(max_requests_per_minute: int = 1000):
95
+ """
96
+ Decorator to throttle requests to prevent overwhelming the system
97
+ """
98
+ def decorator(func: Callable) -> Callable:
99
+ request_times = []
100
+
101
+ @functools.wraps(func)
102
+ def wrapper(*args, **kwargs):
103
+ current_time = time.time()
104
+
105
+ # Remove requests older than 1 minute
106
+ request_times[:] = [req_time for req_time in request_times if current_time - req_time < 60]
107
+
108
+ # Check if we've exceeded the limit
109
+ if len(request_times) >= max_requests_per_minute:
110
+ raise Exception(f"Rate limit exceeded: {max_requests_per_minute} requests per minute")
111
+
112
+ # Add current request time
113
+ request_times.append(current_time)
114
+
115
+ return func(*args, **kwargs)
116
+
117
+ return wrapper
118
+ return decorator
119
+
120
+
121
+ def lazy_load_data(load_func: Callable, threshold: int = 100):
122
+ """
123
+ Utility to implement lazy loading for large datasets
124
+ """
125
+ def wrapper(*args, **kwargs):
126
+ # If the dataset is small, load everything
127
+ result = load_func(*args, **kwargs)
128
+
129
+ if isinstance(result, list) and len(result) > threshold:
130
+ # For large datasets, implement pagination or chunking
131
+ return {
132
+ "data": result[:threshold], # Return first chunk
133
+ "has_more": True,
134
+ "total_count": len(result)
135
+ }
136
+ else:
137
+ return result
138
+
139
+ return wrapper
140
+
141
+
142
+ def debounce(wait_time: float = 0.3):
143
+ """
144
+ Decorator to debounce function calls (useful for UI events)
145
+ """
146
+ def decorator(func: Callable) -> Callable:
147
+ timer = None
148
+
149
+ @functools.wraps(func)
150
+ def debounced(*args, **kwargs):
151
+ nonlocal timer
152
+
153
+ if timer:
154
+ # Cancel previous timer
155
+ timer.cancel()
156
+
157
+ # Set new timer
158
+ import threading
159
+ timer = threading.Timer(wait_time, lambda: func(*args, **kwargs))
160
+ timer.start()
161
+
162
+ return debounced
163
+ return decorator
164
+
165
+
166
+ def memoize(func: Callable) -> Callable:
167
+ """
168
+ Simple memoization decorator to cache function results based on arguments
169
+ """
170
+ cache = {}
171
+
172
+ @functools.wraps(func)
173
+ def wrapper(*args, **kwargs):
174
+ # Create a key from the function arguments
175
+ key = str(args) + str(sorted(kwargs.items()))
176
+
177
+ if key in cache:
178
+ return cache[key]
179
+
180
+ result = func(*args, **kwargs)
181
+ cache[key] = result
182
+ return result
183
+
184
+ return wrapper
185
+
186
+
187
+ def apply_performance_optimizations():
188
+ """
189
+ Apply all performance optimizations to the application
190
+ """
191
+ log_operation("APPLYING_PERFORMANCE_OPTIMIZATIONS")
192
+
193
+ optimizations = {
194
+ "execution_time_monitoring": "enabled",
195
+ "result_caching": "configured",
196
+ "request_throttling": "set_to_1000_per_minute",
197
+ "database_optimizations": optimize_database_queries(),
198
+ "lazy_loading_threshold": 100,
199
+ "debounce_defaults": 0.3
200
+ }
201
+
202
+ log_operation("PERFORMANCE_OPTIMIZATIONS_APPLIED")
203
+ return optimizations
src/utils/validators.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ from src.models.task import TaskCreate, TaskUpdate
3
+ from fastapi import HTTPException, status
4
+
5
+
6
+ def validate_task_create(task_create: TaskCreate) -> None:
7
+ """
8
+ Validate task creation data
9
+ """
10
+ if not task_create.title or len(task_create.title.strip()) == 0:
11
+ raise HTTPException(
12
+ status_code=status.HTTP_400_BAD_REQUEST,
13
+ detail="Task title is required"
14
+ )
15
+
16
+ if len(task_create.title) > 255:
17
+ raise HTTPException(
18
+ status_code=status.HTTP_400_BAD_REQUEST,
19
+ detail="Task title must be 255 characters or less"
20
+ )
21
+
22
+ if task_create.description and len(task_create.description) > 1000:
23
+ raise HTTPException(
24
+ status_code=status.HTTP_400_BAD_REQUEST,
25
+ detail="Task description must be 1000 characters or less"
26
+ )
27
+
28
+ # Skip user_id validation here since it will be set from JWT token
29
+ # The user_id will be validated after it's set from the JWT
30
+ # This validation should happen after the user_id is set in the endpoint
31
+
32
+
33
+ def validate_task_update(task_update: TaskUpdate) -> None:
34
+ """
35
+ Validate task update data
36
+ """
37
+ if task_update.title is not None:
38
+ if len(task_update.title) == 0:
39
+ raise HTTPException(
40
+ status_code=status.HTTP_400_BAD_REQUEST,
41
+ detail="Task title cannot be empty"
42
+ )
43
+ if len(task_update.title) > 255:
44
+ raise HTTPException(
45
+ status_code=status.HTTP_400_BAD_REQUEST,
46
+ detail="Task title must be 255 characters or less"
47
+ )
48
+
49
+ if task_update.description is not None and len(task_update.description) > 1000:
50
+ raise HTTPException(
51
+ status_code=status.HTTP_400_BAD_REQUEST,
52
+ detail="Task description must be 1000 characters or less"
53
+ )
54
+
55
+
56
+ def validate_user_access(task_user_id: str, requesting_user_id: Optional[str]) -> bool:
57
+ """
58
+ Validate that the requesting user has access to the task
59
+ """
60
+ if not requesting_user_id:
61
+ return False
62
+ return task_user_id == requesting_user_id
test_implementation.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Simple test to validate the backend implementation
3
+ """
4
+ from src.models.task import Task, TaskCreate, TaskUpdate, TaskResponse
5
+ from src.services.task_service import TaskService
6
+ from src.core.database import engine
7
+ from sqlmodel import Session, create_engine, SQLModel
8
+
9
+
10
+ def test_basic_functionality():
11
+ """
12
+ Test basic functionality of the task system
13
+ """
14
+ print("Testing basic task functionality...")
15
+
16
+ # Create a test task
17
+ task_create = TaskCreate(
18
+ title="Test Task",
19
+ description="This is a test task",
20
+ user_id="test_user_123"
21
+ )
22
+
23
+ print(f"Created TaskCreate: {task_create}")
24
+ print(f"Title: {task_create.title}")
25
+ print(f"Description: {task_create.description}")
26
+ print(f"User ID: {task_create.user_id}")
27
+ print(f"Completed (default): {task_create.completed}")
28
+
29
+ # Test TaskUpdate
30
+ task_update = TaskUpdate(title="Updated Title", completed=True)
31
+ print(f"\nTaskUpdate: {task_update}")
32
+
33
+ # Test TaskResponse
34
+ from datetime import datetime
35
+ task_response = TaskResponse(
36
+ id=1,
37
+ title="Response Task",
38
+ description="Test response",
39
+ completed=False,
40
+ user_id="test_user_123",
41
+ created_at=datetime.now(),
42
+ updated_at=datetime.now()
43
+ )
44
+ print(f"\nTaskResponse: {task_response}")
45
+
46
+ print("\n✅ Basic functionality test passed!")
47
+
48
+
49
+ if __name__ == "__main__":
50
+ test_basic_functionality()
tests/contract/test_data_isolation.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from backend.src.main import app
4
+ from backend.src.auth.security import create_access_token
5
+ from backend.src.models.task import TaskCreate
6
+
7
+
8
+ def test_user_data_isolation_with_different_users():
9
+ """Test that different users cannot access each other's tasks"""
10
+ client = TestClient(app)
11
+
12
+ # Create tokens for two different users
13
+ user1_data = {"user_id": "user_1_test", "role": "user"}
14
+ user2_data = {"user_id": "user_2_test", "role": "user"}
15
+
16
+ token_user1 = create_access_token(data=user1_data)
17
+ token_user2 = create_access_token(data=user2_data)
18
+
19
+ # User 1 creates a task
20
+ task_data = {
21
+ "title": "User 1 task",
22
+ "description": "This is a task for user 1",
23
+ "user_id": "user_1_test"
24
+ }
25
+
26
+ response = client.post(
27
+ "/api/v1/tasks/",
28
+ json=task_data,
29
+ headers={"Authorization": f"Bearer {token_user1}"}
30
+ )
31
+
32
+ assert response.status_code == 201
33
+ task_response = response.json()
34
+ task_id = task_response["id"]
35
+
36
+ # User 2 tries to access user 1's task (should be denied)
37
+ response_user2_access = client.get(
38
+ f"/api/v1/tasks/{task_id}",
39
+ headers={"Authorization": f"Bearer {token_user2}"}
40
+ )
41
+
42
+ # This should fail with 403 Forbidden or 404 Not Found (depending on implementation)
43
+ assert response_user2_access.status_code in [403, 404]
44
+
45
+
46
+ def test_user_can_access_own_tasks():
47
+ """Test that users can access their own tasks"""
48
+ client = TestClient(app)
49
+
50
+ # Create a token for a user
51
+ user_data = {"user_id": "own_task_user", "role": "user"}
52
+ token = create_access_token(data=user_data)
53
+
54
+ # User creates a task
55
+ task_data = {
56
+ "title": "Own task",
57
+ "description": "This is my own task",
58
+ "user_id": "own_task_user"
59
+ }
60
+
61
+ response = client.post(
62
+ "/api/v1/tasks/",
63
+ json=task_data,
64
+ headers={"Authorization": f"Bearer {token}"}
65
+ )
66
+
67
+ assert response.status_code == 201
68
+ task_response = response.json()
69
+ task_id = task_response["id"]
70
+
71
+ # User should be able to access their own task
72
+ response_get = client.get(
73
+ f"/api/v1/tasks/{task_id}",
74
+ headers={"Authorization": f"Bearer {token}"}
75
+ )
76
+
77
+ # This should succeed
78
+ assert response_get.status_code in [200, 404] # 200 if endpoint allows getting single task, 404 if not
79
+
80
+
81
+ def test_user_cannot_modify_other_users_task():
82
+ """Test that users cannot modify other users' tasks"""
83
+ client = TestClient(app)
84
+
85
+ # Create tokens for two different users
86
+ user1_data = {"user_id": "mod_user_1", "role": "user"}
87
+ user2_data = {"user_id": "mod_user_2", "role": "user"}
88
+
89
+ token_user1 = create_access_token(data=user1_data)
90
+ token_user2 = create_access_token(data=user2_data)
91
+
92
+ # User 1 creates a task
93
+ task_data = {
94
+ "title": "User 1 task to be protected",
95
+ "description": "This task should not be modifiable by others",
96
+ "user_id": "mod_user_1"
97
+ }
98
+
99
+ response = client.post(
100
+ "/api/v1/tasks/",
101
+ json=task_data,
102
+ headers={"Authorization": f"Bearer {token_user1}"}
103
+ )
104
+
105
+ assert response.status_code == 201
106
+ task_response = response.json()
107
+ task_id = task_response["id"]
108
+
109
+ # User 2 tries to update user 1's task (should be denied)
110
+ update_data = {
111
+ "title": "Attempted unauthorized update",
112
+ "description": "User 2 shouldn't be able to do this"
113
+ }
114
+
115
+ response_user2_update = client.put(
116
+ f"/api/v1/tasks/{task_id}",
117
+ json=update_data,
118
+ headers={"Authorization": f"Bearer {token_user2}"}
119
+ )
120
+
121
+ # This should fail with 403 Forbidden
122
+ assert response_user2_update.status_code == 403
123
+
124
+
125
+ def test_user_cannot_delete_other_users_task():
126
+ """Test that users cannot delete other users' tasks"""
127
+ client = TestClient(app)
128
+
129
+ # Create tokens for two different users
130
+ user1_data = {"user_id": "del_user_1", "role": "user"}
131
+ user2_data = {"user_id": "del_user_2", "role": "user"}
132
+
133
+ token_user1 = create_access_token(data=user1_data)
134
+ token_user2 = create_access_token(data=user2_data)
135
+
136
+ # User 1 creates a task
137
+ task_data = {
138
+ "title": "User 1 task to be protected from deletion",
139
+ "description": "This task should not be deletable by others",
140
+ "user_id": "del_user_1"
141
+ }
142
+
143
+ response = client.post(
144
+ "/api/v1/tasks/",
145
+ json=task_data,
146
+ headers={"Authorization": f"Bearer {token_user1}"}
147
+ )
148
+
149
+ assert response.status_code == 201
150
+ task_response = response.json()
151
+ task_id = task_response["id"]
152
+
153
+ # User 2 tries to delete user 1's task (should be denied)
154
+ response_user2_delete = client.delete(
155
+ f"/api/v1/tasks/{task_id}",
156
+ headers={"Authorization": f"Bearer {token_user2}"}
157
+ )
158
+
159
+ # This should fail with 403 Forbidden
160
+ assert response_user2_delete.status_code == 403
161
+
162
+
163
+ def test_user_can_access_their_task_list():
164
+ """Test that users can access their own task list"""
165
+ client = TestClient(app)
166
+
167
+ # Create a token for a user
168
+ user_data = {"user_id": "task_list_user", "role": "user"}
169
+ token = create_access_token(data=user_data)
170
+
171
+ # User accesses their own task list (should be allowed)
172
+ response = client.get(
173
+ "/api/v1/tasks/task_list_user",
174
+ headers={"Authorization": f"Bearer {token}"}
175
+ )
176
+
177
+ # This should succeed (might return empty list if no tasks exist)
178
+ assert response.status_code in [200, 404] # 200 for success, 404 if endpoint not found but auth passed
179
+
180
+
181
+ if __name__ == "__main__":
182
+ pytest.main([__file__])
tests/contract/test_jwt_validation.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from jose import JWTError, jwt
3
+ from backend.src.auth.security import verify_token, create_access_token
4
+ from backend.src.core.config import settings
5
+ from datetime import datetime, timedelta
6
+
7
+
8
+ def test_jwt_token_validation_with_valid_token():
9
+ """Test that a valid JWT token can be successfully validated"""
10
+ # Create a valid token
11
+ data = {"user_id": "test_user_123", "role": "user"}
12
+ token = create_access_token(data=data)
13
+
14
+ # Verify the token
15
+ payload = verify_token(token)
16
+
17
+ # Assert the payload is returned correctly
18
+ assert payload is not None
19
+ assert payload["user_id"] == "test_user_123"
20
+ assert payload["role"] == "user"
21
+ assert "exp" in payload
22
+
23
+
24
+ def test_jwt_token_validation_with_invalid_token():
25
+ """Test that an invalid JWT token returns None"""
26
+ # Create an invalid token (tampered with)
27
+ invalid_token = "invalid.token.string"
28
+
29
+ # Try to verify the token
30
+ payload = verify_token(invalid_token)
31
+
32
+ # Assert the payload is None
33
+ assert payload is None
34
+
35
+
36
+ def test_jwt_token_validation_with_expired_token():
37
+ """Test that an expired JWT token returns None"""
38
+ # Create an expired token
39
+ data = {"user_id": "test_user_123", "role": "user"}
40
+ expired_token = create_access_token(data=data, expires_delta=timedelta(seconds=-1))
41
+
42
+ # Try to verify the expired token
43
+ payload = verify_token(expired_token)
44
+
45
+ # Assert the payload is None
46
+ assert payload is None
47
+
48
+
49
+ def test_jwt_token_contains_correct_claims():
50
+ """Test that JWT tokens contain the expected claims"""
51
+ # Create a token with specific data
52
+ user_data = {"user_id": "test_user_456", "role": "admin", "email": "test@example.com"}
53
+ token = create_access_token(data=user_data)
54
+
55
+ # Decode the token without verification to check claims
56
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
57
+
58
+ # Assert the expected claims are present
59
+ assert decoded_payload["user_id"] == "test_user_456"
60
+ assert decoded_payload["role"] == "admin"
61
+ assert decoded_payload["email"] == "test@example.com"
62
+ assert "exp" in decoded_payload
63
+
64
+
65
+ def test_jwt_algorithm_compliance():
66
+ """Test that JWT tokens are created and validated with the correct algorithm"""
67
+ # Create a token
68
+ data = {"user_id": "test_user_789"}
69
+ token = create_access_token(data=data)
70
+
71
+ # Verify the token using the configured algorithm
72
+ payload = verify_token(token)
73
+
74
+ # Assert the payload is valid
75
+ assert payload is not None
76
+ assert payload["user_id"] == "test_user_789"
77
+
78
+
79
+ if __name__ == "__main__":
80
+ pytest.main([__file__])
tests/contract/test_token_expiry.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from datetime import timedelta
3
+ from jose import jwt
4
+ from backend.src.auth.security import create_access_token, verify_token
5
+ from backend.src.core.config import settings
6
+
7
+
8
+ def test_token_expires_after_specified_duration():
9
+ """Test that tokens expire after the configured duration"""
10
+ # Create a token with a short expiration time
11
+ user_data = {"user_id": "expiry_test_user", "role": "user"}
12
+
13
+ # Create a token that expires in 1 second
14
+ short_lived_token = create_access_token(
15
+ data=user_data,
16
+ expires_delta=timedelta(seconds=1)
17
+ )
18
+
19
+ # Verify the token is valid initially
20
+ payload = verify_token(short_lived_token)
21
+ assert payload is not None
22
+ assert payload["user_id"] == "expiry_test_user"
23
+
24
+ # Wait for more than 1 second
25
+ import time
26
+ time.sleep(2)
27
+
28
+ # Now verify the token should be expired
29
+ expired_payload = verify_token(short_lived_token)
30
+ assert expired_payload is None
31
+
32
+
33
+ def test_token_validation_fails_for_expired_tokens():
34
+ """Test that expired tokens fail validation"""
35
+ from backend.src.auth.security import create_access_token, verify_token
36
+ from datetime import timedelta
37
+
38
+ # Create an expired token manually
39
+ expired_data = {
40
+ "user_id": "expired_user",
41
+ "role": "user",
42
+ "exp": 1000 # Set to Unix epoch + 1000 seconds (definitely in the past)
43
+ }
44
+
45
+ expired_token = jwt.encode(expired_data, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
46
+
47
+ # Verify that the expired token is rejected
48
+ payload = verify_token(expired_token)
49
+ assert payload is None
50
+
51
+
52
+ def test_token_with_future_expiry_remains_valid():
53
+ """Test that tokens with future expiry remain valid"""
54
+ from backend.src.auth.security import create_access_token, verify_token
55
+ from datetime import datetime, timedelta
56
+
57
+ # Create a token that expires in 1 hour
58
+ user_data = {"user_id": "future_expiry_user", "role": "user"}
59
+ future_expiry_token = create_access_token(
60
+ data=user_data,
61
+ expires_delta=timedelta(hours=1)
62
+ )
63
+
64
+ # Verify the token is valid
65
+ payload = verify_token(future_expiry_token)
66
+ assert payload is not None
67
+ assert payload["user_id"] == "future_expiry_user"
68
+
69
+ # Check that the expiry time is in the future
70
+ exp_time = payload.get("exp")
71
+ assert exp_time is not None
72
+ current_time = datetime.utcnow().timestamp()
73
+ assert exp_time > current_time
74
+
75
+
76
+ def test_token_expiry_configuration_respected():
77
+ """Test that the configured expiry duration is respected"""
78
+ from backend.src.auth.security import create_access_token, verify_token
79
+ from datetime import datetime, timedelta
80
+
81
+ # Create a token with default expiry
82
+ user_data = {"user_id": "config_test_user", "role": "user"}
83
+ token = create_access_token(data=user_data)
84
+
85
+ # Decode without verification to check expiry
86
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
87
+
88
+ # Check that expiry is approximately the configured duration from now
89
+ exp_time = decoded_payload.get("exp")
90
+ current_time = datetime.utcnow().timestamp()
91
+ configured_expiry_seconds = int(settings.JWT_EXPIRATION_DELTA)
92
+
93
+ # Allow for a small margin of error (e.g., 5 seconds)
94
+ assert abs(exp_time - current_time - configured_expiry_seconds) < 5
95
+
96
+
97
+ def test_short_lived_token_expires_correctly():
98
+ """Test that tokens with short lifetimes expire correctly"""
99
+ from backend.src.auth.security import create_access_token, verify_token
100
+ from datetime import timedelta
101
+
102
+ # Create a token that expires in 0.5 seconds
103
+ user_data = {"user_id": "short_lived_user", "role": "user"}
104
+ quick_expiry_token = create_access_token(
105
+ data=user_data,
106
+ expires_delta=timedelta(milliseconds=500)
107
+ )
108
+
109
+ # Token should be valid immediately
110
+ payload = verify_token(quick_expiry_token)
111
+ assert payload is not None
112
+
113
+ # Wait for token to expire
114
+ import time
115
+ time.sleep(1) # Sleep for 1 second (longer than 500ms expiry)
116
+
117
+ # Token should now be invalid
118
+ expired_payload = verify_token(quick_expiry_token)
119
+ assert expired_payload is None
120
+
121
+
122
+ def test_token_expiry_validation_in_payload():
123
+ """Test that token expiry validation works correctly in the payload"""
124
+ from backend.src.auth.security import create_access_token, validate_token_not_expired
125
+ from datetime import timedelta
126
+
127
+ # Create a token that expires in 1 hour
128
+ user_data = {"user_id": "validation_test_user", "role": "user"}
129
+ valid_token = create_access_token(
130
+ data=user_data,
131
+ expires_delta=timedelta(hours=1)
132
+ )
133
+
134
+ # Decode the token to get the payload
135
+ payload = jwt.decode(valid_token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
136
+
137
+ # Validate that the token is not expired
138
+ is_not_expired = validate_token_not_expired(payload)
139
+ assert is_not_expired is True
140
+
141
+ # Create an expired token manually
142
+ expired_payload = {
143
+ "user_id": "expired_validation_user",
144
+ "role": "user",
145
+ "exp": 1000 # Definitely in the past
146
+ }
147
+
148
+ # Validate that the expired token is detected
149
+ is_expired = validate_token_not_expired(expired_payload)
150
+ assert is_expired is False
151
+
152
+
153
+ def test_token_without_exp_claim_is_invalid():
154
+ """Test that tokens without an expiry claim are treated as invalid"""
155
+ from backend.src.auth.security import verify_token
156
+
157
+ # Create a token without an expiry claim
158
+ token_without_exp = jwt.encode(
159
+ {"user_id": "no_exp_user", "role": "user"},
160
+ settings.SECRET_KEY,
161
+ algorithm=settings.JWT_ALGORITHM
162
+ )
163
+
164
+ # The token should be considered invalid due to missing exp
165
+ payload = verify_token(token_without_exp)
166
+ # Note: Depending on implementation, this might succeed or fail
167
+ # If it succeeds, it would be caught by validate_token_not_expired function
168
+
169
+
170
+ if __name__ == "__main__":
171
+ pytest.main([__file__])
tests/contract/test_unauthorized_access.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from backend.src.main import app
4
+ from backend.src.auth.security import create_access_token
5
+
6
+
7
+ def test_unauthorized_access_without_token():
8
+ """Test that requests without tokens return 401 Unauthorized"""
9
+ client = TestClient(app)
10
+
11
+ # Try to access a protected endpoint without a token
12
+ response = client.get("/api/v1/tasks/test_user")
13
+
14
+ assert response.status_code == 401
15
+ assert "WWW-Authenticate" in response.headers
16
+ assert response.json()["detail"] == "No authorization token provided"
17
+
18
+
19
+ def test_unauthorized_access_with_invalid_token():
20
+ """Test that requests with invalid tokens return 401 Unauthorized"""
21
+ client = TestClient(app)
22
+
23
+ # Try to access a protected endpoint with an invalid token
24
+ response = client.get(
25
+ "/api/v1/tasks/test_user",
26
+ headers={"Authorization": "Bearer invalid_token_here"}
27
+ )
28
+
29
+ assert response.status_code == 401
30
+ assert "WWW-Authenticate" in response.headers
31
+ assert response.json()["detail"] == "Invalid authentication credentials"
32
+
33
+
34
+ def test_unauthorized_access_with_malformed_token():
35
+ """Test that requests with malformed tokens return 401 Unauthorized"""
36
+ client = TestClient(app)
37
+
38
+ # Try to access a protected endpoint with a malformed token
39
+ response = client.get(
40
+ "/api/v1/tasks/test_user",
41
+ headers={"Authorization": "Bearer malformed.token.format"}
42
+ )
43
+
44
+ assert response.status_code == 401
45
+ assert "WWW-Authenticate" in response.headers
46
+ assert response.json()["detail"] == "Invalid authentication credentials"
47
+
48
+
49
+ def test_unauthorized_access_to_protected_endpoints():
50
+ """Test that all protected endpoints return 401 when accessed without tokens"""
51
+ client = TestClient(app)
52
+
53
+ # Test GET tasks for user
54
+ response_get = client.get("/api/v1/tasks/test_user")
55
+ assert response_get.status_code == 401
56
+
57
+ # Test POST to create task
58
+ response_post = client.post(
59
+ "/api/v1/tasks/",
60
+ json={"title": "Test", "user_id": "test_user"}
61
+ )
62
+ assert response_post.status_code == 401
63
+
64
+ # Test PUT to update task
65
+ response_put = client.put(
66
+ "/api/v1/tasks/1",
67
+ json={"title": "Updated Test"}
68
+ )
69
+ assert response_put.status_code == 401
70
+
71
+ # Test PATCH to toggle task
72
+ response_patch = client.patch("/api/v1/tasks/1/toggle")
73
+ assert response_patch.status_code == 401
74
+
75
+ # Test DELETE task
76
+ response_delete = client.delete("/api/v1/tasks/1")
77
+ assert response_delete.status_code == 401
78
+
79
+
80
+ def test_authorized_access_with_valid_token():
81
+ """Test that requests with valid tokens are allowed"""
82
+ client = TestClient(app)
83
+
84
+ # Create a valid token
85
+ user_data = {"user_id": "valid_user", "role": "user"}
86
+ token = create_access_token(data=user_data)
87
+
88
+ # Access endpoint with valid token (should succeed or return 404 if no tasks exist)
89
+ response = client.get(
90
+ "/api/v1/tasks/valid_user",
91
+ headers={"Authorization": f"Bearer {token}"}
92
+ )
93
+
94
+ # Should be authorized (might return 200 or 404 depending on if tasks exist)
95
+ assert response.status_code in [200, 404]
96
+
97
+
98
+ def test_token_format_validation():
99
+ """Test that various invalid token formats return 401"""
100
+ client = TestClient(app)
101
+
102
+ invalid_formats = [
103
+ "", # Empty token
104
+ "Bearer", # Missing token
105
+ "Bearer ", # Space instead of token
106
+ "Basic token", # Wrong scheme
107
+ "Bearer token with spaces", # Token with spaces
108
+ ]
109
+
110
+ for auth_header in invalid_formats:
111
+ response = client.get(
112
+ "/api/v1/tasks/test_user",
113
+ headers={"Authorization": auth_header} if auth_header else {}
114
+ )
115
+
116
+ # If no header at all, it should return 401 for missing token
117
+ # If invalid format, it should return 401 for invalid token
118
+ assert response.status_code == 401, f"Failed for format: '{auth_header}'"
119
+
120
+
121
+ def test_expired_token_handling():
122
+ """Test that expired tokens return 401"""
123
+ from backend.src.auth.security import create_access_token
124
+ from datetime import timedelta
125
+
126
+ client = TestClient(app)
127
+
128
+ # Create an expired token
129
+ user_data = {"user_id": "expired_user", "role": "user"}
130
+ expired_token = create_access_token(data=user_data, expires_delta=timedelta(seconds=-1))
131
+
132
+ # Try to access with expired token
133
+ response = client.get(
134
+ "/api/v1/tasks/expired_user",
135
+ headers={"Authorization": f"Bearer {expired_token}"}
136
+ )
137
+
138
+ # Should return 401 for expired token
139
+ assert response.status_code == 401
140
+ assert response.json()["detail"] == "Invalid or expired token"
141
+
142
+
143
+ if __name__ == "__main__":
144
+ pytest.main([__file__])
tests/integration/test_401_responses.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from backend.src.main import app
4
+
5
+
6
+ def test_integration_all_endpoints_require_authentication():
7
+ """Integration test to ensure all protected endpoints return 401 without authentication"""
8
+ client = TestClient(app)
9
+
10
+ # Test GET all tasks for user without authentication
11
+ response_get = client.get("/api/v1/tasks/test_user")
12
+ assert response_get.status_code == 401, f"Expected 401 for GET, got {response_get.status_code}"
13
+ assert "WWW-Authenticate" in response_get.headers
14
+ assert "Bearer" in str(response_get.headers.get("WWW-Authenticate"))
15
+
16
+ # Test POST to create task without authentication
17
+ response_post = client.post(
18
+ "/api/v1/tasks/",
19
+ json={"title": "Test task", "user_id": "test_user"}
20
+ )
21
+ assert response_post.status_code == 401, f"Expected 401 for POST, got {response_post.status_code}"
22
+ assert "WWW-Authenticate" in response_post.headers
23
+
24
+ # Test PUT to update task without authentication
25
+ response_put = client.put(
26
+ "/api/v1/tasks/1",
27
+ json={"title": "Updated task"}
28
+ )
29
+ assert response_put.status_code == 401, f"Expected 401 for PUT, got {response_put.status_code}"
30
+ assert "WWW-Authenticate" in response_put.headers
31
+
32
+ # Test PATCH to toggle task completion without authentication
33
+ response_patch = client.patch("/api/v1/tasks/1/toggle")
34
+ assert response_patch.status_code == 401, f"Expected 401 for PATCH, got {response_patch.status_code}"
35
+ assert "WWW-Authenticate" in response_patch.headers
36
+
37
+ # Test DELETE task without authentication
38
+ response_delete = client.delete("/api/v1/tasks/1")
39
+ assert response_delete.status_code == 401, f"Expected 401 for DELETE, got {response_delete.status_code}"
40
+ assert "WWW-Authenticate" in response_delete.headers
41
+
42
+
43
+ def test_integration_unauthorized_requests_return_consistent_format():
44
+ """Integration test to ensure 401 responses have consistent format"""
45
+ client = TestClient(app)
46
+
47
+ # Test various endpoints and verify consistent 401 response format
48
+ endpoints_to_test = [
49
+ ("GET", "/api/v1/tasks/test_user", {}),
50
+ ("POST", "/api/v1/tasks/", {"json": {"title": "Test", "user_id": "test"}}),
51
+ ("PUT", "/api/v1/tasks/1", {"json": {"title": "Updated"}}),
52
+ ("PATCH", "/api/v1/tasks/1/toggle", {}),
53
+ ("DELETE", "/api/v1/tasks/1", {})
54
+ ]
55
+
56
+ for method, endpoint, kwargs in endpoints_to_test:
57
+ response = getattr(client, method.lower())(endpoint, **kwargs)
58
+
59
+ assert response.status_code == 401, f"Method {method} to {endpoint} should return 401, got {response.status_code}"
60
+
61
+ # Verify WWW-Authenticate header is present
62
+ assert "WWW-Authenticate" in response.headers, f"Missing WWW-Authenticate header for {method} {endpoint}"
63
+ assert "Bearer" in str(response.headers["WWW-Authenticate"]), f"Wrong authentication scheme for {method} {endpoint}"
64
+
65
+ # Verify error response format
66
+ error_detail = response.json()
67
+ assert "detail" in error_detail, f"Missing detail in error response for {method} {endpoint}"
68
+ assert isinstance(error_detail["detail"], str), f"Detail should be string for {method} {endpoint}"
69
+
70
+
71
+ def test_integration_public_endpoints_still_work():
72
+ """Integration test to ensure public endpoints still work without authentication"""
73
+ client = TestClient(app)
74
+
75
+ # Test the root endpoint (should be public)
76
+ response = client.get("/")
77
+ assert response.status_code in [200, 404], f"Public endpoint should be accessible, got {response.status_code}"
78
+
79
+
80
+ def test_integration_multiple_unauthenticated_requests():
81
+ """Integration test to ensure system handles multiple unauthenticated requests correctly"""
82
+ client = TestClient(app)
83
+
84
+ # Send multiple unauthenticated requests in sequence
85
+ for i in range(5):
86
+ response = client.get(f"/api/v1/tasks/test_user_{i}")
87
+ assert response.status_code == 401
88
+ assert "WWW-Authenticate" in response.headers
89
+ assert "Bearer" in str(response.headers["WWW-Authenticate"])
90
+
91
+
92
+ def test_integration_different_http_methods_unauthorized():
93
+ """Integration test to ensure different HTTP methods return 401 when unauthenticated"""
94
+ client = TestClient(app)
95
+
96
+ # Test various HTTP methods to ensure they all require authentication
97
+ methods_to_test = ["GET", "POST", "PUT", "PATCH", "DELETE"]
98
+
99
+ for method in methods_to_test:
100
+ # Use a method-appropriate endpoint
101
+ if method == "GET":
102
+ response = client.get("/api/v1/tasks/test_user")
103
+ elif method == "POST":
104
+ response = client.post("/api/v1/tasks/", json={"title": "Test", "user_id": "test"})
105
+ elif method == "PUT":
106
+ response = client.put("/api/v1/tasks/1", json={"title": "Updated"})
107
+ elif method == "PATCH":
108
+ response = client.patch("/api/v1/tasks/1/toggle")
109
+ elif method == "DELETE":
110
+ response = client.delete("/api/v1/tasks/1")
111
+
112
+ assert response.status_code == 401, f"Method {method} should return 401 when unauthenticated"
113
+ assert "WWW-Authenticate" in response.headers, f"Method {method} missing WWW-Authenticate header"
114
+
115
+
116
+ def test_integration_error_message_consistency():
117
+ """Integration test to ensure error messages are consistent across endpoints"""
118
+ client = TestClient(app)
119
+
120
+ # Test that all unauthenticated requests return the same or similar error message
121
+ responses = []
122
+
123
+ # Make requests to different endpoints without authentication
124
+ responses.append(client.get("/api/v1/tasks/test_user"))
125
+ responses.append(client.post("/api/v1/tasks/", json={"title": "Test", "user_id": "test"}))
126
+ responses.append(client.put("/api/v1/tasks/1", json={"title": "Updated"}))
127
+
128
+ # Check that all responses have the same status code and similar error structure
129
+ for response in responses:
130
+ assert response.status_code == 401
131
+ assert "detail" in response.json()
132
+
133
+
134
+ if __name__ == "__main__":
135
+ pytest.main([__file__])
tests/integration/test_authenticated_access.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from backend.src.main import app
4
+ from backend.src.auth.security import create_access_token
5
+ from backend.src.models.task import TaskCreate
6
+
7
+
8
+ def test_authenticated_api_access_with_valid_token():
9
+ """Test that authenticated API endpoints accept valid JWT tokens"""
10
+ client = TestClient(app)
11
+
12
+ # Create a valid JWT token
13
+ user_data = {"user_id": "test_user_123", "role": "user"}
14
+ token = create_access_token(data=user_data)
15
+
16
+ # Make a request to a protected endpoint with the valid token
17
+ response = client.get(
18
+ "/api/v1/tasks/test_user_123",
19
+ headers={"Authorization": f"Bearer {token}"}
20
+ )
21
+
22
+ # Check that the request was accepted (even if no tasks exist)
23
+ # The important thing is that authentication passed
24
+ assert response.status_code in [200, 404] # 200 if tasks exist, 404 if none exist but auth passed
25
+
26
+
27
+ def test_authenticated_api_access_without_token():
28
+ """Test that authenticated API endpoints reject requests without tokens"""
29
+ client = TestClient(app)
30
+
31
+ # Make a request to a protected endpoint without a token
32
+ response = client.get("/api/v1/tasks/test_user_123")
33
+
34
+ # Check that the request was rejected with 401 Unauthorized
35
+ assert response.status_code == 401
36
+ assert "WWW-Authenticate" in response.headers
37
+ assert "Bearer" in str(response.headers.get("WWW-Authenticate"))
38
+
39
+
40
+ def test_authenticated_api_access_with_invalid_token():
41
+ """Test that authenticated API endpoints reject invalid JWT tokens"""
42
+ client = TestClient(app)
43
+
44
+ # Make a request to a protected endpoint with an invalid token
45
+ response = client.get(
46
+ "/api/v1/tasks/test_user_123",
47
+ headers={"Authorization": "Bearer invalid_token_here"}
48
+ )
49
+
50
+ # Check that the request was rejected with 401 Unauthorized
51
+ assert response.status_code == 401
52
+
53
+
54
+ def test_authenticated_api_access_with_expired_token():
55
+ """Test that authenticated API endpoints reject expired JWT tokens"""
56
+ from backend.src.auth.security import create_access_token
57
+ from datetime import timedelta
58
+
59
+ client = TestClient(app)
60
+
61
+ # Create an expired JWT token
62
+ user_data = {"user_id": "test_user_456", "role": "user"}
63
+ expired_token = create_access_token(data=user_data, expires_delta=timedelta(seconds=-1))
64
+
65
+ # Make a request to a protected endpoint with the expired token
66
+ response = client.get(
67
+ "/api/v1/tasks/test_user_456",
68
+ headers={"Authorization": f"Bearer {expired_token}"}
69
+ )
70
+
71
+ # Check that the request was rejected with 401 Unauthorized
72
+ assert response.status_code == 401
73
+
74
+
75
+ def test_authenticated_task_creation_with_valid_token():
76
+ """Test that authenticated task creation works with valid JWT tokens"""
77
+ client = TestClient(app)
78
+
79
+ # Create a valid JWT token
80
+ user_data = {"user_id": "test_user_789", "role": "user"}
81
+ token = create_access_token(data=user_data)
82
+
83
+ # Try to create a task with the valid token
84
+ task_data = {
85
+ "title": "Test task from authenticated access test",
86
+ "description": "This is a test task",
87
+ "user_id": "test_user_789"
88
+ }
89
+
90
+ response = client.post(
91
+ "/api/v1/tasks/",
92
+ json=task_data,
93
+ headers={"Authorization": f"Bearer {token}"}
94
+ )
95
+
96
+ # Check that the request was processed (could be 200 or 422 depending on validation)
97
+ # The important thing is that authentication passed
98
+ assert response.status_code in [201, 422, 400]
99
+
100
+
101
+ def test_different_users_have_different_access():
102
+ """Test that different users have access only to their own resources"""
103
+ client = TestClient(app)
104
+
105
+ # Create tokens for two different users
106
+ user1_data = {"user_id": "user_1", "role": "user"}
107
+ user2_data = {"user_id": "user_2", "role": "user"}
108
+
109
+ token_user1 = create_access_token(data=user1_data)
110
+ token_user2 = create_access_token(data=user2_data)
111
+
112
+ # Both users should be able to access their own endpoints
113
+ response1 = client.get(
114
+ "/api/v1/tasks/user_1",
115
+ headers={"Authorization": f"Bearer {token_user1}"}
116
+ )
117
+
118
+ response2 = client.get(
119
+ "/api/v1/tasks/user_2",
120
+ headers={"Authorization": f"Bearer {token_user2}"}
121
+ )
122
+
123
+ # Both requests should be processed (either 200 or 404 depending on task existence)
124
+ assert response1.status_code in [200, 404]
125
+ assert response2.status_code in [200, 404]
126
+
127
+
128
+ if __name__ == "__main__":
129
+ pytest.main([__file__])
tests/integration/test_cross_user_access.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from backend.src.main import app
4
+ from backend.src.auth.security import create_access_token
5
+
6
+
7
+ def test_integration_cross_user_access_prevention():
8
+ """Integration test to ensure users cannot access other users' data"""
9
+ client = TestClient(app)
10
+
11
+ # Create tokens for two different users
12
+ user1_data = {"user_id": "integration_user_1", "role": "user"}
13
+ user2_data = {"user_id": "integration_user_2", "role": "user"}
14
+
15
+ token_user1 = create_access_token(data=user1_data)
16
+ token_user2 = create_access_token(data=user2_data)
17
+
18
+ # Step 1: User 1 creates a task
19
+ task_data = {
20
+ "title": "Integration test task for user 1",
21
+ "description": "This task belongs to user 1 and should be protected",
22
+ "user_id": "integration_user_1"
23
+ }
24
+
25
+ response_create = client.post(
26
+ "/api/v1/tasks/",
27
+ json=task_data,
28
+ headers={"Authorization": f"Bearer {token_user1}"}
29
+ )
30
+
31
+ assert response_create.status_code == 201, f"Failed to create task: {response_create.text}"
32
+ task_response = response_create.json()
33
+ task_id = task_response["id"]
34
+
35
+ # Step 2: Verify User 1 can access their own task
36
+ response_user1_access = client.get(
37
+ f"/api/v1/tasks/{task_id}",
38
+ headers={"Authorization": f"Bearer {token_user1}"}
39
+ )
40
+
41
+ # This should succeed (if the endpoint exists)
42
+ # Note: Our current API might not have a single-task endpoint, so we check for either 200 or 404
43
+ assert response_user1_access.status_code in [200, 404], f"User 1 should be able to access their task or get 404 if endpoint doesn't exist"
44
+
45
+ # Step 3: User 2 attempts to access User 1's task (should be prevented)
46
+ response_user2_access = client.get(
47
+ f"/api/v1/tasks/{task_id}",
48
+ headers={"Authorization": f"Bearer {token_user2}"}
49
+ )
50
+
51
+ # This should be denied with 403 Forbidden or 404 Not Found (if hiding resources)
52
+ assert response_user2_access.status_code in [403, 404], f"User 2 should not be able to access User 1's task"
53
+
54
+ # Step 4: User 2 attempts to update User 1's task (should be prevented)
55
+ update_data = {
56
+ "title": "Unauthorized update attempt",
57
+ "description": "User 2 should not be able to update this"
58
+ }
59
+
60
+ response_user2_update = client.put(
61
+ f"/api/v1/tasks/{task_id}",
62
+ json=update_data,
63
+ headers={"Authorization": f"Bearer {token_user2}"}
64
+ )
65
+
66
+ assert response_user2_update.status_code == 403, f"User 2 should not be able to update User 1's task"
67
+
68
+ # Step 5: User 2 attempts to delete User 1's task (should be prevented)
69
+ response_user2_delete = client.delete(
70
+ f"/api/v1/tasks/{task_id}",
71
+ headers={"Authorization": f"Bearer {token_user2}"}
72
+ )
73
+
74
+ assert response_user2_delete.status_code == 403, f"User 2 should not be able to delete User 1's task"
75
+
76
+ # Step 6: Verify User 1 can still access their task after all these attempts
77
+ response_final_check = client.get(
78
+ f"/api/v1/tasks/{task_id}",
79
+ headers={"Authorization": f"Bearer {token_user1}"}
80
+ )
81
+
82
+ assert response_final_check.status_code in [200, 404], f"User 1's access should not be affected by other users' attempts"
83
+
84
+
85
+ def test_integration_user_task_list_isolation():
86
+ """Integration test to ensure users can only see their own task lists"""
87
+ client = TestClient(app)
88
+
89
+ # Create tokens for two different users
90
+ user1_data = {"user_id": "task_list_user_1", "role": "user"}
91
+ user2_data = {"user_id": "task_list_user_2", "role": "user"}
92
+
93
+ token_user1 = create_access_token(data=user1_data)
94
+ token_user2 = create_access_token(data=user2_data)
95
+
96
+ # User 1 creates a task
97
+ task1_data = {
98
+ "title": "User 1 task",
99
+ "description": "Task for user 1",
100
+ "user_id": "task_list_user_1"
101
+ }
102
+
103
+ response_user1_task = client.post(
104
+ "/api/v1/tasks/",
105
+ json=task1_data,
106
+ headers={"Authorization": f"Bearer {token_user1}"}
107
+ )
108
+
109
+ assert response_user1_task.status_code == 201
110
+
111
+ # User 2 creates a task
112
+ task2_data = {
113
+ "title": "User 2 task",
114
+ "description": "Task for user 2",
115
+ "user_id": "task_list_user_2"
116
+ }
117
+
118
+ response_user2_task = client.post(
119
+ "/api/v1/tasks/",
120
+ json=task2_data,
121
+ headers={"Authorization": f"Bearer {token_user2}"}
122
+ )
123
+
124
+ assert response_user2_task.status_code == 201
125
+
126
+ # User 1 accesses their task list
127
+ response_user1_list = client.get(
128
+ "/api/v1/tasks/task_list_user_1",
129
+ headers={"Authorization": f"Bearer {token_user1}"}
130
+ )
131
+
132
+ assert response_user1_list.status_code == 200
133
+ user1_tasks = response_user1_list.json()
134
+
135
+ # User 2 accesses their task list
136
+ response_user2_list = client.get(
137
+ "/api/v1/tasks/task_list_user_2",
138
+ headers={"Authorization": f"Bearer {token_user2}"}
139
+ )
140
+
141
+ assert response_user2_list.status_code == 200
142
+ user2_tasks = response_user2_list.json()
143
+
144
+ # Verify that each user only sees their own tasks
145
+ # Check that User 1's list contains their task
146
+ user1_has_own_task = any(task.get("title") == "User 1 task" for task in user1_tasks)
147
+ assert user1_has_own_task, "User 1 should see their own task"
148
+
149
+ # Check that User 2's list contains their task
150
+ user2_has_own_task = any(task.get("title") == "User 2 task" for task in user2_tasks)
151
+ assert user2_has_own_task, "User 2 should see their own task"
152
+
153
+ # Verify that User 1 doesn't see User 2's task and vice versa
154
+ user1_does_not_see_user2_task = all(task.get("title") != "User 2 task" for task in user1_tasks)
155
+ assert user1_does_not_see_user2_task, "User 1 should not see User 2's task"
156
+
157
+ user2_does_not_see_user1_task = all(task.get("title") != "User 1 task" for task in user2_tasks)
158
+ assert user2_does_not_see_user1_task, "User 2 should not see User 1's task"
159
+
160
+
161
+ def test_integration_user_self_modification_allowed():
162
+ """Integration test to ensure users can modify their own tasks"""
163
+ client = TestClient(app)
164
+
165
+ # Create a token for a user
166
+ user_data = {"user_id": "self_modify_user", "role": "user"}
167
+ token = create_access_token(data=user_data)
168
+
169
+ # User creates a task
170
+ task_data = {
171
+ "title": "Original title",
172
+ "description": "Original description",
173
+ "user_id": "self_modify_user"
174
+ }
175
+
176
+ response_create = client.post(
177
+ "/api/v1/tasks/",
178
+ json=task_data,
179
+ headers={"Authorization": f"Bearer {token}"}
180
+ )
181
+
182
+ assert response_create.status_code == 201
183
+ task_response = response_create.json()
184
+ task_id = task_response["id"]
185
+
186
+ # User updates their own task (should be allowed)
187
+ update_data = {
188
+ "title": "Updated title",
189
+ "description": "Updated description"
190
+ }
191
+
192
+ response_update = client.put(
193
+ f"/api/v1/tasks/{task_id}",
194
+ json=update_data,
195
+ headers={"Authorization": f"Bearer {token}"}
196
+ )
197
+
198
+ assert response_update.status_code == 200, f"User should be able to update their own task: {response_update.text}"
199
+
200
+ # User deletes their own task (should be allowed)
201
+ response_delete = client.delete(
202
+ f"/api/v1/tasks/{task_id}",
203
+ headers={"Authorization": f"Bearer {token}"}
204
+ )
205
+
206
+ assert response_delete.status_code == 204, f"User should be able to delete their own task: {response_delete.text}"
207
+
208
+
209
+ if __name__ == "__main__":
210
+ pytest.main([__file__])
tests/integration/test_expired_tokens.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from datetime import timedelta
4
+ from jose import jwt
5
+ from backend.src.main import app
6
+ from backend.src.core.config import settings
7
+ from backend.src.auth.security import create_access_token
8
+
9
+
10
+ def test_integration_request_with_expired_token_returns_401():
11
+ """Integration test that requests with expired tokens return 401"""
12
+ client = TestClient(app)
13
+
14
+ # Create an expired token manually
15
+ expired_data = {
16
+ "user_id": "expired_integration_user",
17
+ "role": "user",
18
+ "exp": 1000 # Set to Unix epoch + 1000 seconds (definitely in the past)
19
+ }
20
+
21
+ expired_token = jwt.encode(expired_data, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
22
+
23
+ # Try to access a protected endpoint with expired token
24
+ response = client.get(
25
+ "/api/v1/tasks/expired_integration_user",
26
+ headers={"Authorization": f"Bearer {expired_token}"}
27
+ )
28
+
29
+ # Should return 401 for expired token
30
+ assert response.status_code == 401
31
+ assert "WWW-Authenticate" in response.headers
32
+ assert response.json()["detail"] == "Invalid or expired token"
33
+
34
+
35
+ def test_integration_short_lived_token_expires_during_session():
36
+ """Integration test that short-lived tokens expire during a session"""
37
+ client = TestClient(app)
38
+
39
+ # Create a token with a very short lifetime
40
+ user_data = {"user_id": "short_session_user", "role": "user"}
41
+ short_lived_token = create_access_token(
42
+ data=user_data,
43
+ expires_delta=timedelta(seconds=1)
44
+ )
45
+
46
+ # Initially, the token should work
47
+ response_before_expiry = client.get(
48
+ "/api/v1/tasks/short_session_user",
49
+ headers={"Authorization": f"Bearer {short_lived_token}"}
50
+ )
51
+
52
+ # Response might be 200 (success) or 404 (not found but auth passed)
53
+ assert response_before_expiry.status_code in [200, 404]
54
+
55
+ # Wait for the token to expire
56
+ import time
57
+ time.sleep(2) # Wait for 2 seconds (longer than 1-second expiry)
58
+
59
+ # Now the same token should fail
60
+ response_after_expiry = client.get(
61
+ "/api/v1/tasks/short_session_user",
62
+ headers={"Authorization": f"Bearer {short_lived_token}"}
63
+ )
64
+
65
+ # Should return 401 for expired token
66
+ assert response_after_expiry.status_code == 401
67
+
68
+
69
+ def test_integration_expired_token_on_different_endpoints():
70
+ """Integration test that expired tokens are rejected on all endpoints"""
71
+ client = TestClient(app)
72
+
73
+ # Create an expired token manually
74
+ expired_data = {
75
+ "user_id": "multi_endpoint_user",
76
+ "role": "user",
77
+ "exp": 1000 # Set to definitely expired
78
+ }
79
+
80
+ expired_token = jwt.encode(expired_data, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
81
+
82
+ # Test all endpoints with expired token
83
+ endpoints_to_test = [
84
+ ("GET", f"/api/v1/tasks/multi_endpoint_user", {}),
85
+ ("POST", "/api/v1/tasks/", {"json": {"title": "Test", "user_id": "multi_endpoint_user"}}),
86
+ ("PUT", "/api/v1/tasks/1", {"json": {"title": "Updated"}}),
87
+ ("PATCH", "/api/v1/tasks/1/toggle", {}),
88
+ ("DELETE", "/api/v1/tasks/1", {})
89
+ ]
90
+
91
+ for method, endpoint, kwargs in endpoints_to_test:
92
+ response = getattr(client, method.lower())(endpoint, headers={"Authorization": f"Bearer {expired_token}"}, **kwargs)
93
+
94
+ # All should return 401 for expired token
95
+ assert response.status_code == 401, f"Method {method} to {endpoint} should return 401 for expired token, got {response.status_code}"
96
+
97
+
98
+ def test_integration_token_expiry_affects_all_operations():
99
+ """Integration test that token expiry affects all user operations consistently"""
100
+ client = TestClient(app)
101
+
102
+ # Create a short-lived token
103
+ user_data = {"user_id": "consistency_user", "role": "user"}
104
+ short_token = create_access_token(
105
+ data=user_data,
106
+ expires_delta=timedelta(seconds=1)
107
+ )
108
+
109
+ # All operations should work initially with the valid token
110
+ initial_responses = []
111
+ initial_responses.append(client.get(f"/api/v1/tasks/consistency_user", headers={"Authorization": f"Bearer {short_token}"}))
112
+ initial_responses.append(client.post("/api/v1/tasks/", json={"title": "Initial", "user_id": "consistency_user"}, headers={"Authorization": f"Bearer {short_token}"}))
113
+
114
+ # Check that initial responses are either successful or indicate auth passed
115
+ for response in initial_responses:
116
+ assert response.status_code in [200, 201, 404, 422], f"Initial request should succeed or reach validation, got {response.status_code}"
117
+
118
+ # Wait for token to expire
119
+ import time
120
+ time.sleep(2)
121
+
122
+ # Same operations should now fail with expired token
123
+ expired_responses = []
124
+ expired_responses.append(client.get(f"/api/v1/tasks/consistency_user", headers={"Authorization": f"Bearer {short_token}"}))
125
+ expired_responses.append(client.post("/api/v1/tasks/", json={"title": "Expired", "user_id": "consistency_user"}, headers={"Authorization": f"Bearer {short_token}"}))
126
+
127
+ # Check that all expired responses return 401
128
+ for response in expired_responses:
129
+ assert response.status_code == 401, f"Request with expired token should return 401, got {response.status_code}"
130
+
131
+
132
+ def test_integration_system_handles_expired_token_gracefully():
133
+ """Integration test that the system handles expired tokens gracefully without crashing"""
134
+ client = TestClient(app)
135
+
136
+ # Create multiple expired tokens with different user IDs
137
+ expired_tokens = []
138
+ for i in range(5):
139
+ expired_data = {
140
+ "user_id": f"graceful_user_{i}",
141
+ "role": "user",
142
+ "exp": 1000 # All definitely expired
143
+ }
144
+ expired_token = jwt.encode(expired_data, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
145
+ expired_tokens.append(expired_token)
146
+
147
+ # Make multiple requests with expired tokens
148
+ for i, token in enumerate(expired_tokens):
149
+ response = client.get(
150
+ f"/api/v1/tasks/graceful_user_{i}",
151
+ headers={"Authorization": f"Bearer {token}"}
152
+ )
153
+
154
+ # Should consistently return 401 without system errors
155
+ assert response.status_code == 401
156
+ assert "WWW-Authenticate" in response.headers
157
+
158
+
159
+ def test_integration_expired_vs_valid_token_behavior():
160
+ """Integration test comparing behavior of expired vs valid tokens"""
161
+ client = TestClient(app)
162
+
163
+ # Create an expired token
164
+ expired_data = {
165
+ "user_id": "compare_expired_user",
166
+ "role": "user",
167
+ "exp": 1000 # Definitely expired
168
+ }
169
+ expired_token = jwt.encode(expired_data, settings.SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
170
+
171
+ # Create a valid token
172
+ valid_data = {"user_id": "compare_valid_user", "role": "user"}
173
+ valid_token = create_access_token(data=valid_data, expires_delta=timedelta(hours=1))
174
+
175
+ # Request with expired token should return 401
176
+ expired_response = client.get(
177
+ "/api/v1/tasks/compare_expired_user",
178
+ headers={"Authorization": f"Bearer {expired_token}"}
179
+ )
180
+ assert expired_response.status_code == 401
181
+
182
+ # Request with valid token should proceed (might return 200 or 404 depending on data)
183
+ valid_response = client.get(
184
+ "/api/v1/tasks/compare_valid_user",
185
+ headers={"Authorization": f"Bearer {valid_token}"}
186
+ )
187
+ assert valid_response.status_code in [200, 404], f"Valid token should allow access, got {valid_response.status_code}"
188
+
189
+
190
+ if __name__ == "__main__":
191
+ pytest.main([__file__])
tests/integration/test_responsive_design.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from fastapi.testclient import TestClient
3
+ from backend.src.main import app
4
+ from backend.src.auth.security import create_access_token
5
+ from backend.src.models.task import TaskCreate
6
+
7
+
8
+ def test_responsive_design_mobile_view():
9
+ """Test that the application works properly on mobile screen sizes"""
10
+ client = TestClient(app)
11
+
12
+ # Create a valid token for testing
13
+ user_data = {"user_id": "responsive_test_user", "role": "user"}
14
+ token = create_access_token(data=user_data)
15
+
16
+ # Test responsive task list endpoint with mobile-like request
17
+ headers = {
18
+ "Authorization": f"Bearer {token}",
19
+ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15"
20
+ }
21
+
22
+ response = client.get(
23
+ "/api/v1/tasks/responsive_test_user",
24
+ headers=headers
25
+ )
26
+
27
+ # Should return 200 or 404 (if no tasks exist but auth passed)
28
+ assert response.status_code in [200, 404]
29
+
30
+ # Verify that the response is properly structured even for mobile
31
+ if response.status_code == 200:
32
+ tasks = response.json()
33
+ assert isinstance(tasks, list) # Response should be a list of tasks
34
+
35
+
36
+ def test_responsive_design_tablet_view():
37
+ """Test that the application works properly on tablet screen sizes"""
38
+ client = TestClient(app)
39
+
40
+ # Create a valid token for testing
41
+ user_data = {"user_id": "tablet_responsive_user", "role": "user"}
42
+ token = create_access_token(data=user_data)
43
+
44
+ # Test with tablet-like user agent
45
+ headers = {
46
+ "Authorization": f"Bearer {token}",
47
+ "User-Agent": "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X) AppleWebKit/605.1.15"
48
+ }
49
+
50
+ response = client.get(
51
+ "/api/v1/tasks/tablet_responsive_user",
52
+ headers=headers
53
+ )
54
+
55
+ # Should return 200 or 404 (if no tasks exist but auth passed)
56
+ assert response.status_code in [200, 404]
57
+
58
+
59
+ def test_responsive_design_desktop_view():
60
+ """Test that the application works properly on desktop screen sizes"""
61
+ client = TestClient(app)
62
+
63
+ # Create a valid token for testing
64
+ user_data = {"user_id": "desktop_responsive_user", "role": "user"}
65
+ token = create_access_token(data=user_data)
66
+
67
+ # Test with desktop-like user agent
68
+ headers = {
69
+ "Authorization": f"Bearer {token}",
70
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
71
+ }
72
+
73
+ response = client.get(
74
+ "/api/v1/tasks/desktop_responsive_user",
75
+ headers=headers
76
+ )
77
+
78
+ # Should return 200 or 404 (if no tasks exist but auth passed)
79
+ assert response.status_code in [200, 404]
80
+
81
+
82
+ def test_responsive_task_creation_form():
83
+ """Test that task creation works across different device types"""
84
+ client = TestClient(app)
85
+
86
+ # Create a valid token for testing
87
+ user_data = {"user_id": "form_responsive_user", "role": "user"}
88
+ token = create_access_token(data=user_data)
89
+
90
+ # Test task creation with different user agents (representing different devices)
91
+ user_agents = [
92
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)", # Mobile
93
+ "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X)", # Tablet
94
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" # Desktop
95
+ ]
96
+
97
+ for ua in user_agents:
98
+ headers = {
99
+ "Authorization": f"Bearer {token}",
100
+ "User-Agent": ua,
101
+ "Content-Type": "application/json"
102
+ }
103
+
104
+ task_data = {
105
+ "title": f"Task from {ua[:10]}...",
106
+ "description": f"Created from device type: {ua}",
107
+ "user_id": "form_responsive_user"
108
+ }
109
+
110
+ response = client.post(
111
+ "/api/v1/tasks/",
112
+ json=task_data,
113
+ headers=headers
114
+ )
115
+
116
+ # Should succeed regardless of device type
117
+ assert response.status_code in [200, 201, 422], f"Failed for user agent: {ua}"
118
+
119
+
120
+ def test_responsive_task_operations():
121
+ """Test that all task operations work properly across different screen sizes"""
122
+ client = TestClient(app)
123
+
124
+ # Create a valid token for testing
125
+ user_data = {"user_id": "ops_responsive_user", "role": "user"}
126
+ token = create_access_token(data=user_data)
127
+
128
+ # Create a task first
129
+ task_data = {
130
+ "title": "Responsive test task",
131
+ "description": "Task to test responsive operations",
132
+ "user_id": "ops_responsive_user"
133
+ }
134
+
135
+ create_response = client.post(
136
+ "/api/v1/tasks/",
137
+ json=task_data,
138
+ headers={"Authorization": f"Bearer {token}"}
139
+ )
140
+
141
+ assert create_response.status_code in [200, 201]
142
+ task = create_response.json()
143
+ task_id = task.get("id") or task.get("data", {}).get("id")
144
+
145
+ if task_id:
146
+ # Test operations with mobile user agent
147
+ mobile_headers = {
148
+ "Authorization": f"Bearer {token}",
149
+ "User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X)"
150
+ }
151
+
152
+ # Test updating task from mobile
153
+ update_data = {"title": "Updated from mobile", "completed": True}
154
+ update_response = client.put(
155
+ f"/api/v1/tasks/{task_id}",
156
+ json=update_data,
157
+ headers=mobile_headers
158
+ )
159
+ assert update_response.status_code in [200, 201]
160
+
161
+ # Test toggling completion from tablet
162
+ tablet_headers = {
163
+ "Authorization": f"Bearer {token}",
164
+ "User-Agent": "Mozilla/5.0 (iPad; CPU OS 14_0 like Mac OS X)"
165
+ }
166
+
167
+ toggle_response = client.patch(
168
+ f"/api/v1/tasks/{task_id}/toggle",
169
+ headers=tablet_headers
170
+ )
171
+ assert toggle_response.status_code == 200
172
+
173
+
174
+ def test_different_screen_size_requests():
175
+ """Test API responses are consistent across different simulated screen sizes"""
176
+ client = TestClient(app)
177
+
178
+ # Create a valid token for testing
179
+ user_data = {"user_id": "screen_size_user", "role": "user"}
180
+ token = create_access_token(data=user_data)
181
+
182
+ # Different viewport sizes simulated through headers
183
+ viewports = [
184
+ {"width": 375, "height": 667, "device": "mobile"}, # iPhone SE
185
+ {"width": 768, "height": 1024, "device": "tablet"}, # iPad
186
+ {"width": 1920, "height": 1080, "device": "desktop"} # Desktop
187
+ ]
188
+
189
+ for viewport in viewports:
190
+ headers = {
191
+ "Authorization": f"Bearer {token}",
192
+ "X-Viewport-Width": str(viewport["width"]),
193
+ "User-Agent": f"TestAgent/{viewport['device']}"
194
+ }
195
+
196
+ response = client.get(
197
+ f"/api/v1/tasks/{viewport['device']}_user",
198
+ headers=headers
199
+ )
200
+
201
+ # Response should be structurally the same regardless of viewport
202
+ assert response.status_code in [200, 404]
203
+
204
+ if response.status_code == 200:
205
+ data = response.json()
206
+ assert isinstance(data, list) # Should always return a list of tasks
207
+
208
+
209
+ if __name__ == "__main__":
210
+ pytest.main([__file__])
tests/unit/test_auth/test_auth_functions.py ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from datetime import timedelta
3
+ from jose import jwt
4
+ from backend.src.auth.security import create_access_token, verify_token
5
+ from backend.src.core.config import settings
6
+
7
+
8
+ def test_create_valid_access_token():
9
+ """Test that creating a valid access token works correctly"""
10
+ user_data = {"user_id": "test_user_123", "role": "user"}
11
+
12
+ token = create_access_token(data=user_data)
13
+
14
+ # Verify that the token was created
15
+ assert token is not None
16
+ assert isinstance(token, str)
17
+ assert len(token) > 0
18
+
19
+ # Verify that the token can be decoded and contains the right data
20
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
21
+ assert decoded_payload["user_id"] == "test_user_123"
22
+ assert decoded_payload["role"] == "user"
23
+ assert "exp" in decoded_payload
24
+
25
+
26
+ def test_create_access_token_with_custom_expiry():
27
+ """Test that creating a token with custom expiry works correctly"""
28
+ user_data = {"user_id": "expiry_test_user", "role": "admin"}
29
+
30
+ # Create a token that expires in 1 hour
31
+ token = create_access_token(data=user_data, expires_delta=timedelta(hours=1))
32
+
33
+ # Decode the token without verification to check expiry
34
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
35
+
36
+ assert decoded_payload["user_id"] == "expiry_test_user"
37
+ assert decoded_payload["role"] == "admin"
38
+
39
+ # Check that expiry is approximately 1 hour from now
40
+ import time
41
+ current_time = time.time()
42
+ exp_time = decoded_payload["exp"]
43
+ expected_exp = current_time + (60 * 60) # 1 hour in seconds
44
+
45
+ # Allow for a small time difference (max 10 seconds)
46
+ assert abs(exp_time - expected_exp) < 10
47
+
48
+
49
+ def test_verify_valid_token():
50
+ """Test that verifying a valid token returns the correct payload"""
51
+ user_data = {"user_id": "valid_user", "role": "user"}
52
+ token = create_access_token(data=user_data)
53
+
54
+ payload = verify_token(token)
55
+
56
+ assert payload is not None
57
+ assert payload["user_id"] == "valid_user"
58
+ assert payload["role"] == "user"
59
+
60
+
61
+ def test_verify_invalid_token():
62
+ """Test that verifying an invalid token returns None"""
63
+ invalid_token = "invalid.token.string"
64
+
65
+ payload = verify_token(invalid_token)
66
+
67
+ assert payload is None
68
+
69
+
70
+ def test_verify_expired_token():
71
+ """Test that verifying an expired token returns None"""
72
+ user_data = {"user_id": "expired_user", "role": "user"}
73
+
74
+ # Create a token that expires immediately
75
+ expired_token = create_access_token(data=user_data, expires_delta=timedelta(seconds=-1))
76
+
77
+ payload = verify_token(expired_token)
78
+
79
+ # The expired token should not be verified successfully
80
+ assert payload is None
81
+
82
+
83
+ def test_verify_token_with_different_secret():
84
+ """Test that verifying a token with a different secret returns None"""
85
+ user_data = {"user_id": "different_secret_user", "role": "user"}
86
+ token = create_access_token(data=user_data)
87
+
88
+ # Try to decode with a different secret - this should raise an exception
89
+ different_secret = "different_secret_key"
90
+ try:
91
+ payload = jwt.decode(token, different_secret, algorithms=[settings.JWT_ALGORITHM])
92
+ # If decoding succeeds with wrong secret, the test should fail
93
+ assert False, "Token should not be valid with different secret"
94
+ except Exception:
95
+ # This is expected - the token should not be valid with a different secret
96
+ pass
97
+
98
+
99
+ def test_token_contains_required_claims():
100
+ """Test that tokens contain all required claims"""
101
+ user_data = {
102
+ "user_id": "claims_test_user",
103
+ "role": "admin",
104
+ "email": "test@example.com"
105
+ }
106
+ token = create_access_token(data=user_data)
107
+
108
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
109
+
110
+ # Check that all original data is preserved
111
+ assert decoded_payload["user_id"] == "claims_test_user"
112
+ assert decoded_payload["role"] == "admin"
113
+ assert decoded_payload["email"] == "test@example.com"
114
+
115
+ # Check that expiration is added
116
+ assert "exp" in decoded_payload
117
+
118
+
119
+ def test_token_algorithm_compliance():
120
+ """Test that tokens are created and verified with the configured algorithm"""
121
+ user_data = {"user_id": "algorithm_test_user", "role": "user"}
122
+ token = create_access_token(data=user_data)
123
+
124
+ # Verify using the configured algorithm
125
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
126
+
127
+ assert decoded_payload["user_id"] == "algorithm_test_user"
128
+
129
+
130
+ def test_empty_user_data_token():
131
+ """Test that tokens can be created with minimal data"""
132
+ user_data = {} # Empty dictionary
133
+ token = create_access_token(data=user_data)
134
+
135
+ decoded_payload = verify_token(token)
136
+
137
+ # Should contain expiration but no other claims
138
+ assert decoded_payload is not None
139
+ assert "exp" in decoded_payload
140
+
141
+
142
+ def test_large_user_data_token():
143
+ """Test that tokens handle large user data payloads correctly"""
144
+ large_data = {
145
+ "user_id": "large_data_user",
146
+ "role": "user",
147
+ "profile": "x" * 1000, # Large string
148
+ "preferences": {
149
+ "theme": "dark",
150
+ "notifications": True,
151
+ "settings": list(range(100)) # Large list
152
+ }
153
+ }
154
+ token = create_access_token(data=large_data)
155
+
156
+ payload = verify_token(token)
157
+
158
+ assert payload is not None
159
+ assert payload["user_id"] == "large_data_user"
160
+ assert len(payload["profile"]) == 1000
161
+
162
+
163
+ def test_concurrent_token_creation():
164
+ """Test that multiple tokens can be created concurrently without issues"""
165
+ import concurrent.futures
166
+ import threading
167
+
168
+ results = []
169
+
170
+ def create_token_for_user(user_id):
171
+ user_data = {"user_id": user_id, "role": "user"}
172
+ token = create_access_token(data=user_data)
173
+ payload = verify_token(token)
174
+ return (user_id, token, payload)
175
+
176
+ # Create 5 tokens in parallel
177
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
178
+ futures = [
179
+ executor.submit(create_token_for_user, f"concurrent_user_{i}")
180
+ for i in range(5)
181
+ ]
182
+
183
+ for future in concurrent.futures.as_completed(futures):
184
+ user_id, token, payload = future.result()
185
+ results.append((user_id, token, payload))
186
+
187
+ # Verify all tokens were created and verified correctly
188
+ assert len(results) == 5
189
+ for user_id, token, payload in results:
190
+ assert token is not None
191
+ assert payload is not None
192
+ assert payload["user_id"] == user_id
193
+
194
+
195
+ def test_unicode_user_data():
196
+ """Test that tokens handle Unicode characters correctly"""
197
+ unicode_data = {
198
+ "user_id": "用户测试",
199
+ "name": "José María",
200
+ "role": "测试角色"
201
+ }
202
+ token = create_access_token(data=unicode_data)
203
+
204
+ payload = verify_token(token)
205
+
206
+ assert payload is not None
207
+ assert payload["user_id"] == "用户测试"
208
+ assert payload["name"] == "José María"
209
+ assert payload["role"] == "测试角色"
210
+
211
+
212
+ def test_token_security_best_practices():
213
+ """Test that tokens follow security best practices"""
214
+ user_data = {"user_id": "security_test_user", "role": "user"}
215
+ token = create_access_token(data=user_data)
216
+
217
+ # Verify token format (should be three parts separated by dots)
218
+ parts = token.split('.')
219
+ assert len(parts) == 3
220
+
221
+ # Verify that header contains expected algorithm
222
+ import base64
223
+ header_json = base64.b64decode(parts[0] + '==').decode('utf-8')
224
+ import json
225
+ header = json.loads(header_json)
226
+ assert header["alg"] == settings.JWT_ALGORITHM
227
+ assert header["typ"] == "JWT"
228
+
229
+
230
+ if __name__ == "__main__":
231
+ pytest.main([__file__])
tests/unit/test_auth/test_authentication_functions.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from datetime import timedelta
3
+ from jose import jwt
4
+ from backend.src.auth.security import create_access_token, verify_token
5
+ from backend.src.core.config import settings
6
+
7
+
8
+ def test_create_valid_jwt_token():
9
+ """Test that creating a JWT token works correctly"""
10
+ user_data = {"user_id": "test_user_123", "role": "user"}
11
+
12
+ token = create_access_token(data=user_data)
13
+
14
+ # Verify the token was created
15
+ assert token is not None
16
+ assert isinstance(token, str)
17
+ assert len(token) > 0
18
+
19
+ # Verify the token can be decoded
20
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
21
+ assert decoded_payload["user_id"] == "test_user_123"
22
+ assert decoded_payload["role"] == "user"
23
+ assert "exp" in decoded_payload
24
+
25
+
26
+ def test_create_token_with_custom_expiry():
27
+ """Test that creating a token with custom expiry works"""
28
+ user_data = {"user_id": "expiry_test_user", "role": "user"}
29
+ expiry_delta = timedelta(minutes=30)
30
+
31
+ token = create_access_token(data=user_data, expires_delta=expiry_delta)
32
+
33
+ # Decode the token without verification to check expiry
34
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
35
+
36
+ # Check that expiry is approximately 30 minutes from now
37
+ import time
38
+ current_time = time.time()
39
+ exp_time = decoded_payload["exp"]
40
+
41
+ # Should be approximately 30 minutes (1800 seconds) from now
42
+ assert abs(exp_time - current_time - 1800) < 10 # Allow 10 second tolerance
43
+
44
+
45
+ def test_verify_valid_token():
46
+ """Test that verifying a valid token returns the correct payload"""
47
+ user_data = {"user_id": "verify_test_user", "role": "admin"}
48
+ token = create_access_token(data=user_data)
49
+
50
+ payload = verify_token(token)
51
+
52
+ assert payload is not None
53
+ assert payload["user_id"] == "verify_test_user"
54
+ assert payload["role"] == "admin"
55
+
56
+
57
+ def test_verify_invalid_token():
58
+ """Test that verifying an invalid token returns None"""
59
+ invalid_token = "this.is.not.a.valid.jwt.token"
60
+
61
+ payload = verify_token(invalid_token)
62
+
63
+ assert payload is None
64
+
65
+
66
+ def test_verify_expired_token():
67
+ """Test that verifying an expired token returns None"""
68
+ user_data = {"user_id": "expired_test_user", "role": "user"}
69
+ # Create a token that expires immediately
70
+ token = create_access_token(data=user_data, expires_delta=timedelta(seconds=-1))
71
+
72
+ payload = verify_token(token)
73
+
74
+ assert payload is None
75
+
76
+
77
+ def test_verify_token_with_different_secret():
78
+ """Test that verifying a token with wrong secret returns None"""
79
+ user_data = {"user_id": "wrong_secret_user", "role": "user"}
80
+ token = create_access_token(data=user_data)
81
+
82
+ # Try to verify with a different secret
83
+ different_secret = "different_secret_key_that_does_not_match"
84
+ try:
85
+ payload = jwt.decode(token, different_secret, algorithms=[settings.JWT_ALGORITHM])
86
+ # If this doesn't raise an exception, the payload should be invalid
87
+ assert False, "Expected JWTError was not raised"
88
+ except jwt.JWTError:
89
+ # Expected behavior - token should not be valid with different secret
90
+ pass
91
+
92
+
93
+ def test_token_contains_correct_claims():
94
+ """Test that tokens contain the expected claims"""
95
+ user_data = {
96
+ "user_id": "claims_test_user",
97
+ "role": "tester",
98
+ "email": "test@example.com",
99
+ "custom_field": "custom_value"
100
+ }
101
+ token = create_access_token(data=user_data)
102
+
103
+ decoded_payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
104
+
105
+ # Check that all original data is preserved
106
+ assert decoded_payload["user_id"] == "claims_test_user"
107
+ assert decoded_payload["role"] == "tester"
108
+ assert decoded_payload["email"] == "test@example.com"
109
+ assert decoded_payload["custom_field"] == "custom_value"
110
+
111
+ # Check that expiration is added
112
+ assert "exp" in decoded_payload
113
+
114
+
115
+ def test_token_algorithm_compliance():
116
+ """Test that tokens are created and verified with the configured algorithm"""
117
+ user_data = {"user_id": "algorithm_test_user", "role": "user"}
118
+ token = create_access_token(data=user_data)
119
+
120
+ # Verify using the configured algorithm
121
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
122
+
123
+ assert payload["user_id"] == "algorithm_test_user"
124
+
125
+
126
+ def test_empty_user_data_token():
127
+ """Test creating and verifying a token with minimal data"""
128
+ user_data = {} # Empty dictionary
129
+ token = create_access_token(data=user_data)
130
+
131
+ payload = verify_token(token)
132
+
133
+ # Should contain expiration but no other claims
134
+ assert payload is not None
135
+ assert "exp" in payload
136
+
137
+
138
+ def test_large_user_data_token():
139
+ """Test creating and verifying a token with large data payload"""
140
+ large_data = {"user_id": "large_data_user", "data": "x" * 1000} # Large string
141
+ token = create_access_token(data=large_data)
142
+
143
+ payload = verify_token(token)
144
+
145
+ assert payload is not None
146
+ assert payload["user_id"] == "large_data_user"
147
+ assert len(payload["data"]) == 1000
148
+
149
+
150
+ def test_token_unicode_support():
151
+ """Test that tokens handle Unicode characters correctly"""
152
+ unicode_data = {"user_id": "用户测试", "name": "José María", "role": "测试角色"}
153
+ token = create_access_token(data=unicode_data)
154
+
155
+ payload = verify_token(token)
156
+
157
+ assert payload is not None
158
+ assert payload["user_id"] == "用户测试"
159
+ assert payload["name"] == "José María"
160
+ assert payload["role"] == "测试角色"
161
+
162
+
163
+ def test_concurrent_token_creation():
164
+ """Test that multiple tokens can be created concurrently without issues"""
165
+ import threading
166
+ import time
167
+
168
+ results = []
169
+
170
+ def create_token_and_store(index):
171
+ user_data = {"user_id": f"concurrent_user_{index}", "role": "user"}
172
+ token = create_access_token(data=user_data)
173
+ payload = verify_token(token)
174
+ results.append((index, token, payload))
175
+
176
+ # Create 5 tokens in parallel
177
+ threads = []
178
+ for i in range(5):
179
+ thread = threading.Thread(target=create_token_and_store, args=(i,))
180
+ threads.append(thread)
181
+ thread.start()
182
+
183
+ # Wait for all threads to complete
184
+ for thread in threads:
185
+ thread.join()
186
+
187
+ # Verify all tokens were created and verified correctly
188
+ assert len(results) == 5
189
+ for index, token, payload in results:
190
+ assert token is not None
191
+ assert payload is not None
192
+ assert payload["user_id"] == f"concurrent_user_{index}"
193
+
194
+
195
+ if __name__ == "__main__":
196
+ pytest.main([__file__])
tests/unit/test_models/test_task.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from datetime import datetime
3
+ from src.models.task import Task, TaskCreate, TaskUpdate, TaskResponse
4
+
5
+
6
+ def test_task_creation():
7
+ """Test creating a basic task"""
8
+ task_create = TaskCreate(
9
+ title="Test Task",
10
+ description="Test Description",
11
+ user_id="user123"
12
+ )
13
+
14
+ assert task_create.title == "Test Task"
15
+ assert task_create.description == "Test Description"
16
+ assert task_create.user_id == "user123"
17
+ assert task_create.completed is False # Default value
18
+
19
+
20
+ def test_task_with_completed_true():
21
+ """Test creating a task with completed=True"""
22
+ task_create = TaskCreate(
23
+ title="Completed Task",
24
+ description="A completed task",
25
+ completed=True,
26
+ user_id="user123"
27
+ )
28
+
29
+ assert task_create.completed is True
30
+
31
+
32
+ def test_task_minimal_fields():
33
+ """Test creating a task with minimal required fields"""
34
+ task_create = TaskCreate(
35
+ title="Minimal Task",
36
+ user_id="user123"
37
+ )
38
+
39
+ assert task_create.title == "Minimal Task"
40
+ assert task_create.user_id == "user123"
41
+ assert task_create.description is None
42
+ assert task_create.completed is False
43
+
44
+
45
+ def test_task_response_model():
46
+ """Test the TaskResponse model"""
47
+ task_response = TaskResponse(
48
+ id=1,
49
+ title="Response Task",
50
+ description="A task response",
51
+ completed=False,
52
+ user_id="user123",
53
+ created_at=datetime.now(),
54
+ updated_at=datetime.now()
55
+ )
56
+
57
+ assert task_response.id == 1
58
+ assert task_response.title == "Response Task"
59
+ assert task_response.description == "A task response"
60
+ assert task_response.completed is False
61
+ assert task_response.user_id == "user123"
62
+
63
+
64
+ def test_task_update_model():
65
+ """Test the TaskUpdate model"""
66
+ task_update = TaskUpdate(
67
+ title="Updated Title",
68
+ description="Updated Description",
69
+ completed=True
70
+ )
71
+
72
+ assert task_update.title == "Updated Title"
73
+ assert task_update.description == "Updated Description"
74
+ assert task_update.completed is True
75
+
76
+
77
+ def test_task_update_partial():
78
+ """Test partial updates in TaskUpdate model"""
79
+ task_update = TaskUpdate(title="Only Title Updated")
80
+
81
+ assert task_update.title == "Only Title Updated"
82
+ # Other fields should be None since they're optional
83
+ assert task_update.description is None
84
+ assert task_update.completed is None