diff --git a/backend/app/auth/routes.py b/backend/app/auth/routes.py index bf2d5b14..e3497ae5 100644 --- a/backend/app/auth/routes.py +++ b/backend/app/auth/routes.py @@ -15,16 +15,13 @@ @router.post("/signup/email", response_model=AuthResponse) async def signup_with_email(request: EmailSignupRequest): """ - Registers a new user using email, password, and name, and returns authentication tokens and user information. + Register a new user with email, password, and name, returning authentication tokens and user information. - Args: - request: Contains the user's email, password, and name for registration. + Parameters: + request (EmailSignupRequest): Registration details including email, password, and name. Returns: - An AuthResponse with access token, refresh token, and user details. - - Raises: - HTTPException: If registration fails or an unexpected error occurs. + AuthResponse: Contains access token, refresh token, and user details. """ try: result = await auth_service.create_user_with_email( @@ -58,9 +55,10 @@ async def signup_with_email(request: EmailSignupRequest): @router.post("/login/email", response_model=AuthResponse) async def login_with_email(request: EmailLoginRequest): """ - Authenticates a user using email and password credentials. + Authenticate a user with email and password, returning access and refresh tokens along with user details. - On successful authentication, returns an access token, refresh token, and user information. Raises an HTTP 500 error if authentication fails due to an unexpected error. + Returns: + AuthResponse: Contains the JWT access token, refresh token, and authenticated user information. """ try: result = await auth_service.authenticate_user_with_email( @@ -93,9 +91,10 @@ async def login_with_email(request: EmailLoginRequest): @router.post("/login/google", response_model=AuthResponse) async def login_with_google(request: GoogleLoginRequest): """ - Authenticates or registers a user using a Google OAuth ID token. + Authenticate or register a user using a Google OAuth ID token. - On success, returns an access token, refresh token, and user information. Raises an HTTP 500 error if Google authentication fails. + Returns: + AuthResponse: Contains a JWT access token, refresh token, and user information upon successful authentication or registration. """ try: result = await auth_service.authenticate_with_google(request.id_token) @@ -125,12 +124,12 @@ async def login_with_google(request: GoogleLoginRequest): @router.post("/refresh", response_model=TokenResponse) async def refresh_token(request: RefreshTokenRequest): """ - Refreshes JWT tokens using a valid refresh token. + Refreshes JWT access and refresh tokens using a valid refresh token. + + Validates the provided refresh token, issues new tokens if valid, and returns them. Raises a 401 error if the refresh token is invalid or revoked. - Validates the provided refresh token, issues a new access token and refresh token if valid, and returns them. Raises a 401 error if the refresh token is invalid or revoked. - Returns: - A TokenResponse containing the new access and refresh tokens. + TokenResponse: Contains the new access and refresh tokens. """ try: new_refresh_token = await auth_service.refresh_access_token(request.refresh_token) @@ -169,10 +168,13 @@ async def refresh_token(request: RefreshTokenRequest): @router.post("/token/verify", response_model=UserResponse) async def verify_token(request: TokenVerifyRequest): """ - Verifies an access token and returns the associated user information. + Verify an access token and return the corresponding user information. + + Returns: + UserResponse: User data associated with the valid access token. Raises: - HTTPException: If the token is invalid or expired, returns a 401 Unauthorized error. + HTTPException: Returns 401 Unauthorized if the token is invalid or expired. """ try: user = await auth_service.verify_access_token(request.access_token) @@ -192,10 +194,10 @@ async def verify_token(request: TokenVerifyRequest): @router.post("/password/reset/request", response_model=SuccessResponse) async def request_password_reset(request: PasswordResetRequest): """ - Initiates a password reset process by sending a reset link to the provided email address. + Initiate a password reset by sending a reset link to the specified email address. Returns: - SuccessResponse: Indicates whether the password reset email was sent if the email exists. + SuccessResponse: Indicates that a reset link has been sent if the email exists in the system. """ try: await auth_service.request_password_reset(request.email) @@ -212,16 +214,10 @@ async def request_password_reset(request: PasswordResetRequest): @router.post("/password/reset/confirm", response_model=SuccessResponse) async def confirm_password_reset(request: PasswordResetConfirm): """ - Resets a user's password using a valid password reset token. - - Args: - request: Contains the password reset token and the new password. + Reset a user's password using a valid reset token and new password. Returns: - SuccessResponse indicating the password has been reset successfully. - - Raises: - HTTPException: If the reset token is invalid or an error occurs during the reset process. + SuccessResponse: Indicates the password has been reset successfully. """ try: await auth_service.confirm_password_reset( diff --git a/backend/app/auth/security.py b/backend/app/auth/security.py index 97849cee..95c6ab36 100644 --- a/backend/app/auth/security.py +++ b/backend/app/auth/security.py @@ -15,41 +15,39 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: """ - Verifies whether a plaintext password matches a given hashed password. + Check if a plaintext password matches a given bcrypt hashed password. - Args: - plain_password: The plaintext password to verify. - hashed_password: The hashed password to compare against. + Parameters: + plain_password (str): The plaintext password to verify. + hashed_password (str): The bcrypt hashed password to compare against. Returns: - True if the plaintext password matches the hash, otherwise False. + bool: True if the plaintext password matches the hashed password, otherwise False. """ return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password: str) -> str: """ - Hashes a plaintext password using bcrypt. + Hash a plaintext password using bcrypt and return the hashed string. - Args: - password: The plaintext password to hash. + Parameters: + password (str): The plaintext password to be hashed. Returns: - The bcrypt-hashed password as a string. + str: The bcrypt hash of the provided password. """ return pwd_context.hash(password) def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """ - Creates a JWT access token embedding the provided data and an expiration time. + Generate a signed JWT access token containing the provided payload and an expiration time. - If `expires_delta` is not specified, the token expires after the default duration from settings. The payload includes an expiration timestamp and a type field set to "access". The token is signed using the configured secret key and algorithm. - - Args: - data: The payload to include in the token. - expires_delta: Optional timedelta specifying how long the token is valid. + Parameters: + data (Dict[str, Any]): The payload to embed in the token. + expires_delta (Optional[timedelta]): Optional duration for which the token remains valid. If not provided, a default expiration from settings is used. Returns: - A signed JWT access token as a string. + str: The encoded JWT access token as a string. """ to_encode = data.copy() if expires_delta: @@ -63,19 +61,18 @@ def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] def create_refresh_token() -> str: """ - Generates a secure random refresh token as a URL-safe string. + Generate a cryptographically secure, URL-safe random string for use as a refresh token. Returns: - A cryptographically secure, URL-safe refresh token string. + str: A secure, URL-safe refresh token string. """ return secrets.token_urlsafe(32) def verify_token(token: str) -> Dict[str, Any]: """ - Verifies and decodes a JWT token. + Decode and validate a JWT token, returning its payload as a dictionary. - If the token is invalid or cannot be verified, raises an HTTP 401 Unauthorized exception. - Returns the decoded token payload as a dictionary. + Raises an HTTP 401 Unauthorized exception if the token is invalid or verification fails. """ try: payload = jwt.decode(token, settings.secret_key, algorithms=[settings.algorithm]) @@ -89,9 +86,9 @@ def verify_token(token: str) -> Dict[str, Any]: def generate_reset_token() -> str: """ - Generates a secure, URL-safe token for password reset operations. + Generate a cryptographically secure, URL-safe random string for use as a password reset token. Returns: - A random 32-byte URL-safe string suitable for use as a password reset token. + str: A random 32-byte URL-safe string suitable for password reset operations. """ return secrets.token_urlsafe(32) diff --git a/backend/app/auth/service.py b/backend/app/auth/service.py index 2e5778ce..2fcce2f8 100644 --- a/backend/app/auth/service.py +++ b/backend/app/auth/service.py @@ -53,30 +53,30 @@ class AuthService: def __init__(self): # Initializes the AuthService instance. + """ + Initialize a new instance of the AuthService class. + """ pass def get_db(self): """ - Returns a database connection instance from the application's database module. + Retrieve a database connection instance from the application's database module. """ return get_database() async def create_user_with_email(self, email: str, password: str, name: str) -> Dict[str, Any]: """ - Creates a new user account with the provided email, password, and name. + Create a new user account with the specified email, password, and name. - Checks for existing users with the same email and raises an error if found. Stores the user with a hashed password and default profile fields, then generates and returns a refresh token along with the user data. + Checks for an existing user with the given email and raises an HTTP 400 error if found. Stores the user with a hashed password and default profile fields, then generates and returns a refresh token along with the user data. - Args: - email: The user's email address. - password: The user's plaintext password. - name: The user's display name. + Parameters: + email (str): The user's email address. + password (str): The user's plaintext password. + name (str): The user's display name. Returns: - A dictionary containing the created user document and a refresh token. - - Raises: - HTTPException: If a user with the given email already exists. + Dict[str, Any]: A dictionary containing the created user document and a refresh token. """ db = self.get_db() @@ -119,12 +119,12 @@ async def create_user_with_email(self, email: str, password: str, name: str) -> async def authenticate_user_with_email(self, email: str, password: str) -> Dict[str, Any]: """ - Authenticates a user using email and password credentials. + Authenticate a user by verifying email and password credentials. - Verifies the provided email and password against stored user data. If authentication succeeds, returns the user information and a new refresh token. Raises an HTTP 401 error if credentials are invalid. + If authentication is successful, returns a dictionary containing the user document and a new refresh token. Raises an HTTP 401 error if the credentials are invalid. Returns: - A dictionary containing the authenticated user and a new refresh token. + dict: Contains the authenticated user and a new refresh token. """ db = self.get_db() @@ -145,15 +145,15 @@ async def authenticate_user_with_email(self, email: str, password: str) -> Dict[ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]: """ - Authenticates a user using a Google OAuth ID token, creating a new user if necessary. + Authenticate a user using a Google OAuth ID token, creating or updating the user as needed. - Verifies the provided Firebase ID token, retrieves or creates the corresponding user in the database, updates user information if needed, and issues a new refresh token. Raises an HTTP 400 error if the email is missing or if authentication fails, and HTTP 401 if the token is invalid. + Verifies the provided Firebase ID token, retrieves or creates the corresponding user in the database, updates user information if necessary, and issues a new refresh token. Raises an HTTP 400 error if the email is missing or authentication fails, and HTTP 401 if the token is invalid. - Args: - id_token: The Firebase ID token obtained from Google OAuth. + Parameters: + id_token (str): The Firebase ID token obtained from Google OAuth. Returns: - A dictionary containing the user data and a new refresh token. + Dict[str, Any]: A dictionary containing the user data and a new refresh token. """ try: # Verify the Firebase ID token @@ -229,15 +229,15 @@ async def authenticate_with_google(self, id_token: str) -> Dict[str, Any]: async def refresh_access_token(self, refresh_token: str) -> str: """ - Refreshes an access token by validating and rotating the provided refresh token. + Refreshes the access token by validating and rotating the provided refresh token. If the refresh token is valid and not expired, issues a new refresh token and revokes the old one. Raises an HTTP 401 error if the token is invalid, expired, or the associated user does not exist. - Args: - refresh_token: The refresh token string to validate and rotate. + Parameters: + refresh_token (str): The refresh token to validate and rotate. Returns: - A new refresh token string. + str: A new refresh token string. """ db = self.get_db() @@ -274,13 +274,13 @@ async def refresh_access_token(self, refresh_token: str) -> str: return new_refresh_token async def verify_access_token(self, token: str) -> Dict[str, Any]: """ - Verifies an access token and retrieves the associated user. + Verify a JWT access token and return the associated user document. - Args: - token: The JWT access token to verify. + Parameters: + token (str): The JWT access token to verify. Returns: - The user document corresponding to the token's subject. + Dict[str, Any]: The user document corresponding to the token's subject. Raises: HTTPException: If the token is invalid or the user does not exist. @@ -309,9 +309,9 @@ async def verify_access_token(self, token: str) -> Dict[str, Any]: async def request_password_reset(self, email: str) -> bool: """ - Initiates a password reset process for the specified email address. + Initiates a password reset process for the given email address. - If the user exists, generates a password reset token with a 1-hour expiration and stores it in the database. The reset token and link are logged for development purposes. Always returns True to avoid revealing whether the email is registered. + If the user exists, generates and stores a password reset token with a 1-hour expiration. Always returns True to prevent email enumeration. """ db = self.get_db() @@ -342,16 +342,16 @@ async def request_password_reset(self, email: str) -> bool: async def confirm_password_reset(self, reset_token: str, new_password: str) -> bool: """ - Confirms a password reset using a valid reset token and updates the user's password. + Reset a user's password using a valid reset token and revoke all existing refresh tokens. - Validates the reset token, updates the user's password, marks the token as used, and revokes all existing refresh tokens for the user to require re-authentication. + Validates the provided reset token, updates the user's password, marks the token as used, and revokes all refresh tokens for the user to require re-authentication. - Args: - reset_token: The password reset token to validate. - new_password: The new password to set for the user. + Parameters: + reset_token (str): The password reset token to validate and consume. + new_password (str): The new password to set for the user. Returns: - True if the password reset is successful. + bool: True if the password reset is successful. Raises: HTTPException: If the reset token is invalid or expired. @@ -393,15 +393,15 @@ async def confirm_password_reset(self, reset_token: str, new_password: str) -> b return True async def _create_refresh_token_record(self, user_id: str) -> str: """ - Generates and stores a new refresh token for the specified user. + Generate and store a new refresh token for a user, returning the token string. - Creates a refresh token with an expiration date and saves it in the database for token management and rotation. + A refresh token with an expiration date is created and saved in the database for the specified user. The token is used for session management and token rotation. - Args: - user_id: The unique identifier of the user for whom the refresh token is created. + Parameters: + user_id (str): Unique identifier of the user for whom the refresh token is generated. Returns: - The generated refresh token string. + str: The generated refresh token. """ db = self.get_db() diff --git a/backend/app/database.py b/backend/app/database.py index bf50ab72..44e66c9c 100644 --- a/backend/app/database.py +++ b/backend/app/database.py @@ -9,9 +9,9 @@ class MongoDB: async def connect_to_mongo(): """ - Initializes an asynchronous connection to MongoDB and sets the active database. + Establishes an asynchronous connection to MongoDB and sets the active database. - Establishes a connection using the configured MongoDB URL and selects the database specified in the application settings. + Initializes the MongoDB client using the configured connection URL and selects the database specified in the application settings. """ mongodb.client = AsyncIOMotorClient(settings.mongodb_url) mongodb.database = mongodb.client[settings.database_name] @@ -21,8 +21,7 @@ async def close_mongo_connection(): """ Closes the MongoDB client connection if it is currently open. - This function safely terminates the connection to the MongoDB server by closing - the existing client instance. + This function safely terminates the connection to the MongoDB server by closing the existing client instance. """ if mongodb.client: mongodb.client.close() @@ -30,8 +29,9 @@ async def close_mongo_connection(): def get_database(): """ - Returns the current MongoDB database instance. + Return the currently active MongoDB database instance. - Use this function to access the active database connection managed by the module. + Returns: + The MongoDB database object managed by the module, or None if not connected. """ return mongodb.database diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py index 6032515f..d707f806 100644 --- a/backend/app/dependencies.py +++ b/backend/app/dependencies.py @@ -9,12 +9,12 @@ async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> Dict[str, Any]: """ - Retrieves the currently authenticated user based on a JWT token from the HTTP Authorization header. + Retrieve the authenticated user based on a JWT token from the HTTP Authorization header. - Verifies the provided JWT token, extracts the user ID, and fetches the corresponding user document from the database. Raises an HTTP 401 Unauthorized error if the token is invalid, the user ID is missing, or the user does not exist. + Verifies the JWT token, extracts the user ID, and fetches the corresponding user document from the database. Raises an HTTP 401 Unauthorized error if authentication fails or the user does not exist. Returns: - A dictionary representing the authenticated user, with the `_id` field as a string. + dict: The authenticated user's data with the `_id` field as a string. """ try: # Verify token diff --git a/backend/generate_secret.py b/backend/generate_secret.py index 284e5621..a02145b7 100644 --- a/backend/generate_secret.py +++ b/backend/generate_secret.py @@ -3,12 +3,12 @@ def generate_jwt_secret(): """ - Generates a cryptographically secure 64-character secret key for JWT authentication. + Generate a cryptographically secure 64-character secret key for JWT authentication. - The key consists of uppercase and lowercase letters, digits, and the special characters "!@#$%^&*". + The generated key includes uppercase and lowercase letters, digits, and the special characters "!@#$%^&*". Returns: - A randomly generated 64-character string suitable for use as a JWT secret key. + str: A randomly generated 64-character string suitable for use as a JWT secret key. """ # Generate a 64-character secret key alphabet = string.ascii_letters + string.digits + "!@#$%^&*" diff --git a/backend/main.py b/backend/main.py index 68a36afd..0a202aab 100644 --- a/backend/main.py +++ b/backend/main.py @@ -52,7 +52,16 @@ # Add a catch-all OPTIONS handler that should work for any path @app.options("/{path:path}") async def options_handler(request: Request, path: str): - """Handle all OPTIONS requests""" + """ + Handles all OPTIONS requests for any path, returning a 200 OK response with appropriate CORS headers based on the request origin and allowed origins. + + Parameters: + request (Request): The incoming HTTP request. + path (str): The requested path segment. + + Returns: + Response: An HTTP response with CORS headers set according to the allowed origins configuration. + """ print(f"OPTIONS request received for path: /{path}") print(f"Origin: {request.headers.get('origin', 'No origin header')}") @@ -78,14 +87,14 @@ async def options_handler(request: Request, path: str): @app.on_event("startup") async def startup_event(): """ - Initializes the MongoDB connection when the application starts. + Establishes a connection to MongoDB when the FastAPI application starts. """ await connect_to_mongo() @app.on_event("shutdown") async def shutdown_event(): """ - Closes the MongoDB connection when the application shuts down. + Close the MongoDB connection during application shutdown. """ await close_mongo_connection() @@ -93,9 +102,10 @@ async def shutdown_event(): @app.get("/health") async def health_check(): """ - Returns the health status of the Splitwiser API service. + Return the health status of the Splitwiser API service. - This endpoint can be used for health checks and monitoring. + Returns: + dict: A JSON object indicating the service is healthy and specifying the service name. """ return {"status": "healthy", "service": "Splitwiser API"} diff --git a/backend/tests/auth/test_auth_routes.py b/backend/tests/auth/test_auth_routes.py index a2f47b30..f7ca0590 100644 --- a/backend/tests/auth/test_auth_routes.py +++ b/backend/tests/auth/test_auth_routes.py @@ -17,6 +17,11 @@ @pytest.mark.asyncio async def test_signup_with_email_success(mock_db): # mock_db fixture is auto-used + """ + Test successful user signup via email, verifying response tokens and user creation. + + Sends a valid signup request to the email signup endpoint and asserts that the response contains access and refresh tokens, as well as correct user data. Confirms that the user is created in the mock database with a hashed password and that a corresponding refresh token is stored and not revoked. + """ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac: signup_data = { "email": "testuser@example.com", @@ -51,6 +56,11 @@ async def test_signup_with_email_success(mock_db): # mock_db fixture is auto-use @pytest.mark.asyncio async def test_signup_with_existing_email(mock_db): # Pre-populate with a user + """ + Test that signing up with an existing email returns a 400 error and appropriate error message. + + Ensures the API rejects duplicate email registrations and provides a clear error response. + """ existing_email = "existing@example.com" await mock_db.users.insert_one({ "email": existing_email, @@ -84,6 +94,11 @@ async def test_signup_with_existing_email(mock_db): ] ) async def test_signup_invalid_input_refined(mock_db, payload_modifier, affected_field, description): + """ + Test that the signup endpoint returns appropriate validation errors for various invalid input payloads. + + This parameterized test modifies the signup payload to simulate different invalid input scenarios (such as missing fields, short passwords, or invalid email formats), sends a POST request to the signup endpoint, and asserts that the response contains a 422 status code with a validation error corresponding to the affected field and error type. + """ base_payload = { "email": "testuser@example.com", "password": "securepassword123", @@ -118,6 +133,11 @@ async def test_signup_invalid_input_refined(mock_db, payload_modifier, affected_ @pytest.mark.asyncio async def test_login_with_email_success(mock_db): + """ + Test successful login with email and password, verifying token issuance and user data. + + This test ensures that a user can log in with valid credentials, receives access and refresh tokens, and that the returned user data matches the database. It also confirms that a refresh token is created in the database, is not revoked, and matches the token in the response. + """ user_email = "loginuser@example.com" user_password = "loginpassword123" hashed_password = get_password_hash(user_password) @@ -164,6 +184,9 @@ async def test_login_with_email_success(mock_db): @pytest.mark.asyncio async def test_login_with_incorrect_password(mock_db): + """ + Test that logging in with an incorrect password returns a 401 Unauthorized error and an appropriate error message. + """ user_email = "wrongpass@example.com" correct_password = "correctpassword" incorrect_password = "incorrectpassword" @@ -190,6 +213,9 @@ async def test_login_with_incorrect_password(mock_db): @pytest.mark.asyncio async def test_login_with_non_existent_email(mock_db): + """ + Test that logging in with a non-existent email returns a 401 Unauthorized error with a generic credentials message. + """ async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac: login_data = { "email": "nosuchuser@example.com", @@ -212,6 +238,11 @@ async def test_login_with_non_existent_email(mock_db): ] ) async def test_login_invalid_input(mock_db, payload_modifier, affected_field, description): + """ + Test that the login endpoint returns appropriate validation errors for invalid input payloads. + + This parameterized test modifies the login payload to simulate various invalid input scenarios (such as missing fields or invalid email format), sends a POST request to the login endpoint, and asserts that the response contains the expected validation error for the affected field. + """ base_payload = { "email": "validuser@example.com", "password": "validpassword123" diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 7d369a85..020141ba 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -13,6 +13,11 @@ def mock_firebase_admin(request): # Mock firebase_admin.credentials.Certificate # Create a mock object that can be called and returns another mock + """ + Session-scoped pytest fixture that mocks Firebase Admin SDK components for testing. + + This fixture patches key parts of the `firebase_admin` module, including credentials, app initialization, and authentication, so that tests can run without real Firebase credentials or network access. The mocked `verify_id_token` method returns a dummy decoded token. The fixture is applied automatically to all tests in the session. + """ mock_certificate = MagicMock() # When firebase_admin.credentials.Certificate(path) is called, it returns a dummy object mock_certificate.return_value = MagicMock() @@ -51,6 +56,14 @@ def mock_firebase_admin(request): @pytest_asyncio.fixture(scope="function", autouse=True) async def mock_db(): + """ + Asynchronous pytest fixture that provides a mocked MongoDB database instance for testing. + + This fixture creates a new `AsyncMongoMockClient` for each test function, retrieves a mock database named "test_db", and patches the `get_database` function in `app.auth.service` to return this mock database. Tests using this fixture interact with the mock database transparently, ensuring isolation from real database operations. + + Yields: + The mock database instance for use within tests. + """ print("mock_db fixture: Creating AsyncMongoMockClient") mock_mongo_client = AsyncMongoMockClient() print(f"mock_db fixture: mock_mongo_client type: {type(mock_mongo_client)}") diff --git a/frontend/App.tsx b/frontend/App.tsx index 1154186d..ce4b415b 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -8,6 +8,11 @@ import LoginScreen from './screens/LoginScreen'; const Stack = createNativeStackNavigator(); +/** + * Renders the navigation stack based on authentication status. + * + * Displays the Home screen if the user is authenticated, or the Login screen if not. + */ function AppNavigator() { const { isAuthenticated } = useAuth(); @@ -30,6 +35,11 @@ function AppNavigator() { ); } +/** + * The main entry point of the React Native app, providing authentication context and navigation. + * + * Wraps the application in an authentication provider and navigation container, rendering the appropriate screens based on authentication state. + */ export default function App() { return (