backend/feature/supabase #60
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -18,7 +18,6 @@ PAYPAL_API_URL = "https://api-m.sandbox.paypal.com"
 | 
			
		||||
 | 
			
		||||
SUPABASE_URL = os.getenv("SUPABASE_URL", None)
 | 
			
		||||
SUPABASE_KEY = os.getenv("SUPABASE_API_KEY", None)
 | 
			
		||||
SUPABASE_TEST_USER_ID = os.getenv("SUPABASE_TEST_USER_ID", None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
cache_dir_string = os.getenv('OSM_CACHE_DIR', './cache')
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ from .structs.landmark import Landmark, Toilets
 | 
			
		||||
from .structs.preferences import Preferences
 | 
			
		||||
from .structs.linked_landmarks import LinkedLandmarks
 | 
			
		||||
from .structs.trip import Trip
 | 
			
		||||
from .structs.requests import UserDeleteRequest
 | 
			
		||||
from .utils.landmarks_manager import LandmarkManager
 | 
			
		||||
from .utils.toilets_manager import ToiletsManager
 | 
			
		||||
from .optimization.optimizer import Optimizer
 | 
			
		||||
@@ -62,15 +63,16 @@ def new_trip(user_id: str = Body(...),
 | 
			
		||||
        (uuid) : The uuid of the first landmark in the optimized route
 | 
			
		||||
    """
 | 
			
		||||
    # Check for valid user balance.
 | 
			
		||||
    logger.debug(f'user id: {user_id}')
 | 
			
		||||
 | 
			
		||||
    try : 
 | 
			
		||||
        if not supabase.check_balance(user_id=user_id) :
 | 
			
		||||
    try:
 | 
			
		||||
        if not supabase.check_balance(user_id=user_id):
 | 
			
		||||
            logger.warning('Insufficient credits to perform this action.')
 | 
			
		||||
            return None
 | 
			
		||||
    except Exception as exc :
 | 
			
		||||
        traceback.print_exc()  
 | 
			
		||||
        raise HTTPException(status_code=500, detail=str(exc)) from exc
 | 
			
		||||
            return {"error": "Insufficient credits"}, 400  # Return a 400 Bad Request with an appropriate message
 | 
			
		||||
    except SyntaxError as se :
 | 
			
		||||
        raise HTTPException(status_code=400, detail=str(se)) from se
 | 
			
		||||
    except ValueError as ve :
 | 
			
		||||
        raise HTTPException(status_code=406, detail=str(ve)) from ve
 | 
			
		||||
    except Exception as exc:
 | 
			
		||||
        raise HTTPException(status_code=500, detail=f"Internal Server Error: {str(exc)}") from exc
 | 
			
		||||
 | 
			
		||||
    # Check for invalid input.
 | 
			
		||||
    if preferences is None:
 | 
			
		||||
@@ -266,3 +268,46 @@ def get_toilets(location: tuple[float, float] = Query(...), radius: int = 500) -
 | 
			
		||||
        return toilets_list
 | 
			
		||||
    except KeyError as exc:
 | 
			
		||||
        raise HTTPException(status_code=404, detail="No toilets found") from exc
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.post("/user/create")
 | 
			
		||||
def register_user(email: str = Body(...), password: str = Body(...)) -> str:
 | 
			
		||||
    try:
 | 
			
		||||
        response = supabase.supabase.auth.admin.create_user({
 | 
			
		||||
            "email": email,
 | 
			
		||||
            "password": password
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        if e.code == 'email_exists' :
 | 
			
		||||
            logger.error(f"Failed to create user : {str(e.code)}")
 | 
			
		||||
            raise HTTPException(status_code=422, detail=str(e)) from e
 | 
			
		||||
        logger.error(f"Failed to create user : {str(e.code)}")
 | 
			
		||||
        raise HTTPException(status_code=500, detail=str(e)) from e
 | 
			
		||||
 | 
			
		||||
    logger.debug(response)
 | 
			
		||||
    # Extract the identity_id and user_id
 | 
			
		||||
    # identity = response.user.id  # Accessing the identity
 | 
			
		||||
    user_id = response.user.id
 | 
			
		||||
 | 
			
		||||
    logger.info(f"User created successfully, ID: {user_id}")
 | 
			
		||||
    return user_id
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.post("/user/delete")
 | 
			
		||||
def delete_user(request: UserDeleteRequest):
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        response = supabase.supabase.auth.admin.delete_user(request.user_id)
 | 
			
		||||
        logger.debug(response)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        if e.code == 'user_not_found' :
 | 
			
		||||
            logger.error(f"Failed to delete user : {str(e.code)}")
 | 
			
		||||
            raise HTTPException(status_code=404, detail=str(e)) from e
 | 
			
		||||
        logger.error(f"Failed to create user : {str(e.code)}")
 | 
			
		||||
        raise HTTPException(status_code=500, detail=str(e)) from e
 | 
			
		||||
    
 | 
			
		||||
    logger.info(f"User with ID {request.user_id} deleted successfully")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,14 @@ class Supabase:
 | 
			
		||||
        with open(os.path.join(PARAMETERS_DIR, 'secrets.yaml')) as f:
 | 
			
		||||
            secrets = yaml.safe_load(f)
 | 
			
		||||
            self.SUPABASE_URL = secrets['SUPABASE_URL']
 | 
			
		||||
            self.SUPABASE_KEY = secrets['SUPABASE_API_KEY']
 | 
			
		||||
            self.SUPABASE_ADMIN_KEY = secrets['SUPABASE_ADMIN_KEY']
 | 
			
		||||
            self.SUPABASE_TEST_USER_ID = secrets['SUPABASE_TEST_USER_ID']
 | 
			
		||||
 | 
			
		||||
        self.supabase: Client = create_client(self.SUPABASE_URL, self.SUPABASE_KEY, options=ClientOptions(schema='public'))
 | 
			
		||||
        self.supabase = create_client(
 | 
			
		||||
            self.SUPABASE_URL, 
 | 
			
		||||
            self.SUPABASE_ADMIN_KEY, 
 | 
			
		||||
            options=ClientOptions(schema='public')
 | 
			
		||||
        )
 | 
			
		||||
        self.logger.debug('Supabase client initialized.')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -47,24 +51,31 @@ class Supabase:
 | 
			
		||||
                .single()
 | 
			
		||||
                .execute()
 | 
			
		||||
            )
 | 
			
		||||
            # Check if the response was successful and contains data
 | 
			
		||||
            if response.data :
 | 
			
		||||
                credits = response.data['credit_amount']
 | 
			
		||||
                self.logger.debug(f'Credits of user {user_id}: {credits}')
 | 
			
		||||
            # self.logger.critical(response)
 | 
			
		||||
 | 
			
		||||
                if credits > 0:
 | 
			
		||||
                    self.logger.info(f'Credit balance is positive. Proceeding with trip generation')
 | 
			
		||||
                    return True
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            if e.code == '22P02' :
 | 
			
		||||
                self.logger.error(f"Failed querying credits : {str(e)}")
 | 
			
		||||
                raise SyntaxError(f"Failed querying credits : {str(e)}") from e
 | 
			
		||||
            if e.code == 'PGRST116' :
 | 
			
		||||
                self.logger.error(f"User not found : {str(e)}")
 | 
			
		||||
                raise ValueError(f"User not found : {str(e)}") from e
 | 
			
		||||
            else :
 | 
			
		||||
                self.logger.error(f"An unexpected error occured while checking user balance : {str(e)}")
 | 
			
		||||
                raise Exception(f"An unexpected error occured while checking user balance : {str(e)}") from e
 | 
			
		||||
 | 
			
		||||
                self.logger.warning(f'Insufficient balance. Trip generation cannot be granted')
 | 
			
		||||
                return False
 | 
			
		||||
        # Proceed to check the user's credit balance
 | 
			
		||||
        credits = response.data['credit_amount']
 | 
			
		||||
        self.logger.debug(f'Credits of user {user_id}: {credits}')
 | 
			
		||||
 | 
			
		||||
            # If user or credits not found, raise a 404 error
 | 
			
		||||
            raise HTTPException(status_code=404, detail="User data not found.")
 | 
			
		||||
        if credits > 0:
 | 
			
		||||
            self.logger.info(f'Credit balance is positive for user {user_id}. Proceeding with trip generation.')
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        except Exception as exc:
 | 
			
		||||
            raise HTTPException(status_code=500, detail="Error retrieving credit balance") from exc
 | 
			
		||||
        self.logger.warning(f'Insufficient balance for user {user_id}. Trip generation cannot proceed.')
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    def decrement_credit_balance(self, user_id: str) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
@@ -82,32 +93,36 @@ class Supabase:
 | 
			
		||||
                .single()
 | 
			
		||||
                .execute()
 | 
			
		||||
            )
 | 
			
		||||
            # Check if the response was successful and contains data
 | 
			
		||||
            if response.data :
 | 
			
		||||
                current_credits = response.data['credit_amount']
 | 
			
		||||
                updated_credits = current_credits - 1
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            if e.code == '22P02' :
 | 
			
		||||
                self.logger.error(f"Failed decrementing credits : {str(e)}")
 | 
			
		||||
                raise SyntaxError(f"Failed decrementing credits : {str(e)}") from e
 | 
			
		||||
            if e.code == 'PGRST116' :
 | 
			
		||||
                self.logger.error(f"User not found : {str(e)}")
 | 
			
		||||
                raise ValueError(f"User not found : {str(e)}") from e
 | 
			
		||||
            else :
 | 
			
		||||
                self.logger.error(f"An unexpected error occured while decrementing user balance : {str(e)}")
 | 
			
		||||
                raise Exception(f"An unexpected error occured while decrementing user balance : {str(e)}") from e
 | 
			
		||||
 | 
			
		||||
                # Update the user's credits in the table
 | 
			
		||||
                update_response = (
 | 
			
		||||
                    self.supabase.table('credits')
 | 
			
		||||
                    .update({'credit_amount': updated_credits})
 | 
			
		||||
                    .eq('id', user_id)
 | 
			
		||||
                    .execute()
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # Check if the update was successful
 | 
			
		||||
                if update_response.data:
 | 
			
		||||
                    self.logger.debug(f'Credit balance successfully decremented.')
 | 
			
		||||
                    return True
 | 
			
		||||
                else:
 | 
			
		||||
                    raise HTTPException(status_code=500, detail="Error decrementing credit balance.")
 | 
			
		||||
        current_credits = response.data['credit_amount']
 | 
			
		||||
        updated_credits = current_credits - 1
 | 
			
		||||
 | 
			
		||||
            # If user or credits not found, raise a 404 error
 | 
			
		||||
            raise HTTPException(status_code=404, detail="User data not found.")
 | 
			
		||||
        # Update the user's credits in the table
 | 
			
		||||
        update_response = (
 | 
			
		||||
            self.supabase.table('credits')
 | 
			
		||||
            .update({'credit_amount': updated_credits})
 | 
			
		||||
            .eq('id', user_id)
 | 
			
		||||
            .execute()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Check if the update was successful
 | 
			
		||||
        if update_response.data:
 | 
			
		||||
            self.logger.debug(f'Credit balance successfully decremented.')
 | 
			
		||||
            return True
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception("Error decrementing credit balance.")
 | 
			
		||||
 | 
			
		||||
        except Exception:
 | 
			
		||||
            # Handle exceptions (like if the user ID doesn't exist or there are database issues)
 | 
			
		||||
            raise HTTPException(status_code=500, detail="Error decrementing credit balance")
 | 
			
		||||
 | 
			
		||||
    def increment_credit_balance(self, user_id: str) -> bool:
 | 
			
		||||
        """
 | 
			
		||||
@@ -125,29 +140,32 @@ class Supabase:
 | 
			
		||||
                .single()
 | 
			
		||||
                .execute()
 | 
			
		||||
            )
 | 
			
		||||
            # Check if the response was successful and contains data
 | 
			
		||||
            if response.data :
 | 
			
		||||
                current_credits = response.data['credit_amount']
 | 
			
		||||
                updated_credits = current_credits + 1
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            if e.code == '22P02' :
 | 
			
		||||
                self.logger.error(f"Failed incrementing credits : {str(e)}")
 | 
			
		||||
                raise SyntaxError(f"Failed incrementing credits : {str(e)}") from e
 | 
			
		||||
            if e.code == 'PGRST116' :
 | 
			
		||||
                self.logger.error(f"User not found : {str(e)}")
 | 
			
		||||
                raise ValueError(f"User not found : {str(e)}") from e
 | 
			
		||||
            else :
 | 
			
		||||
                self.logger.error(f"An unexpected error occured while incrementing user balance : {str(e)}")
 | 
			
		||||
                raise Exception(f"An unexpected error occured while incrementing user balance : {str(e)}") from e
 | 
			
		||||
 | 
			
		||||
                # Update the user's credits in the table
 | 
			
		||||
                update_response = (
 | 
			
		||||
                    self.supabase.table('credits')
 | 
			
		||||
                    .update({'credit_amount': updated_credits})
 | 
			
		||||
                    .eq('id', user_id)
 | 
			
		||||
                    .execute()
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                # Check if the update was successful
 | 
			
		||||
                if update_response.data:
 | 
			
		||||
                    self.logger.debug(f'Credit balance successfully incremented.')
 | 
			
		||||
                    return True
 | 
			
		||||
                else:
 | 
			
		||||
                    raise HTTPException(status_code=500, detail="Error incrementing credit balance.")
 | 
			
		||||
        current_credits = response.data['credit_amount']
 | 
			
		||||
        updated_credits = current_credits + 1
 | 
			
		||||
 | 
			
		||||
            # If user or credits not found, raise a 404 error
 | 
			
		||||
            raise HTTPException(status_code=404, detail="User data not found.")
 | 
			
		||||
        # Update the user's credits in the table
 | 
			
		||||
        update_response = (
 | 
			
		||||
            self.supabase.table('credits')
 | 
			
		||||
            .update({'credit_amount': updated_credits})
 | 
			
		||||
            .eq('id', user_id)
 | 
			
		||||
            .execute()
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        except Exception:
 | 
			
		||||
            # Handle exceptions (like if the user ID doesn't exist or there are database issues)
 | 
			
		||||
            raise HTTPException(status_code=500, detail="Error incrementing credit balance")
 | 
			
		||||
        # Check if the update was successful
 | 
			
		||||
        if update_response.data:
 | 
			
		||||
            self.logger.debug(f'Credit balance successfully decremented.')
 | 
			
		||||
            return True
 | 
			
		||||
        else:
 | 
			
		||||
            raise Exception("Error decrementing credit balance.")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								backend/src/structs/requests.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								backend/src/structs/requests.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
from pydantic import BaseModel
 | 
			
		||||
 | 
			
		||||
class UserDeleteRequest(BaseModel):
 | 
			
		||||
    user_id: str
 | 
			
		||||
@@ -62,3 +62,32 @@ def test_input(invalid_client, start, preferences, status_code):   # pylint: dis
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    assert response.status_code == status_code
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.mark.parametrize(
 | 
			
		||||
    "user_id,status_code",
 | 
			
		||||
    [
 | 
			
		||||
        # No user id :
 | 
			
		||||
        ({}, 422),
 | 
			
		||||
        ("invalid_user_id", 400),
 | 
			
		||||
        # ("12345678-1234-5678-1234-567812345678", 406)
 | 
			
		||||
    ]
 | 
			
		||||
)
 | 
			
		||||
def test_input(invalid_client, user_id, status_code):   # pylint: disable=redefined-outer-name
 | 
			
		||||
    """
 | 
			
		||||
    Test new trip creation with invalid user ID.
 | 
			
		||||
    """
 | 
			
		||||
    response = invalid_client.post(
 | 
			
		||||
        "/trip/new",
 | 
			
		||||
        json={
 | 
			
		||||
            "user_id": user_id,
 | 
			
		||||
            "preferences": {"sightseeing": {"type": "sightseeing", "score": 5},
 | 
			
		||||
                "nature": {"type": "nature", "score": 0},
 | 
			
		||||
                "shopping": {"type": "shopping", "score": 0},
 | 
			
		||||
                "max_time_minute": 20,
 | 
			
		||||
                "detour_tolerance_minute": 0},
 | 
			
		||||
            "start": [48.084588, 7.280405]
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    assert response.status_code == status_code
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										68
									
								
								backend/src/tests/test_user.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								backend/src/tests/test_user.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
"""Collection of tests to ensure correct handling of user data."""
 | 
			
		||||
 | 
			
		||||
from fastapi.testclient import TestClient
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
from ..main import app
 | 
			
		||||
 | 
			
		||||
TEST_EMAIL = "dummy@example.com"
 | 
			
		||||
TEST_PW = "DummyPassword123"
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope="module")
 | 
			
		||||
def client():
 | 
			
		||||
    """Client used to call the app."""
 | 
			
		||||
    return TestClient(app)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_user_handling(client) :
 | 
			
		||||
    """
 | 
			
		||||
    Test the creation of a new user.
 | 
			
		||||
    """
 | 
			
		||||
    # Create a new user
 | 
			
		||||
    response = client.post(
 | 
			
		||||
        "/user/create", 
 | 
			
		||||
        json={
 | 
			
		||||
            "email": TEST_EMAIL,
 | 
			
		||||
            "password": TEST_PW
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    # Verify user has been created
 | 
			
		||||
    assert response.status_code == 200
 | 
			
		||||
    user_id = response.json()
 | 
			
		||||
 | 
			
		||||
    # # Create same user again to raise an error
 | 
			
		||||
    response = client.post(
 | 
			
		||||
        "/user/create", 
 | 
			
		||||
        json={
 | 
			
		||||
            "email": TEST_EMAIL,
 | 
			
		||||
            "password": TEST_PW
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    # Verify user already exists
 | 
			
		||||
    assert response.status_code == 422
 | 
			
		||||
 | 
			
		||||
    # Delete the user.
 | 
			
		||||
    response = client.post(
 | 
			
		||||
        "/user/delete", 
 | 
			
		||||
        json={
 | 
			
		||||
            "user_id": user_id
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    print(response)
 | 
			
		||||
    # Verify user has been deleted
 | 
			
		||||
    assert response.status_code == 200, "Failed to delete dummy user."
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    # Delete the user again to raise an error
 | 
			
		||||
    response = client.post(
 | 
			
		||||
        "/user/delete", 
 | 
			
		||||
        json={
 | 
			
		||||
            "user_id": user_id
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    print(response)
 | 
			
		||||
    # Verify user has been deleted
 | 
			
		||||
    assert response.status_code == 404, "Failed to delete dummy user."
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user