diff --git a/src/routes/shopping_cart.py b/src/routes/shopping_cart.py index b8d39ae..5a6ce55 100644 --- a/src/routes/shopping_cart.py +++ b/src/routes/shopping_cart.py @@ -44,7 +44,19 @@ router = APIRouter() -@router.get("/", response_model=CartResponse) +@router.get( + "/", + response_model=CartResponse, + summary="Retrieve user's shopping cart", + description=( + "

Fetch the user's shopping cart.

" + "Returns a list of movies currently in the user's shopping cart." + ), + responses={ + 404: {"description": "User not found."}, + 401: {"description": "Unauthorized request."} + } +) def get_cart( db: Annotated[Session, Depends(get_postgresql_db)], token: Annotated[str, Depends(get_token)], @@ -72,7 +84,20 @@ def get_cart( return CartResponse(user_id=user.id, movies=get_cart_items_details(cart)) -@router.post("/add", response_model=CartResponse) +@router.post( + "/add", + response_model=CartResponse, + summary="Add a movie to the shopping cart", + description=( + "

Add a movie to the user's shopping cart.

" + "Validates availability and ensures the movie is not already in the cart." + ), + responses={ + 404: {"description": "User or movie not found."}, + 400: {"description": "Movie already in cart."}, + 401: {"description": "Unauthorized request."} + } +) def add_to_cart( cart_data: CartCreate, db: Annotated[Session, Depends(get_postgresql_db)], @@ -110,7 +135,19 @@ def add_to_cart( return CartResponse(user_id=user.id, movies=get_cart_items_details(cart)) -@router.delete("/remove/{movie_id}", response_model=CartItemResponse) +@router.delete( + "/remove/{movie_id}", + response_model=CartItemResponse, + summary="Remove a movie from the shopping cart", + description=( + "

Removes a specific movie from the user's shopping cart.

" + "If the movie is not found in the cart, returns an error." + ), + responses={ + 404: {"description": "Movie or cart not found."}, + 401: {"description": "Unauthorized request."} + } +) def remove_from_cart( movie_id: int, db: Annotated[Session, Depends(get_postgresql_db)], @@ -145,7 +182,19 @@ def remove_from_cart( return CartItemResponse(message="Movie removed from cart") -@router.delete("/clear", response_model=CartItemResponse) +@router.delete( + "/clear", + response_model=CartItemResponse, + summary="Clear the shopping cart", + description=( + "

Removes all movies from the shopping cart.

" + "If the cart is already empty, returns an error." + ), + responses={ + 404: {"description": "Cart already empty."}, + 401: {"description": "Unauthorized request."} + } +) def clear_cart( db: Annotated[Session, Depends(get_postgresql_db)], token: Annotated[str, Depends(get_token)], @@ -176,7 +225,21 @@ def clear_cart( return CartItemResponse(message="Cart cleared successfully") -@router.post("/checkout", response_model=MessageResponseSchema) +@router.post( + "/checkout", + response_model=MessageResponseSchema, + summary="Checkout and complete purchase", + description=( + "

Processes the purchase of items in the shopping cart.

" + "Creates an order and clears the shopping cart." + ), + responses={ + 404: {"description": "User not found."}, + 403: {"description": "User not activated."}, + 400: {"description": "Cart is empty."}, + 401: {"description": "Unauthorized request."} + } +) def checkout( db: Annotated[Session, Depends(get_postgresql_db)], token: Annotated[str, Depends(get_token)], @@ -216,10 +279,24 @@ def checkout( process_order_payment_and_clear_cart(db, user, order, cart) - return MessageResponseSchema(message="Payment successful") + return MessageResponseSchema( + message="Order placed successfully. Payment has been created." + ) -@router.get("/purchased", response_model=PurchasedMoviesResponse) +@router.get( + "/purchased", + response_model=PurchasedMoviesResponse, + summary="Retrieve purchased movies", + description=( + "

Fetch a list of movies the user has purchased.

" + "Returns a list of movie names that the user has successfully bought." + ), + responses={ + 404: {"description": "User not found."}, + 401: {"description": "Unauthorized request."} + } +) def get_purchased_movies( db: Annotated[Session, Depends(get_postgresql_db)], token: Annotated[str, Depends(get_token)], @@ -247,7 +324,20 @@ def get_purchased_movies( ) -@router.get("/admin/{user_id}", response_model=CartResponse) +@router.get( + "/admin/{user_id}", + response_model=CartResponse, + summary="Retrieve a user's cart as an admin", + description=( + "

Allows an admin to view a specific user's shopping cart.

" + "Admin access is required." + ), + responses={ + 404: {"description": "User or cart not found."}, + 403: {"description": "Access denied."}, + 401: {"description": "Unauthorized request."} + } +) def get_user_cart_admin( user_id: int, db: Annotated[Session, Depends(get_postgresql_db)], @@ -280,7 +370,18 @@ def get_user_cart_admin( @router.delete( "/admin/movies/{movie_id}", - response_model=MessageResponseSchema + response_model=MessageResponseSchema, + summary="Delete a movie from the system", + description=( + "

Allows an admin or moderator to delete a movie from the system.

" + "If the movie is present in any user's cart, deletion is denied." + ), + responses={ + 404: {"description": "Movie not found."}, + 403: {"description": "Access denied."}, + 400: {"description": "Movie is in user carts and cannot be deleted."}, + 401: {"description": "Unauthorized request."} + } ) def delete_movie_route( movie_id: int, diff --git a/src/tests/test_shopping_carts.py b/src/tests/test_shopping_carts.py new file mode 100644 index 0000000..f7b97e0 --- /dev/null +++ b/src/tests/test_shopping_carts.py @@ -0,0 +1,174 @@ +import os +from unittest.mock import patch + +import pytest +from fastapi.testclient import TestClient + +from database import Movie, User, get_db +from main import app + + +@pytest.fixture(scope="session", autouse=True) +def mock_env_vars(): + with patch.dict(os.environ, { + "SECRET_KEY_ACCESS": "test_secret_key_access", + "SECRET_KEY_REFRESH": "test_secret_key_refresh", + "STRIPE_SECRET_KEY": "test_stripe_secret_key", + "STRIPE_PUBLIC_KEY": "test_stripe_public_key", + "STRIPE_WEBHOOK_SECRET": "test_stripe_webhook_secret" + }): + yield + + +@pytest.fixture(scope="function") +def client(db_session): + def override_get_db(): + yield db_session + + app.dependency_overrides[get_db] = override_get_db + return TestClient(app) + + +@pytest.fixture +def create_movie(db_session): + movie = Movie(name="Test Movie", price=9.99) + db_session.add(movie) + db_session.commit() + db_session.refresh(movie) + return movie + + +@pytest.fixture +def create_user(db_session): + user = User(email="testuser@example.com", is_active=True) + db_session.add(user) + db_session.commit() + db_session.refresh(user) + return user + + +@pytest.fixture +def auth_headers(): + return {"Authorization": "Bearer test-token"} + + +def test_get_cart_empty(client, auth_headers): + response = client.get("/api/v1/cart/", headers=auth_headers) + assert response.status_code == 200 + assert response.json()["movies"] == [] + + +def test_add_to_cart(client, auth_headers, create_movie): + movie = create_movie + response = client.post( + "/api/v1/cart/add", + json={"movie_id": movie.id}, + headers=auth_headers + ) + assert response.status_code == 200 + assert any(movie_item["id"] == movie.id for movie_item in response.json()["movies"]) + + +def test_add_to_cart_duplicate(client, auth_headers, create_movie): + movie = create_movie + client.post("/api/v1/cart/add", json={"movie_id": movie.id}, headers=auth_headers) + response = client.post( + "/api/v1/cart/add", + json={"movie_id": movie.id}, + headers=auth_headers + ) + assert response.status_code == 400 + assert response.json()["detail"] == "Movie is already in the cart" + + +def test_remove_from_cart(client, auth_headers, create_movie): + movie = create_movie + client.post("/api/v1/cart/add", json={"movie_id": movie.id}, headers=auth_headers) + response = client.delete(f"/api/v1/cart/remove/{movie.id}", headers=auth_headers) + assert response.status_code == 200 + + +def test_remove_nonexistent_movie(client, auth_headers): + response = client.delete("/api/v1/cart/remove/99999", headers=auth_headers) + assert response.status_code == 404 + assert response.json()["detail"] == "Movie not in cart" + + +def test_clear_cart(client, auth_headers, create_movie): + movie = create_movie + client.post("/api/v1/cart/add", json={"movie_id": movie.id}, headers=auth_headers) + response = client.delete("/api/v1/cart/clear", headers=auth_headers) + assert response.status_code == 200 + + +def test_clear_empty_cart(client, auth_headers): + response = client.delete("/api/v1/cart/clear", headers=auth_headers) + assert response.status_code == 404 + assert response.json()["detail"] == "Cart is already empty" + + +def test_checkout(client, auth_headers, create_movie): + movie = create_movie + client.post("/api/v1/cart/add", json={"movie_id": movie.id}, headers=auth_headers) + response = client.post("/api/v1/cart/checkout", headers=auth_headers) + assert response.status_code == 200 + + +def test_checkout_empty_cart(client, auth_headers): + response = client.post("/api/v1/cart/checkout", headers=auth_headers) + assert response.status_code == 400 + + +def test_checkout_inactive_user(client, auth_headers, create_movie): + movie = create_movie + response = client.post( + "/api/v1/cart/add", + json={"movie_id": movie.id}, + headers=auth_headers + ) + assert response.status_code == 403 + assert response.json()["detail"] == ("Please activate your " + "account before making a purchase.") + + +def test_get_purchased_movies(client, auth_headers, create_movie): + movie = create_movie + client.post("/api/v1/cart/add", json={"movie_id": movie.id}, headers=auth_headers) + client.post("/api/v1/cart/checkout", headers=auth_headers) + response = client.get("/api/v1/cart/purchased", headers=auth_headers) + assert response.status_code == 200 + assert movie.name in response.json()["purchased_movies"] + + +def test_get_user_cart_admin(client, auth_headers, create_movie, create_user): + user = create_user + movie = create_movie + client.post("/api/v1/cart/add", json={"movie_id": movie.id}, headers=auth_headers) + response = client.get(f"/api/v1/cart/admin/{user.id}", headers=auth_headers) + assert response.status_code == 200 + user_cart_movies = response.json()["movies"] + assert any(movie_item["id"] == movie.id for movie_item in user_cart_movies) + + +def test_delete_movie(client, auth_headers, create_movie): + movie = create_movie + response = client.delete( + f"/api/v1/cart/admin/movies/{movie.id}", + headers=auth_headers + ) + assert response.status_code == 200 + + +def test_delete_movie_not_found(client, auth_headers): + response = client.delete("/api/v1/cart/admin/movies/99999", headers=auth_headers) + assert response.status_code == 404 + assert response.json()["detail"] == "Movie not found" + + +def test_admin_access_restricted(client, auth_headers, create_movie): + movie = create_movie + response = client.delete( + f"/api/v1/cart/admin/movies/{movie.id}", + headers=auth_headers + ) + assert response.status_code in [403, 401]