auth service completed and tested
This commit is contained in:
parent
08b1815156
commit
486fadbfb3
|
|
@ -0,0 +1,37 @@
|
|||
"""
|
||||
FastAPI Application Entry Point
|
||||
|
||||
This module initializes and configures the FastAPI application with:
|
||||
- CORS middleware for cross-origin requests
|
||||
- Request timing middleware for performance monitoring
|
||||
- Custom exception handlers for consistent error responses
|
||||
- Prometheus instrumentation for metrics
|
||||
- API routers for endpoint organization
|
||||
"""
|
||||
|
||||
import uvicorn
|
||||
import routers
|
||||
|
||||
from create_file import create_app
|
||||
from prometheus_fastapi_instrumentator import Instrumentator
|
||||
from app_handler import setup_middleware, get_uvicorn_config
|
||||
|
||||
|
||||
print("Loading app.py module...")
|
||||
|
||||
# Initialize FastAPI application
|
||||
app = create_app(routers=routers)
|
||||
|
||||
# Setup Prometheus metrics
|
||||
Instrumentator().instrument(app=app).expose(app=app)
|
||||
|
||||
# Configure middleware and exception handlers
|
||||
setup_middleware(app)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting server from __main__...")
|
||||
# Run the application with Uvicorn
|
||||
uvicorn_config = get_uvicorn_config()
|
||||
print(f"Using config: {uvicorn_config}")
|
||||
uvicorn.Server(uvicorn.Config(**uvicorn_config)).run()
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
"""
|
||||
FastAPI Application Handler Module
|
||||
|
||||
This module contains all the handler functions for configuring and setting up the FastAPI application:
|
||||
- CORS middleware configuration
|
||||
- Exception handlers setup
|
||||
- Uvicorn server configuration
|
||||
"""
|
||||
|
||||
from typing import Dict, Any
|
||||
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi import FastAPI, Request, HTTPException, status
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from ErrorHandlers.bases import (
|
||||
BaseErrorModelClass,
|
||||
StatusesModelClass,
|
||||
LanguageModelClass,
|
||||
)
|
||||
from ErrorHandlers import statuses
|
||||
from middleware.auth_middleware import MiddlewareModule
|
||||
|
||||
|
||||
def setup_cors_middleware(app: FastAPI) -> None:
|
||||
"""
|
||||
Configure CORS middleware for the application.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
"""
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
||||
"""
|
||||
Handle HTTP exceptions and return formatted error responses.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
exc: HTTP exception instance
|
||||
|
||||
Returns:
|
||||
JSONResponse: Formatted error response
|
||||
"""
|
||||
error_code = getattr(exc, "error_code", None)
|
||||
if error_code:
|
||||
status_code = StatusesModelClass.retrieve_error_by_code(error_code)
|
||||
error_message = LanguageModelClass.retrieve_error_by_code(
|
||||
error_code, request.headers.get("accept-language", "en")
|
||||
)
|
||||
else:
|
||||
status_code = exc.status_code
|
||||
error_message = str(exc.detail)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status_code,
|
||||
content={"detail": error_message, "error_code": error_code},
|
||||
)
|
||||
|
||||
|
||||
async def generic_exception_handler(request: Request, exc: Exception) -> JSONResponse:
|
||||
"""
|
||||
Handle generic exceptions and return formatted error responses.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
exc: Exception instance
|
||||
|
||||
Returns:
|
||||
JSONResponse: Formatted error response
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
content={"detail": "Internal server error", "error_code": "INTERNAL_ERROR"},
|
||||
)
|
||||
|
||||
|
||||
def setup_exception_handlers(app: FastAPI) -> None:
|
||||
"""
|
||||
Configure custom exception handlers for the application.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
"""
|
||||
app.add_exception_handler(HTTPException, http_exception_handler)
|
||||
app.add_exception_handler(Exception, generic_exception_handler)
|
||||
|
||||
|
||||
def setup_middleware(app: FastAPI) -> None:
|
||||
"""
|
||||
Configure all middleware for the application.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
"""
|
||||
setup_cors_middleware(app)
|
||||
app.add_middleware(MiddlewareModule.RequestTimingMiddleware)
|
||||
setup_exception_handlers(app)
|
||||
|
||||
|
||||
def get_uvicorn_config() -> Dict[str, Any]:
|
||||
"""
|
||||
Get Uvicorn server configuration.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Uvicorn configuration dictionary
|
||||
"""
|
||||
return {
|
||||
"app": "app:app",
|
||||
"host": "0.0.0.0",
|
||||
"port": 41575,
|
||||
"log_level": "info",
|
||||
"reload": True,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
"""
|
||||
Base FastAPI application configuration.
|
||||
"""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(title="API Service")
|
||||
|
||||
# Configure CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
return app
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
"""
|
||||
FastAPI Application Factory Module
|
||||
|
||||
This module provides functionality to create and configure a FastAPI application with:
|
||||
- Custom OpenAPI schema configuration
|
||||
- Security scheme configuration for Bearer authentication
|
||||
- Automatic router registration
|
||||
- Response class configuration
|
||||
- Security requirements for protected endpoints
|
||||
"""
|
||||
from types import ModuleType
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from fastapi import FastAPI, APIRouter
|
||||
from fastapi.responses import JSONResponse, RedirectResponse
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from fastapi.routing import APIRoute
|
||||
from AllConfigs.main import MainConfig as Config
|
||||
from middleware.auth_middleware import MiddlewareModule
|
||||
|
||||
|
||||
def setup_security_schema() -> Dict[str, Any]:
|
||||
"""
|
||||
Configure security schema for the OpenAPI documentation.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Security schema configuration
|
||||
"""
|
||||
return {
|
||||
"Bearer": {
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT",
|
||||
"description": "Enter the token",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def configure_route_security(
|
||||
path: str,
|
||||
method: str,
|
||||
schema: Dict[str, Any],
|
||||
protected_paths: List[str]
|
||||
) -> None:
|
||||
"""
|
||||
Configure security requirements for a specific route.
|
||||
|
||||
Args:
|
||||
path: Route path
|
||||
method: HTTP method
|
||||
schema: OpenAPI schema to modify
|
||||
protected_paths: List of paths that require authentication
|
||||
"""
|
||||
if path in protected_paths:
|
||||
if "paths" not in schema:
|
||||
schema["paths"] = {}
|
||||
|
||||
if path not in schema["paths"]:
|
||||
schema["paths"][path] = {}
|
||||
|
||||
if method not in schema["paths"][path]:
|
||||
schema["paths"][path][method] = {}
|
||||
|
||||
schema["paths"][path][method]["security"] = [{"Bearer": []}]
|
||||
|
||||
|
||||
def get_routers(routers_module: ModuleType) -> List[APIRouter]:
|
||||
"""
|
||||
Extract all routers from the routers module.
|
||||
|
||||
Args:
|
||||
routers_module: Module containing router definitions
|
||||
|
||||
Returns:
|
||||
List[APIRouter]: List of router instances
|
||||
"""
|
||||
routers = []
|
||||
for attr_name in dir(routers_module):
|
||||
attr = getattr(routers_module, attr_name)
|
||||
if isinstance(attr, APIRouter):
|
||||
routers.append(attr)
|
||||
return routers
|
||||
|
||||
|
||||
def create_app(routers: ModuleType) -> FastAPI:
|
||||
"""
|
||||
Create and configure a FastAPI application.
|
||||
|
||||
Args:
|
||||
routers: Module containing router definitions
|
||||
|
||||
Returns:
|
||||
FastAPI: Configured FastAPI application instance
|
||||
"""
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(
|
||||
title=Config.TITLE,
|
||||
description=Config.DESCRIPTION,
|
||||
default_response_class=JSONResponse,
|
||||
)
|
||||
|
||||
# Add home route that redirects to API documentation
|
||||
@app.get("/", include_in_schema=False, summary=str(Config.DESCRIPTION))
|
||||
async def home() -> RedirectResponse:
|
||||
"""Redirect root path to API documentation."""
|
||||
return RedirectResponse(url="/docs")
|
||||
|
||||
# Get all routers
|
||||
router_instances = get_routers(routers)
|
||||
|
||||
# Find protected paths
|
||||
protected_paths = []
|
||||
for router in router_instances:
|
||||
for route in router.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
# Check if the route has auth_required decorator
|
||||
if any(d.__name__ == 'auth_required' for d in route.dependencies):
|
||||
protected_paths.append(route.path)
|
||||
|
||||
# Include routers
|
||||
for router in router_instances:
|
||||
app.include_router(router)
|
||||
|
||||
# Configure custom OpenAPI schema
|
||||
def custom_openapi():
|
||||
if app.openapi_schema:
|
||||
return app.openapi_schema
|
||||
|
||||
openapi_schema = get_openapi(
|
||||
title=Config.TITLE,
|
||||
version="1.0.0",
|
||||
description=Config.DESCRIPTION,
|
||||
routes=app.routes,
|
||||
)
|
||||
|
||||
# Add security schemes
|
||||
openapi_schema["components"] = {"securitySchemes": setup_security_schema()}
|
||||
|
||||
# Configure security for each route
|
||||
for route in app.routes:
|
||||
if isinstance(route, APIRoute):
|
||||
configure_route_security(
|
||||
route.path,
|
||||
route.methods.pop().lower(),
|
||||
openapi_schema,
|
||||
protected_paths
|
||||
)
|
||||
|
||||
app.openapi_schema = openapi_schema
|
||||
return app.openapi_schema
|
||||
|
||||
app.openapi = custom_openapi
|
||||
return app
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
"""
|
||||
Authentication and Authorization middleware for FastAPI applications.
|
||||
|
||||
This module provides authentication decorator for protecting endpoints
|
||||
and a middleware for request timing measurements.
|
||||
"""
|
||||
|
||||
from time import perf_counter
|
||||
from typing import Callable, Optional, Dict, Any, Tuple
|
||||
from functools import wraps
|
||||
from fastapi import HTTPException, Request, Response, status
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from AllConfigs.Token.config import Auth
|
||||
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
||||
|
||||
|
||||
class MiddlewareModule:
|
||||
"""
|
||||
Module containing authentication and middleware functionality.
|
||||
|
||||
This class provides:
|
||||
- Token extraction and validation
|
||||
- Authentication decorator for endpoints
|
||||
- Request timing middleware
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_access_token(request: Request) -> Tuple[str, str]:
|
||||
"""
|
||||
Extract access token from request headers.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
|
||||
Returns:
|
||||
Tuple[str, str]: A tuple containing (scheme, token)
|
||||
|
||||
Raises:
|
||||
HTTPExceptionApi: If token is missing or malformed
|
||||
"""
|
||||
auth_header = request.headers.get(Auth.ACCESS_TOKEN_TAG)
|
||||
if not auth_header:
|
||||
raise HTTPExceptionApi(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="No authorization header",
|
||||
)
|
||||
|
||||
try:
|
||||
scheme, token = auth_header.split()
|
||||
if scheme.lower() != "bearer":
|
||||
raise HTTPExceptionApi(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid authentication scheme",
|
||||
)
|
||||
return scheme, token
|
||||
except ValueError:
|
||||
raise HTTPExceptionApi(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token format"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def validate_token(token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Validate the authentication token.
|
||||
|
||||
Args:
|
||||
token: JWT token to validate
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: User data extracted from token
|
||||
|
||||
Raises:
|
||||
HTTPExceptionApi: If token is invalid
|
||||
"""
|
||||
try:
|
||||
# TODO: Implement your token validation logic
|
||||
# Example:
|
||||
# return jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
|
||||
return {"user_id": "test", "role": "user"} # Placeholder
|
||||
except Exception as e:
|
||||
raise HTTPExceptionApi(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Token validation failed: {str(e)}",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def auth_required(cls, func: Callable) -> Callable:
|
||||
"""
|
||||
Decorator for protecting FastAPI endpoints with authentication.
|
||||
|
||||
Usage:
|
||||
@router.get("/protected")
|
||||
@MiddlewareModule.auth_required
|
||||
async def protected_endpoint(request: Request):
|
||||
user = request.state.user # Access authenticated user data
|
||||
return {"message": "Protected content"}
|
||||
|
||||
@router.get("/public") # No decorator = public endpoint
|
||||
async def public_endpoint():
|
||||
return {"message": "Public content"}
|
||||
|
||||
Args:
|
||||
func: The FastAPI route handler function to protect
|
||||
|
||||
Returns:
|
||||
Callable: Wrapped function that checks authentication before execution
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(request: Request, *args, **kwargs):
|
||||
try:
|
||||
# Get token from header
|
||||
_, token = cls.get_access_token(request)
|
||||
|
||||
# Validate token and get user data
|
||||
token_data = await cls.validate_token(token)
|
||||
|
||||
# Add user data to request state for use in endpoint
|
||||
request.state.user = token_data
|
||||
|
||||
# Call the original endpoint function
|
||||
return await func(request, *args, **kwargs)
|
||||
|
||||
except HTTPExceptionApi:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPExceptionApi(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=f"Authentication failed: {str(e)}",
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
class RequestTimingMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
Middleware for measuring and logging request timing.
|
||||
Only handles timing, no authentication.
|
||||
"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
||||
"""
|
||||
Process each request through the middleware.
|
||||
|
||||
Args:
|
||||
request: FastAPI request object
|
||||
call_next: Next middleware in the chain
|
||||
|
||||
Returns:
|
||||
Response: Processed response with timing headers
|
||||
"""
|
||||
start_time = perf_counter()
|
||||
|
||||
# Process the request
|
||||
response = await call_next(request)
|
||||
|
||||
# Add timing information to response headers
|
||||
self._add_timing_headers(response, start_time)
|
||||
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _add_timing_headers(response: Response, start_time: float) -> None:
|
||||
"""
|
||||
Add request timing information to response headers.
|
||||
|
||||
Args:
|
||||
response: FastAPI response object
|
||||
start_time: Time when request processing started
|
||||
"""
|
||||
end_time = perf_counter()
|
||||
elapsed = (end_time - start_time) * 1000 # Convert to milliseconds
|
||||
|
||||
response.headers.update(
|
||||
{
|
||||
"request-start": f"{start_time:.6f}",
|
||||
"request-end": f"{end_time:.6f}",
|
||||
"request-duration": f"{elapsed:.2f}ms",
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
"""
|
||||
OpenAPI Schema Creator Module
|
||||
|
||||
This module provides functionality to create and customize OpenAPI documentation:
|
||||
- Custom security schemes (Bearer Auth, API Key)
|
||||
- Response schemas and examples
|
||||
- Tag management and descriptions
|
||||
- Error responses and validation
|
||||
- Custom documentation extensions
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Set
|
||||
from fastapi import FastAPI, APIRouter
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
from AllConfigs.main import MainConfig as Config
|
||||
|
||||
|
||||
class OpenAPISchemaCreator:
|
||||
"""
|
||||
OpenAPI schema creator and customizer for FastAPI applications.
|
||||
"""
|
||||
|
||||
def __init__(self, app: FastAPI):
|
||||
"""
|
||||
Initialize the OpenAPI schema creator.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
"""
|
||||
self.app = app
|
||||
self.protected_paths: Set[str] = set()
|
||||
self.tags_metadata = self._create_tags_metadata()
|
||||
|
||||
@staticmethod
|
||||
def _create_tags_metadata() -> List[Dict[str, str]]:
|
||||
"""
|
||||
Create metadata for API tags.
|
||||
|
||||
Returns:
|
||||
List[Dict[str, str]]: List of tag metadata
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"name": "Authentication",
|
||||
"description": "Operations related to user authentication and authorization",
|
||||
},
|
||||
{
|
||||
"name": "Users",
|
||||
"description": "User management and profile operations",
|
||||
},
|
||||
# Add more tags as needed
|
||||
]
|
||||
|
||||
def _create_security_schemes(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Create security scheme definitions.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Security scheme configurations
|
||||
"""
|
||||
return {
|
||||
"Bearer Auth": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "evyos-session-key",
|
||||
"description": "Enter: **'Bearer <JWT>'**, where JWT is the access token",
|
||||
},
|
||||
"API Key": {
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-API-Key",
|
||||
"description": "API key for service authentication",
|
||||
},
|
||||
}
|
||||
|
||||
def _create_common_responses(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Create common response schemas.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Common response configurations
|
||||
"""
|
||||
return {
|
||||
"401": {
|
||||
"description": "Unauthorized - Authentication failed or not provided",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {"type": "string"},
|
||||
"error_code": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"example": {
|
||||
"detail": "Invalid authentication credentials",
|
||||
"error_code": "INVALID_CREDENTIALS",
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden - Insufficient permissions",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HTTPValidationError"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {"$ref": "#/components/schemas/HTTPValidationError"}
|
||||
}
|
||||
},
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"detail": {"type": "string"},
|
||||
"error_code": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"example": {
|
||||
"detail": "Internal server error occurred",
|
||||
"error_code": "INTERNAL_ERROR",
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def configure_route_security(
|
||||
self, path: str, method: str, schema: Dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Configure security requirements for a specific route.
|
||||
|
||||
Args:
|
||||
path: Route path
|
||||
method: HTTP method
|
||||
schema: OpenAPI schema to modify
|
||||
"""
|
||||
if path not in Config.INSECURE_PATHS:
|
||||
schema["paths"][path][method]["security"] = [
|
||||
{"Bearer Auth": []},
|
||||
{"API Key": []},
|
||||
]
|
||||
schema["paths"][path][method]["responses"].update(
|
||||
self._create_common_responses()
|
||||
)
|
||||
|
||||
def create_schema(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Create the complete OpenAPI schema.
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Complete OpenAPI schema
|
||||
"""
|
||||
openapi_schema = get_openapi(
|
||||
title=Config.TITLE,
|
||||
description=Config.DESCRIPTION,
|
||||
version="1.0.0",
|
||||
routes=self.app.routes,
|
||||
tags=self.tags_metadata,
|
||||
)
|
||||
|
||||
# Add security schemes
|
||||
if "components" not in openapi_schema:
|
||||
openapi_schema["components"] = {}
|
||||
|
||||
openapi_schema["components"]["securitySchemes"] = self._create_security_schemes()
|
||||
|
||||
# Configure route security and responses
|
||||
for route in self.app.routes:
|
||||
if isinstance(route, APIRoute) and route.include_in_schema:
|
||||
path = str(route.path)
|
||||
methods = [method.lower() for method in route.methods]
|
||||
|
||||
for method in methods:
|
||||
self.configure_route_security(path, method, openapi_schema)
|
||||
|
||||
# Add custom documentation extensions
|
||||
openapi_schema["x-documentation"] = {
|
||||
"postman_collection": "/docs/postman",
|
||||
"swagger_ui": "/docs",
|
||||
"redoc": "/redoc",
|
||||
}
|
||||
|
||||
return openapi_schema
|
||||
|
||||
|
||||
def create_openapi_schema(app: FastAPI) -> Dict[str, Any]:
|
||||
"""
|
||||
Create OpenAPI schema for a FastAPI application.
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: Complete OpenAPI schema
|
||||
"""
|
||||
creator = OpenAPISchemaCreator(app)
|
||||
return creator.create_schema()
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
from .base_router import test_route
|
||||
|
||||
__all__ = [
|
||||
"test_route"
|
||||
]
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
"""
|
||||
Base router configuration and setup.
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from middleware.auth_middleware import MiddlewareModule
|
||||
|
||||
# Create test router
|
||||
test_route = APIRouter(prefix="/test", tags=["Test"])
|
||||
|
||||
@test_route.get("/health")
|
||||
@MiddlewareModule.auth_required
|
||||
async def health_check(request: Request):
|
||||
return {"status": "healthy", "message": "Service is running"}
|
||||
|
||||
@test_route.get("/ping")
|
||||
async def ping_test():
|
||||
return {"ping": "pong", "service": "base-router"}
|
||||
|
||||
# Initialize and include test routes
|
||||
def init_test_routes():
|
||||
return test_route
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy all required directories
|
||||
COPY DockerApiServices/AllApiNeeds /app/
|
||||
COPY ApiLibrary /app/ApiLibrary
|
||||
COPY ApiValidations /app/ApiValidations
|
||||
COPY AllConfigs /app/AllConfigs
|
||||
COPY ErrorHandlers /app/ErrorHandlers
|
||||
COPY Schemas /app/Schemas
|
||||
COPY Services /app/Services
|
||||
|
||||
# Install Python dependencies
|
||||
COPY DockerApiServices/requirements.txt /app/
|
||||
RUN pip install --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Set Python path to include app directory
|
||||
ENV PYTHONPATH=/app
|
||||
|
||||
# Run the application using the configured uvicorn server
|
||||
CMD ["python", "app.py"]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
fastapi==0.104.1
|
||||
uvicorn==0.24.0.post1
|
||||
pydantic==2.10.5
|
||||
sqlalchemy==2.0.37
|
||||
psycopg2-binary==2.9.10
|
||||
python-dateutil==2.9.0.post0
|
||||
motor==3.3.2
|
||||
redis==5.2.1
|
||||
pytest==7.4.4
|
||||
pytest-asyncio==0.21.2
|
||||
pytest-cov==4.1.0
|
||||
coverage==7.6.10
|
||||
arrow==1.3.0
|
||||
redmail==0.6.0
|
||||
sqlalchemy-mixins==2.0.5
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY pyproject.toml .
|
||||
RUN pip install poetry && \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install --no-dev
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application
|
||||
CMD ["uvicorn", "EventServiceApi.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
# Docker Services Guide
|
||||
|
||||
This repository contains multiple microservices that can be run using Docker Compose.
|
||||
|
||||
## Quick Start (With Cache)
|
||||
For regular development when dependencies haven't changed:
|
||||
```bash
|
||||
# Build and run Auth Service
|
||||
docker compose -f ../docker-compose-services.yml up auth-service
|
||||
|
||||
# Build and run Event Service
|
||||
docker compose -f ../docker-compose-services.yml up event-service
|
||||
|
||||
# Build and run Validation Service
|
||||
docker compose -f ../docker-compose-services.yml up validation-service
|
||||
|
||||
# Build and run all services
|
||||
docker compose -f ../docker-compose-services.yml up
|
||||
```
|
||||
|
||||
## Clean Build (No Cache)
|
||||
Use these commands when changing Dockerfile or dependencies:
|
||||
```bash
|
||||
# Auth Service
|
||||
docker compose -f ../docker-compose-services.yml build --no-cache auth-service && docker compose -f ../docker-compose-services.yml up auth-service
|
||||
|
||||
# Event Service
|
||||
docker compose -f ../docker-compose-services.yml build --no-cache event-service && docker compose -f ../docker-compose-services.yml up event-service
|
||||
|
||||
# Validation Service
|
||||
docker compose -f ../docker-compose-services.yml build --no-cache validation-service && docker compose -f ../docker-compose-services.yml up validation-service
|
||||
|
||||
# All Services
|
||||
docker compose -f ../docker-compose-services.yml build --no-cache && docker compose -f ../docker-compose-services.yml up
|
||||
```
|
||||
|
||||
## Service Ports
|
||||
- Auth Service: `http://localhost:8000`
|
||||
- `/test/health` - Protected health check endpoint (requires authentication)
|
||||
- `/test/ping` - Public ping endpoint
|
||||
- Event Service: `http://localhost:8001`
|
||||
- Validation Service: `http://localhost:8002`
|
||||
|
||||
## Development Notes
|
||||
- Use clean build (--no-cache) when:
|
||||
- Changing Dockerfile
|
||||
- Updating dependencies
|
||||
- Experiencing caching issues
|
||||
- Use regular build (with cache) when:
|
||||
- Only changing application code
|
||||
- For faster development iterations
|
||||
- Run in detached mode:
|
||||
```bash
|
||||
docker compose -f ../docker-compose-services.yml up -d auth-service
|
||||
```
|
||||
- Stop services:
|
||||
```bash
|
||||
docker compose -f ../docker-compose-services.yml down
|
||||
```
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
COPY pyproject.toml .
|
||||
RUN pip install poetry && \
|
||||
poetry config virtualenvs.create false && \
|
||||
poetry install --no-dev
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8000
|
||||
|
||||
# Run the application
|
||||
CMD ["uvicorn", "ValidationServiceApi.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
[tool.poetry]
|
||||
name = "wag-management-api-services"
|
||||
version = "0.1.1"
|
||||
description = "WAG Management API Service"
|
||||
authors = ["Karatay Berkay <karatay.berkay@evyos.com.tr>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
# FastAPI and Web
|
||||
fastapi = "^0.104.1"
|
||||
uvicorn = "^0.24.0"
|
||||
pydantic = "^2.5.2"
|
||||
|
||||
# MongoDB
|
||||
motor = "3.3.2" # Pinned version
|
||||
pymongo = "4.5.0" # Pinned version to match motor
|
||||
|
||||
# PostgreSQL
|
||||
sqlalchemy = "^2.0.23"
|
||||
sqlalchemy-mixins = "^2.0.5"
|
||||
psycopg2-binary = "^2.9.9"
|
||||
|
||||
# Redis
|
||||
redis = "^5.0.1"
|
||||
arrow = "^1.3.0"
|
||||
|
||||
# Email
|
||||
redmail = "^0.6.0"
|
||||
|
||||
# Testing
|
||||
pytest = "^7.4.3"
|
||||
pytest-asyncio = "^0.21.1"
|
||||
pytest-cov = "^4.1.0"
|
||||
|
||||
# Utilities
|
||||
python-dateutil = "^2.8.2"
|
||||
typing-extensions = "^4.8.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^23.11.0"
|
||||
isort = "^5.12.0"
|
||||
mypy = "^1.7.1"
|
||||
flake8 = "^6.1.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py39']
|
||||
include = '\.pyi?$'
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = true
|
||||
force_grid_wrap = 0
|
||||
use_parentheses = true
|
||||
line_length = 88
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.9"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
check_untyped_defs = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "6.0"
|
||||
addopts = "-ra -q --cov=Services"
|
||||
testpaths = [
|
||||
"Ztest",
|
||||
]
|
||||
python_files = ["test_*.py"]
|
||||
asyncio_mode = "auto"
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
fastapi==0.104.1
|
||||
uvicorn==0.24.0.post1
|
||||
pydantic==2.10.5
|
||||
sqlalchemy==2.0.37
|
||||
psycopg2-binary==2.9.10
|
||||
python-dateutil==2.9.0.post0
|
||||
motor==3.3.2
|
||||
redis==5.2.1
|
||||
pytest==7.4.4
|
||||
pytest-asyncio==0.21.2
|
||||
pytest-cov==4.1.0
|
||||
coverage==7.6.10
|
||||
arrow==1.3.0
|
||||
redmail==0.6.0
|
||||
sqlalchemy-mixins==2.0.5
|
||||
prometheus-client==0.19.0
|
||||
prometheus-fastapi-instrumentator==6.1.0
|
||||
|
|
@ -1,12 +1,29 @@
|
|||
What to do with services?
|
||||
WAG Management API Microservices Setup
|
||||
|
||||
*
|
||||
1. Authentication Service (Port 8000)
|
||||
- User authentication and authorization
|
||||
- JWT token management
|
||||
- Role-based access control
|
||||
- Uses PostgreSQL for user data
|
||||
|
||||
*
|
||||
2. Event Service (Port 8001)
|
||||
- Event processing and handling
|
||||
- Message queue integration
|
||||
- Real-time notifications
|
||||
- Uses MongoDB for event storage
|
||||
|
||||
*
|
||||
3. Validation Service (Port 8002)
|
||||
- Request validation
|
||||
- Data sanitization
|
||||
- Schema validation
|
||||
- Uses Redis for caching
|
||||
|
||||
*
|
||||
|
||||
*
|
||||
To run the services:
|
||||
```bash
|
||||
docker compose up --build
|
||||
```
|
||||
|
||||
Access services at:
|
||||
- Auth Service: http://localhost:8000
|
||||
- Event Service: http://localhost:8001
|
||||
- Validation Service: http://localhost:8002
|
||||
|
|
|
|||
85
README.md
85
README.md
|
|
@ -1,3 +1,84 @@
|
|||
# wag-managment-api-service-version-4
|
||||
# WAG Management API Service v4
|
||||
|
||||
wag managment api service version 4
|
||||
This service provides a comprehensive API for managing WAG (Wide Area Gateway) systems. It handles configuration, monitoring, and control operations for WAG devices in the network infrastructure.
|
||||
|
||||
## Quick Start
|
||||
|
||||
To run the tests using Docker Compose:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.test.yml up --build
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Core Services and Components
|
||||
|
||||
- `Services/` - Core service implementations
|
||||
- `PostgresDb/` - PostgreSQL database operations and models
|
||||
- `MongoDb/` - MongoDB operations and document models
|
||||
- `Redis/` - Redis caching and session management
|
||||
- `Email/` - Email notification service
|
||||
|
||||
- `ApiValidations/` - Request validation and data sanitization
|
||||
- Input validation rules
|
||||
- Data sanitization filters
|
||||
- Schema validation middleware
|
||||
|
||||
- `ApiLibrary/` - Common utilities and helper functions
|
||||
- Shared functions and utilities
|
||||
- Common constants and configurations
|
||||
- Helper classes and decorators
|
||||
|
||||
### Configuration and Settings
|
||||
|
||||
- `AllConfigs/` - Configuration management
|
||||
- Database configurations
|
||||
- Service settings
|
||||
- Environment-specific configs
|
||||
|
||||
- `Schemas/` - Data models and schema definitions
|
||||
- Request/Response models
|
||||
- Database schemas
|
||||
- API contract definitions
|
||||
|
||||
### Docker and Deployment
|
||||
|
||||
- `DockerApiServices/` - API service Docker configurations
|
||||
- API service Dockerfile
|
||||
- Service dependencies
|
||||
|
||||
- `DockerStoreServices/` - Storage service Docker configurations
|
||||
- Database service Dockerfiles
|
||||
- Storage service dependencies
|
||||
|
||||
### Error Handling and Events
|
||||
|
||||
- `ErrorHandlers/` - Error handling and exception management
|
||||
- Custom exceptions
|
||||
- Error handlers
|
||||
- Exception middleware
|
||||
|
||||
- `ApiEvents/` - Event handling and processing
|
||||
- Event listeners
|
||||
- Event dispatchers
|
||||
- Message queue handlers
|
||||
|
||||
### Language and Testing
|
||||
|
||||
- `LanguageModels/` - Localization and language support
|
||||
- Language files
|
||||
- Translation models
|
||||
- i18n configurations
|
||||
|
||||
- `Ztest/` - Test suite
|
||||
- Unit tests
|
||||
- Integration tests
|
||||
- Test fixtures and utilities
|
||||
|
||||
### Additional Components
|
||||
|
||||
- `scripts/` - Utility scripts and tools
|
||||
- Deployment scripts
|
||||
- Database migrations
|
||||
- Maintenance utilities
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ from ApiLibrary import system_arrow
|
|||
|
||||
class PyObjectId(ObjectId):
|
||||
"""Custom type for handling MongoDB ObjectId in Pydantic models."""
|
||||
|
||||
|
||||
@classmethod
|
||||
def __get_pydantic_core_schema__(
|
||||
cls,
|
||||
|
|
@ -21,17 +21,21 @@ class PyObjectId(ObjectId):
|
|||
"""Define the core schema for PyObjectId."""
|
||||
return core_schema.json_or_python_schema(
|
||||
json_schema=core_schema.str_schema(),
|
||||
python_schema=core_schema.union_schema([
|
||||
core_schema.is_instance_schema(ObjectId),
|
||||
core_schema.chain_schema([
|
||||
core_schema.str_schema(),
|
||||
core_schema.no_info_plain_validator_function(cls.validate),
|
||||
]),
|
||||
]),
|
||||
python_schema=core_schema.union_schema(
|
||||
[
|
||||
core_schema.is_instance_schema(ObjectId),
|
||||
core_schema.chain_schema(
|
||||
[
|
||||
core_schema.str_schema(),
|
||||
core_schema.no_info_plain_validator_function(cls.validate),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||
lambda x: str(x),
|
||||
return_schema=core_schema.str_schema(),
|
||||
when_used='json',
|
||||
when_used="json",
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -54,14 +58,14 @@ class PyObjectId(ObjectId):
|
|||
|
||||
class MongoBaseModel(BaseModel):
|
||||
"""Base model for all MongoDB documents."""
|
||||
|
||||
|
||||
model_config = ConfigDict(
|
||||
arbitrary_types_allowed=True,
|
||||
json_encoders={ObjectId: str},
|
||||
populate_by_name=True,
|
||||
from_attributes=True,
|
||||
validate_assignment=True,
|
||||
extra='allow'
|
||||
extra="allow",
|
||||
)
|
||||
|
||||
# Optional _id field that will be ignored in create operations
|
||||
|
|
@ -69,11 +73,11 @@ class MongoBaseModel(BaseModel):
|
|||
|
||||
def get_extra(self, field_name: str, default: Any = None) -> Any:
|
||||
"""Safely get extra field value.
|
||||
|
||||
|
||||
Args:
|
||||
field_name: Name of the extra field to retrieve
|
||||
default: Default value to return if field doesn't exist
|
||||
|
||||
|
||||
Returns:
|
||||
Value of the extra field if it exists, otherwise the default value
|
||||
"""
|
||||
|
|
@ -81,7 +85,7 @@ class MongoBaseModel(BaseModel):
|
|||
|
||||
def as_dict(self) -> Dict[str, Any]:
|
||||
"""Convert model to dictionary including all fields and extra fields.
|
||||
|
||||
|
||||
Returns:
|
||||
Dict containing all model fields and extra fields with proper type conversion
|
||||
"""
|
||||
|
|
@ -94,18 +98,18 @@ class MongoDocument(MongoBaseModel):
|
|||
created_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
|
||||
updated_at: float = Field(default_factory=lambda: system_arrow.now().timestamp())
|
||||
|
||||
@model_validator(mode='before')
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def prevent_protected_fields(cls, data: Any) -> Any:
|
||||
"""Prevent user from setting protected fields like _id and timestamps."""
|
||||
if isinstance(data, dict):
|
||||
# Remove protected fields from input
|
||||
data.pop('_id', None)
|
||||
data.pop('created_at', None)
|
||||
data.pop('updated_at', None)
|
||||
|
||||
data.pop("_id", None)
|
||||
data.pop("created_at", None)
|
||||
data.pop("updated_at", None)
|
||||
|
||||
# Set timestamps
|
||||
data['created_at'] = system_arrow.now().timestamp()
|
||||
data['updated_at'] = system_arrow.now().timestamp()
|
||||
|
||||
data["created_at"] = system_arrow.now().timestamp()
|
||||
data["updated_at"] = system_arrow.now().timestamp()
|
||||
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ including domain history and access details.
|
|||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
from pydantic import Field, model_validator
|
||||
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
||||
|
||||
from ApiLibrary import system_arrow
|
||||
from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocument
|
||||
|
|
@ -15,40 +15,38 @@ from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocu
|
|||
|
||||
class DomainData(MongoBaseModel):
|
||||
"""Model for domain data.
|
||||
|
||||
|
||||
Attributes:
|
||||
user_uu_id: Unique identifier of the user
|
||||
main_domain: Primary domain
|
||||
other_domains_list: List of additional domains
|
||||
extra_data: Additional domain-related data
|
||||
"""
|
||||
|
||||
|
||||
user_uu_id: str = Field(..., description="User's unique identifier")
|
||||
main_domain: str = Field(..., description="Primary domain")
|
||||
other_domains_list: List[str] = Field(
|
||||
default_factory=list,
|
||||
description="List of additional domains"
|
||||
default_factory=list, description="List of additional domains"
|
||||
)
|
||||
extra_data: Optional[Dict[str, Any]] = Field(
|
||||
default_factory=dict,
|
||||
alias="extraData",
|
||||
description="Additional domain-related data"
|
||||
description="Additional domain-related data",
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
populate_by_name = True
|
||||
validate_assignment = True
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||
)
|
||||
|
||||
|
||||
class DomainDocument(MongoDocument):
|
||||
"""Model for domain-related documents."""
|
||||
|
||||
|
||||
data: DomainData = Field(..., description="Domain data")
|
||||
|
||||
|
||||
def update_main_domain(self, new_domain: str) -> None:
|
||||
"""Update the main domain and move current to history.
|
||||
|
||||
|
||||
Args:
|
||||
new_domain: New main domain to set
|
||||
"""
|
||||
|
|
@ -60,19 +58,19 @@ class DomainDocument(MongoDocument):
|
|||
|
||||
class DomainDocumentCreate(MongoDocument):
|
||||
"""Model for creating new domain documents."""
|
||||
|
||||
data: DomainData = Field(..., description="Initial domain data")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
populate_by_name = True
|
||||
validate_assignment = True
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||
)
|
||||
|
||||
|
||||
class DomainDocumentUpdate(MongoDocument):
|
||||
"""Model for updating existing domain documents."""
|
||||
|
||||
data: DomainData = Field(..., description="Updated domain data")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
populate_by_name = True
|
||||
validate_assignment = True
|
||||
model_config = ConfigDict(
|
||||
from_attributes=True, populate_by_name=True, validate_assignment=True
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from Services.MongoDb.Models.action_models.base import MongoBaseModel, MongoDocu
|
|||
|
||||
class PasswordHistoryDetail(MongoBaseModel):
|
||||
"""Model for password history details."""
|
||||
|
||||
|
||||
timestamp: datetime
|
||||
ip_address: Optional[str] = Field(None, alias="ipAddress")
|
||||
user_agent: Optional[str] = Field(None, alias="userAgent")
|
||||
|
|
@ -24,27 +24,26 @@ class PasswordHistoryDetail(MongoBaseModel):
|
|||
|
||||
class PasswordHistoryData(MongoBaseModel):
|
||||
"""Model for password history data."""
|
||||
|
||||
|
||||
password_history: List[str] = Field([], alias="passwordHistory")
|
||||
access_history_detail: Dict[str, PasswordHistoryDetail] = Field(
|
||||
default_factory=dict,
|
||||
alias="accessHistoryDetail"
|
||||
default_factory=dict, alias="accessHistoryDetail"
|
||||
)
|
||||
|
||||
|
||||
class PasswordDocument(MongoDocument):
|
||||
"""Model for password-related documents."""
|
||||
|
||||
|
||||
data: PasswordHistoryData
|
||||
|
||||
|
||||
class PasswordDocumentCreate(MongoBaseModel):
|
||||
"""Model for creating new password documents."""
|
||||
|
||||
|
||||
data: PasswordHistoryData = Field(..., description="Initial password data")
|
||||
|
||||
|
||||
class PasswordDocumentUpdate(MongoBaseModel):
|
||||
"""Model for updating existing password documents."""
|
||||
|
||||
|
||||
data: PasswordHistoryData
|
||||
|
|
|
|||
|
|
@ -30,23 +30,19 @@ class MongoActions(
|
|||
MongoInsertMixin,
|
||||
MongoFindMixin,
|
||||
MongoDeleteMixin,
|
||||
MongoAggregateMixin
|
||||
MongoAggregateMixin,
|
||||
):
|
||||
"""Main MongoDB actions class that inherits all CRUD operation mixins.
|
||||
|
||||
|
||||
This class provides a unified interface for all MongoDB operations while
|
||||
managing collections based on company UUID and storage reason.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client: MongoClient,
|
||||
database: str,
|
||||
company_uuid: str,
|
||||
storage_reason: str
|
||||
self, client: MongoClient, database: str, company_uuid: str, storage_reason: str
|
||||
):
|
||||
"""Initialize MongoDB actions with client and collection info.
|
||||
|
||||
|
||||
Args:
|
||||
client: MongoDB client
|
||||
database: Database name to use
|
||||
|
|
@ -62,7 +58,7 @@ class MongoActions(
|
|||
|
||||
def use_collection(self, storage_reason: str) -> None:
|
||||
"""Switch to a different collection.
|
||||
|
||||
|
||||
Args:
|
||||
storage_reason: New storage reason for collection naming
|
||||
"""
|
||||
|
|
@ -82,7 +78,9 @@ class MongoActions(
|
|||
"""Insert multiple documents."""
|
||||
return super().insert_many(self.collection, documents)
|
||||
|
||||
def find_one(self, filter_query: Dict[str, Any], projection: Optional[Dict[str, Any]] = None):
|
||||
def find_one(
|
||||
self, filter_query: Dict[str, Any], projection: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
"""Find a single document."""
|
||||
return super().find_one(self.collection, filter_query, projection)
|
||||
|
||||
|
|
@ -96,12 +94,7 @@ class MongoActions(
|
|||
):
|
||||
"""Find multiple documents."""
|
||||
return super().find_many(
|
||||
self.collection,
|
||||
filter_query,
|
||||
projection,
|
||||
sort,
|
||||
limit,
|
||||
skip
|
||||
self.collection, filter_query, projection, sort, limit, skip
|
||||
)
|
||||
|
||||
def update_one(
|
||||
|
|
@ -111,12 +104,7 @@ class MongoActions(
|
|||
upsert: bool = False,
|
||||
):
|
||||
"""Update a single document."""
|
||||
return super().update_one(
|
||||
self.collection,
|
||||
filter_query,
|
||||
update_data,
|
||||
upsert
|
||||
)
|
||||
return super().update_one(self.collection, filter_query, update_data, upsert)
|
||||
|
||||
def update_many(
|
||||
self,
|
||||
|
|
@ -125,12 +113,7 @@ class MongoActions(
|
|||
upsert: bool = False,
|
||||
):
|
||||
"""Update multiple documents."""
|
||||
return super().update_many(
|
||||
self.collection,
|
||||
filter_query,
|
||||
update_data,
|
||||
upsert
|
||||
)
|
||||
return super().update_many(self.collection, filter_query, update_data, upsert)
|
||||
|
||||
def delete_one(self, filter_query: Dict[str, Any]):
|
||||
"""Delete a single document."""
|
||||
|
|
|
|||
|
|
@ -19,27 +19,27 @@ from Services.MongoDb.Models.exceptions import (
|
|||
PasswordHistoryError,
|
||||
PasswordReuseError,
|
||||
PasswordHistoryLimitError,
|
||||
InvalidPasswordDetailError
|
||||
InvalidPasswordDetailError,
|
||||
)
|
||||
from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
||||
|
||||
|
||||
def handle_mongo_errors(func: Callable) -> Callable:
|
||||
"""Decorator to handle MongoDB operation errors.
|
||||
|
||||
|
||||
Args:
|
||||
func: Function to wrap with error handling
|
||||
|
||||
|
||||
Returns:
|
||||
Wrapped function with error handling
|
||||
"""
|
||||
|
||||
async def wrapper(*args, **kwargs) -> Any:
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except ConnectionFailure as e:
|
||||
raise MongoConnectionError(
|
||||
message=str(e),
|
||||
details={"error_type": "connection_failure"}
|
||||
message=str(e), details={"error_type": "connection_failure"}
|
||||
).to_http_exception()
|
||||
except DuplicateKeyError as e:
|
||||
raise MongoDuplicateKeyError(
|
||||
|
|
@ -48,143 +48,138 @@ def handle_mongo_errors(func: Callable) -> Callable:
|
|||
).to_http_exception()
|
||||
except PyMongoError as e:
|
||||
raise MongoBaseException(
|
||||
message=str(e),
|
||||
details={"error_type": "pymongo_error"}
|
||||
message=str(e), details={"error_type": "pymongo_error"}
|
||||
).to_http_exception()
|
||||
except Exception as e:
|
||||
raise HTTPExceptionApi(
|
||||
lang="en",
|
||||
error_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def mongo_base_exception_handler(
|
||||
request: Request,
|
||||
exc: MongoBaseException
|
||||
request: Request, exc: MongoBaseException
|
||||
) -> JSONResponse:
|
||||
"""Handle base MongoDB exceptions.
|
||||
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: MongoDB base exception
|
||||
|
||||
|
||||
Returns:
|
||||
JSON response with error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"error": exc.to_http_exception()}
|
||||
status_code=exc.status_code, content={"error": exc.to_http_exception()}
|
||||
)
|
||||
|
||||
|
||||
async def mongo_connection_error_handler(
|
||||
request: Request,
|
||||
exc: MongoConnectionError
|
||||
request: Request, exc: MongoConnectionError
|
||||
) -> JSONResponse:
|
||||
"""Handle MongoDB connection errors.
|
||||
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: MongoDB connection error
|
||||
|
||||
|
||||
Returns:
|
||||
JSON response with connection error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
content={"error": exc.to_http_exception()}
|
||||
content={"error": exc.to_http_exception()},
|
||||
)
|
||||
|
||||
|
||||
async def mongo_document_not_found_handler(
|
||||
request: Request,
|
||||
exc: MongoDocumentNotFoundError
|
||||
request: Request, exc: MongoDocumentNotFoundError
|
||||
) -> JSONResponse:
|
||||
"""Handle document not found errors.
|
||||
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: Document not found error
|
||||
|
||||
|
||||
Returns:
|
||||
JSON response with not found error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
content={"error": exc.to_http_exception()}
|
||||
content={"error": exc.to_http_exception()},
|
||||
)
|
||||
|
||||
|
||||
async def mongo_validation_error_handler(
|
||||
request: Request,
|
||||
exc: MongoValidationError
|
||||
request: Request, exc: MongoValidationError
|
||||
) -> JSONResponse:
|
||||
"""Handle validation errors.
|
||||
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: Validation error
|
||||
|
||||
|
||||
Returns:
|
||||
JSON response with validation error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
content={"error": exc.to_http_exception()}
|
||||
content={"error": exc.to_http_exception()},
|
||||
)
|
||||
|
||||
|
||||
async def mongo_duplicate_key_error_handler(
|
||||
request: Request,
|
||||
exc: MongoDuplicateKeyError
|
||||
request: Request, exc: MongoDuplicateKeyError
|
||||
) -> JSONResponse:
|
||||
"""Handle duplicate key errors.
|
||||
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: Duplicate key error
|
||||
|
||||
|
||||
Returns:
|
||||
JSON response with duplicate key error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
content={"error": exc.to_http_exception()}
|
||||
status_code=status.HTTP_409_CONFLICT, content={"error": exc.to_http_exception()}
|
||||
)
|
||||
|
||||
|
||||
async def password_history_error_handler(
|
||||
request: Request,
|
||||
exc: PasswordHistoryError
|
||||
request: Request, exc: PasswordHistoryError
|
||||
) -> JSONResponse:
|
||||
"""Handle password history errors.
|
||||
|
||||
|
||||
Args:
|
||||
request: FastAPI request
|
||||
exc: Password history error
|
||||
|
||||
|
||||
Returns:
|
||||
JSON response with password history error details
|
||||
"""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={"error": exc.to_http_exception()}
|
||||
status_code=exc.status_code, content={"error": exc.to_http_exception()}
|
||||
)
|
||||
|
||||
|
||||
def register_exception_handlers(app: Any) -> None:
|
||||
"""Register all MongoDB exception handlers with FastAPI app.
|
||||
|
||||
|
||||
Args:
|
||||
app: FastAPI application instance
|
||||
"""
|
||||
app.add_exception_handler(MongoBaseException, mongo_base_exception_handler)
|
||||
app.add_exception_handler(MongoConnectionError, mongo_connection_error_handler)
|
||||
app.add_exception_handler(MongoDocumentNotFoundError, mongo_document_not_found_handler)
|
||||
app.add_exception_handler(
|
||||
MongoDocumentNotFoundError, mongo_document_not_found_handler
|
||||
)
|
||||
app.add_exception_handler(MongoValidationError, mongo_validation_error_handler)
|
||||
app.add_exception_handler(MongoDuplicateKeyError, mongo_duplicate_key_error_handler)
|
||||
app.add_exception_handler(PasswordHistoryError, password_history_error_handler)
|
||||
app.add_exception_handler(PasswordReuseError, password_history_error_handler)
|
||||
app.add_exception_handler(PasswordHistoryLimitError, password_history_error_handler)
|
||||
app.add_exception_handler(InvalidPasswordDetailError, password_history_error_handler)
|
||||
app.add_exception_handler(
|
||||
InvalidPasswordDetailError, password_history_error_handler
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ from ErrorHandlers.ErrorHandlers.api_exc_handler import HTTPExceptionApi
|
|||
|
||||
class MongoBaseException(Exception):
|
||||
"""Base exception for MongoDB-related errors."""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
self.message = message
|
||||
self.status_code = status_code
|
||||
|
|
@ -34,128 +34,110 @@ class MongoBaseException(Exception):
|
|||
|
||||
class MongoConnectionError(MongoBaseException):
|
||||
"""Raised when there's an error connecting to MongoDB."""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Failed to connect to MongoDB",
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
details=details
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class MongoDocumentNotFoundError(MongoBaseException):
|
||||
"""Raised when a document is not found in MongoDB."""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collection: str,
|
||||
filter_query: Dict[str, Any],
|
||||
message: Optional[str] = None
|
||||
message: Optional[str] = None,
|
||||
):
|
||||
message = message or f"Document not found in collection '{collection}'"
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
details={
|
||||
"collection": collection,
|
||||
"filter": filter_query
|
||||
}
|
||||
details={"collection": collection, "filter": filter_query},
|
||||
)
|
||||
|
||||
|
||||
class MongoValidationError(MongoBaseException):
|
||||
"""Raised when document validation fails."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
field_errors: Optional[Dict[str, str]] = None
|
||||
):
|
||||
|
||||
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None):
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
details={"field_errors": field_errors or {}}
|
||||
details={"field_errors": field_errors or {}},
|
||||
)
|
||||
|
||||
|
||||
class MongoDuplicateKeyError(MongoBaseException):
|
||||
"""Raised when trying to insert a document with a duplicate key."""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
collection: str,
|
||||
key_pattern: Dict[str, Any],
|
||||
message: Optional[str] = None
|
||||
message: Optional[str] = None,
|
||||
):
|
||||
message = message or f"Duplicate key error in collection '{collection}'"
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
details={
|
||||
"collection": collection,
|
||||
"key_pattern": key_pattern
|
||||
}
|
||||
details={"collection": collection, "key_pattern": key_pattern},
|
||||
)
|
||||
|
||||
|
||||
class PasswordHistoryError(MongoBaseException):
|
||||
"""Base exception for password history-related errors."""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
status_code: int = status.HTTP_400_BAD_REQUEST,
|
||||
details: Optional[Dict[str, Any]] = None
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
super().__init__(message, status_code, details)
|
||||
|
||||
|
||||
class PasswordReuseError(PasswordHistoryError):
|
||||
"""Raised when attempting to reuse a recent password."""
|
||||
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str = "Password was used recently",
|
||||
history_limit: Optional[int] = None
|
||||
history_limit: Optional[int] = None,
|
||||
):
|
||||
details = {"history_limit": history_limit} if history_limit else None
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
details=details
|
||||
details=details,
|
||||
)
|
||||
|
||||
|
||||
class PasswordHistoryLimitError(PasswordHistoryError):
|
||||
"""Raised when password history limit is reached."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
limit: int,
|
||||
message: Optional[str] = None
|
||||
):
|
||||
|
||||
def __init__(self, limit: int, message: Optional[str] = None):
|
||||
message = message or f"Password history limit of {limit} reached"
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
details={"limit": limit}
|
||||
details={"limit": limit},
|
||||
)
|
||||
|
||||
|
||||
class InvalidPasswordDetailError(PasswordHistoryError):
|
||||
"""Raised when password history detail is invalid."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
field_errors: Optional[Dict[str, str]] = None
|
||||
):
|
||||
|
||||
def __init__(self, message: str, field_errors: Optional[Dict[str, str]] = None):
|
||||
super().__init__(
|
||||
message=message,
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
details={"field_errors": field_errors or {}}
|
||||
details={"field_errors": field_errors or {}},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -34,25 +34,16 @@ def handle_mongo_errors(func):
|
|||
try:
|
||||
return func(*args, **kwargs)
|
||||
except ConnectionFailure:
|
||||
raise HTTPExceptionApi(
|
||||
error_code="HTTP_503_SERVICE_UNAVAILABLE",
|
||||
lang="en"
|
||||
)
|
||||
raise HTTPExceptionApi(error_code="HTTP_503_SERVICE_UNAVAILABLE", lang="en")
|
||||
except ServerSelectionTimeoutError:
|
||||
raise HTTPExceptionApi(
|
||||
error_code="HTTP_504_GATEWAY_TIMEOUT",
|
||||
lang="en"
|
||||
)
|
||||
raise HTTPExceptionApi(error_code="HTTP_504_GATEWAY_TIMEOUT", lang="en")
|
||||
except OperationFailure as e:
|
||||
raise HTTPExceptionApi(
|
||||
error_code="HTTP_400_BAD_REQUEST",
|
||||
lang="en"
|
||||
)
|
||||
raise HTTPExceptionApi(error_code="HTTP_400_BAD_REQUEST", lang="en")
|
||||
except PyMongoError as e:
|
||||
raise HTTPExceptionApi(
|
||||
error_code="HTTP_500_INTERNAL_SERVER_ERROR",
|
||||
lang="en"
|
||||
error_code="HTTP_500_INTERNAL_SERVER_ERROR", lang="en"
|
||||
)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,11 +42,15 @@ class MongoFindMixin:
|
|||
class MongoUpdateMixin:
|
||||
"""Mixin for MongoDB update operations."""
|
||||
|
||||
def update_one(self, filter_query: Dict[str, Any], update: Dict[str, Any]) -> UpdateResult:
|
||||
def update_one(
|
||||
self, filter_query: Dict[str, Any], update: Dict[str, Any]
|
||||
) -> UpdateResult:
|
||||
"""Update a single document."""
|
||||
return self.collection.update_one(filter_query, update)
|
||||
|
||||
def update_many(self, filter_query: Dict[str, Any], update: Dict[str, Any]) -> UpdateResult:
|
||||
def update_many(
|
||||
self, filter_query: Dict[str, Any], update: Dict[str, Any]
|
||||
) -> UpdateResult:
|
||||
"""Update multiple documents."""
|
||||
return self.collection.update_many(filter_query, update)
|
||||
|
||||
|
|
@ -100,7 +104,7 @@ class MongoDBHandler(
|
|||
)
|
||||
else:
|
||||
connection_url = f"mongodb://{MongoConfig.HOST}:{MongoConfig.PORT}"
|
||||
|
||||
|
||||
# Build connection options
|
||||
connection_kwargs = {
|
||||
"host": connection_url,
|
||||
|
|
@ -110,11 +114,11 @@ class MongoDBHandler(
|
|||
"waitQueueTimeoutMS": 2000, # How long a thread will wait for a connection
|
||||
"serverSelectionTimeoutMS": 5000, # How long to wait for server selection
|
||||
}
|
||||
|
||||
|
||||
self._client = MongoClient(**connection_kwargs)
|
||||
|
||||
|
||||
# Test connection
|
||||
self._client.admin.command('ping')
|
||||
self._client.admin.command("ping")
|
||||
|
||||
def close(self):
|
||||
"""Close MongoDB connection."""
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ from functools import lru_cache
|
|||
from typing import Generator
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker, scoped_session, Session
|
||||
from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session, Session
|
||||
|
||||
from AllConfigs.SqlDatabase.configs import WagDatabase
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
"""Test fixtures and models."""
|
||||
|
||||
from sqlalchemy import Column, String
|
||||
|
||||
from Services.PostgresDb.Models.mixins import CrudCollection
|
||||
|
||||
|
||||
class TestUser(CrudCollection):
|
||||
"""Test user model for PostgreSQL tests."""
|
||||
|
||||
__tablename__ = "test_users"
|
||||
username = Column(String, unique=True, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
"""Test models."""
|
||||
|
||||
from sqlalchemy import Column, String
|
||||
|
||||
from Services.PostgresDb.Models.mixins import CrudCollection
|
||||
|
||||
|
||||
class UserModel(CrudCollection):
|
||||
"""User model for PostgreSQL tests."""
|
||||
|
||||
__tablename__ = "test_users"
|
||||
username = Column(String, unique=True, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
|
|
@ -7,7 +7,7 @@ from Services.MongoDb.Models.actions import MongoActions
|
|||
from Services.MongoDb.Models.action_models.domain import (
|
||||
DomainData,
|
||||
DomainDocumentCreate,
|
||||
DomainDocumentUpdate
|
||||
DomainDocumentUpdate,
|
||||
)
|
||||
from AllConfigs.NoSqlDatabase.configs import MongoConfig
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ def mongo_client():
|
|||
"""Create MongoDB test client."""
|
||||
# Connect using configured credentials
|
||||
client = MongoClient(MongoConfig.URL)
|
||||
client.admin.command('ping') # Test connection
|
||||
client.admin.command("ping") # Test connection
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
|
@ -27,12 +27,12 @@ def mongo_actions(mongo_client):
|
|||
"""Create MongoActions instance for testing."""
|
||||
if not mongo_client:
|
||||
pytest.skip("MongoDB connection not available")
|
||||
|
||||
|
||||
actions = MongoActions(
|
||||
client=mongo_client,
|
||||
database=MongoConfig.DATABASE_NAME,
|
||||
company_uuid="test_company",
|
||||
storage_reason="domains"
|
||||
storage_reason="domains",
|
||||
)
|
||||
yield actions
|
||||
try:
|
||||
|
|
@ -45,37 +45,36 @@ def mongo_actions(mongo_client):
|
|||
|
||||
def test_mongo_crud_operations(mongo_actions: MongoActions):
|
||||
"""Test CRUD operations with MongoActions."""
|
||||
|
||||
|
||||
# Create test data
|
||||
domain_data = DomainData(
|
||||
user_uu_id="test_user",
|
||||
main_domain="example.com",
|
||||
other_domains_list=["old.com"]
|
||||
other_domains_list=["old.com"],
|
||||
)
|
||||
create_doc = DomainDocumentCreate(data=domain_data)
|
||||
|
||||
|
||||
# Test create
|
||||
result = mongo_actions.insert_one(create_doc.model_dump())
|
||||
assert result.inserted_id is not None
|
||||
|
||||
|
||||
# Test read
|
||||
doc = mongo_actions.find_one({"data.main_domain": "example.com"})
|
||||
assert doc is not None
|
||||
assert doc["data"]["main_domain"] == "example.com"
|
||||
|
||||
|
||||
# Test update
|
||||
update_data = DomainData(
|
||||
user_uu_id="test_user",
|
||||
main_domain="new.com",
|
||||
other_domains_list=["example.com", "old.com"]
|
||||
other_domains_list=["example.com", "old.com"],
|
||||
)
|
||||
update_doc = DomainDocumentUpdate(data=update_data)
|
||||
result = mongo_actions.update_one(
|
||||
{"_id": doc["_id"]},
|
||||
{"$set": update_doc.model_dump()}
|
||||
{"_id": doc["_id"]}, {"$set": update_doc.model_dump()}
|
||||
)
|
||||
assert result.modified_count == 1
|
||||
|
||||
|
||||
# Test delete
|
||||
result = mongo_actions.delete_one({"_id": doc["_id"]})
|
||||
assert result.deleted_count == 1
|
||||
|
|
@ -83,23 +82,18 @@ def test_mongo_crud_operations(mongo_actions: MongoActions):
|
|||
|
||||
def test_mongo_aggregate(mongo_actions: MongoActions):
|
||||
"""Test aggregate operations with MongoActions."""
|
||||
|
||||
|
||||
# Insert test documents
|
||||
docs = [
|
||||
DomainDocumentCreate(
|
||||
data=DomainData(
|
||||
user_uu_id="user1",
|
||||
main_domain=f"domain{i}.com"
|
||||
)
|
||||
data=DomainData(user_uu_id="user1", main_domain=f"domain{i}.com")
|
||||
).model_dump()
|
||||
for i in range(3)
|
||||
]
|
||||
mongo_actions.insert_many(docs)
|
||||
|
||||
|
||||
# Test aggregation
|
||||
pipeline = [
|
||||
{"$group": {"_id": "$data.user_uu_id", "count": {"$sum": 1}}}
|
||||
]
|
||||
pipeline = [{"$group": {"_id": "$data.user_uu_id", "count": {"$sum": 1}}}]
|
||||
result = mongo_actions.aggregate(pipeline)
|
||||
result_list = list(result)
|
||||
assert len(result_list) == 1
|
||||
|
|
@ -107,4 +101,4 @@ def test_mongo_aggregate(mongo_actions: MongoActions):
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
pytest.main([__file__, "-v"])
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
"""Test PostgreSQL database operations."""
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import Column, String, create_engine, text
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from Services.PostgresDb.database import Base, get_db
|
||||
from Services.PostgresDb.Models.mixins import CrudCollection
|
||||
from AllConfigs.SqlDatabase.configs import WagDatabase
|
||||
|
||||
|
||||
class TestUser(CrudCollection):
|
||||
"""Test user model for PostgreSQL tests."""
|
||||
|
||||
__tablename__ = "test_users"
|
||||
|
||||
username = Column(String, unique=True, index=True)
|
||||
email = Column(String, unique=True, index=True)
|
||||
from Ztest.models import UserModel
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
|
@ -24,12 +15,12 @@ def db_engine():
|
|||
# Use the same database URL but with test database
|
||||
test_db_url = WagDatabase.DATABASE_URL
|
||||
engine = create_engine(test_db_url, echo=True)
|
||||
|
||||
|
||||
# Create all tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
yield engine
|
||||
|
||||
|
||||
# Drop all tables after tests
|
||||
Base.metadata.drop_all(bind=engine)
|
||||
|
||||
|
|
@ -40,9 +31,9 @@ def db_session(db_engine):
|
|||
connection = db_engine.connect()
|
||||
transaction = connection.begin()
|
||||
session = Session(bind=connection)
|
||||
|
||||
|
||||
yield session
|
||||
|
||||
|
||||
# Rollback the transaction after each test
|
||||
transaction.rollback()
|
||||
connection.close()
|
||||
|
|
@ -52,12 +43,12 @@ def db_session(db_engine):
|
|||
def test_create_user(db_session):
|
||||
"""Test creating a user in the database."""
|
||||
# Create user using CrudMixin methods
|
||||
user = TestUser(username="testuser", email="test@example.com")
|
||||
user = UserModel(username="testuser", email="test@example.com")
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
# Verify user was created
|
||||
db_user = db_session.query(TestUser).filter_by(username="testuser").first()
|
||||
db_user = db_session.query(UserModel).filter_by(username="testuser").first()
|
||||
assert db_user is not None
|
||||
assert db_user.email == "test@example.com"
|
||||
assert db_user.created_at is not None
|
||||
|
|
@ -68,16 +59,16 @@ def test_create_user(db_session):
|
|||
def test_update_user(db_session):
|
||||
"""Test updating a user in the database."""
|
||||
# Create user
|
||||
user = TestUser(username="updateuser", email="update@example.com")
|
||||
user = UserModel(username="updateuser", email="update@example.com")
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
# Update user using CrudMixin methods
|
||||
user.update(session=db_session, email="newemail@example.com")
|
||||
user.update(db=db_session, email="newemail@example.com")
|
||||
db_session.commit()
|
||||
|
||||
|
||||
# Verify update
|
||||
updated_user = db_session.query(TestUser).filter_by(username="updateuser").first()
|
||||
updated_user = db_session.query(UserModel).filter_by(username="updateuser").first()
|
||||
assert updated_user.email == "newemail@example.com"
|
||||
assert updated_user.updated_at is not None
|
||||
|
||||
|
|
@ -85,16 +76,16 @@ def test_update_user(db_session):
|
|||
def test_soft_delete_user(db_session):
|
||||
"""Test soft deleting a user from the database."""
|
||||
# Create user
|
||||
user = TestUser(username="deleteuser", email="delete@example.com")
|
||||
user = UserModel(username="deleteuser", email="delete@example.com")
|
||||
db_session.add(user)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
# Soft delete by updating deleted and active flags
|
||||
user.update(session=db_session, deleted=True, active=False)
|
||||
user.update(db=db_session, deleted=True, active=False)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
# Verify soft deletion
|
||||
deleted_user = db_session.query(TestUser).filter_by(username="deleteuser").first()
|
||||
deleted_user = db_session.query(UserModel).filter_by(username="deleteuser").first()
|
||||
assert deleted_user is not None
|
||||
assert deleted_user.deleted
|
||||
assert not deleted_user.active
|
||||
|
|
@ -105,4 +96,4 @@ def test_get_db_context_manager():
|
|||
with get_db() as session:
|
||||
# Verify we can execute a simple query
|
||||
result = session.execute(text("SELECT 1"))
|
||||
assert result.scalar() == 1
|
||||
assert result.scalar() == 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
services:
|
||||
auth-service:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: DockerApiServices/AuthServiceApi/Dockerfile
|
||||
ports:
|
||||
- "41575:41575"
|
||||
|
||||
event-service:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: DockerApiServices/EventServiceApi/Dockerfile
|
||||
ports:
|
||||
- "8001:8000"
|
||||
|
||||
validation-service:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: DockerApiServices/ValidationServiceApi/Dockerfile
|
||||
ports:
|
||||
- "8002:8000"
|
||||
# and lets try to implement potry again in the dockerfile now we now that it is about copy of files
|
||||
Loading…
Reference in New Issue