From da52b4e6cebfd57625cfd7219c4844b3824f7459 Mon Sep 17 00:00:00 2001 From: cisc0f Date: Sat, 27 Apr 2024 11:20:02 -0400 Subject: [PATCH] Fixed BDD testing + almost completed --- README.md | 2 +- features/steps/shopcarts_steps.py | 3 +- service/common/error_handlers.py | 20 +- service/models/shop_cart.py | 4 +- service/models/shop_cart_item.py | 2 +- service/routes.py | 436 +++++++++++++++++------------- tests/factories.py | 22 +- tests/test_routes.py | 76 +++--- tests/test_shop_cart.py | 4 +- tests/test_shop_cart_item.py | 4 +- 10 files changed, 323 insertions(+), 250 deletions(-) diff --git a/README.md b/README.md index f163c38..1c5a026 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # NYU DevOps Project Template -[![CI Build](https://github.com/CSCI-GA-2820-SP24-003/shopcarts/actions/workflows/ci.yml/badge.svg)](https://github.com/CSCI-GA-2820-SP24-003/shopcarts/actions/workflows/ci.yml) +[![CI Build](https://github.com/CSCI-GA-2820-SP24-003/shopcarts/actions/workflows/ci.yml/badge.svg)](https://github.com/CSCI-GA-2820-SP24-003/shopcarts/actions/workflows/tdd.yml) [![codecov](https://codecov.io/gh/CSCI-GA-2820-SP24-003/shopcarts/graph/badge.svg?token=MM379ZQE9D)](https://codecov.io/gh/CSCI-GA-2820-SP24-003/shopcarts) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) diff --git a/features/steps/shopcarts_steps.py b/features/steps/shopcarts_steps.py index 29b05b6..8d0dc6b 100644 --- a/features/steps/shopcarts_steps.py +++ b/features/steps/shopcarts_steps.py @@ -1,3 +1,4 @@ +# spell: ignore shopcarts shopcart """ Shopcart Steps @@ -19,7 +20,7 @@ def step_impl(context): """ Delete all shopcarts and load new ones """ # List all of the shopcarts and delete them one by one - rest_endpoint = f"{context.base_url}/shopcarts" + rest_endpoint = f"{context.base_url}/api/shopcarts" context.resp = requests.get(rest_endpoint) assert(context.resp.status_code == HTTP_200_OK) for shopcart in context.resp.json(): diff --git a/service/common/error_handlers.py b/service/common/error_handlers.py index 1d8c8f1..33ca89a 100644 --- a/service/common/error_handlers.py +++ b/service/common/error_handlers.py @@ -17,7 +17,6 @@ Module: error_handlers """ from service import api -from flask import jsonify from flask import current_app as app # Import Flask application from service.models import DataValidationError from . import status @@ -29,17 +28,10 @@ @api.errorhandler(DataValidationError) def request_validation_error(error): """Handles Value Errors from bad data""" - return bad_request(error) - - -@app.errorhandler(status.HTTP_400_BAD_REQUEST) -def bad_request(error): - """Handles bad requests with 400_BAD_REQUEST""" message = str(error) - app.logger.warning(message) - return ( - jsonify( - status=status.HTTP_400_BAD_REQUEST, error="Bad Request", message=message - ), - status.HTTP_400_BAD_REQUEST, - ) + app.logger.error(message) + return { + "status_code": status.HTTP_400_BAD_REQUEST, + "error": "Bad Request", + "message": message, + }, status.HTTP_400_BAD_REQUEST diff --git a/service/models/shop_cart.py b/service/models/shop_cart.py index e0b9657..b3bd64f 100644 --- a/service/models/shop_cart.py +++ b/service/models/shop_cart.py @@ -50,7 +50,7 @@ def serialize(self) -> dict: "id": self.id, "user_id": self.user_id, "name": self.name, - "total_price": self.total_price, + "total_price": float(self.total_price), "status": self.status.name, "items": [], } @@ -151,5 +151,5 @@ def find_by_status(cls, status): Args: status (ShopCartStatus): the status of the ShopCarts you want to match """ - logger.info("Processing status query for %s ...", status.name) + logger.info("Processing status query for %s ...", status) return cls.query.filter(cls.status == status).all() diff --git a/service/models/shop_cart_item.py b/service/models/shop_cart_item.py index 52362db..8f873c9 100644 --- a/service/models/shop_cart_item.py +++ b/service/models/shop_cart_item.py @@ -38,7 +38,7 @@ def serialize(self) -> dict: "name": self.name, "product_id": self.product_id, "quantity": self.quantity, - "price": self.price, + "price": float(self.price), } def deserialize(self, data): diff --git a/service/routes.py b/service/routes.py index 1d88e0c..d7d77a9 100644 --- a/service/routes.py +++ b/service/routes.py @@ -21,9 +21,9 @@ and Delete Shop Carts """ from decimal import Decimal -from flask import jsonify, request, url_for, abort +from flask import jsonify, request, abort from flask import current_app as app # Import Flask application -from flask_restx import Resource, fields # , reqparse, inputs +from flask_restx import Resource, fields, reqparse # , inputs from service.models import ShopCart, ShopCartItem from service.models.shop_cart import ShopCartStatus from service.common import status # HTTP Status Codes @@ -54,7 +54,7 @@ def read_health(): "Item", { "id": fields.Integer(required=True, description="Id of the shopcart item"), - "shopcart_id": fields.Integer(required=True, description="Id of the shopcart"), + "shop_cart_id": fields.Integer(required=True, description="Id of the shopcart"), "name": fields.String(required=True, description="Name of the item"), "product_id": fields.Integer(required=True, description="Id of the product"), "quantity": fields.Integer(required=True, description="Quantity of product"), @@ -70,7 +70,7 @@ def read_health(): readOnly=True, description="The Id of the item assigned internally by the service", ), - "shopcart_id": fields.Integer( + "shop_cart_id": fields.Integer( readOnly=True, description="The Id of the shopcart to which the item belongs", ), @@ -88,6 +88,7 @@ def read_health(): "total_price": fields.Float( required=True, description="Total price of the shopcart" ), + # pylint: disable=protected-access "status": fields.String( enum=ShopCartStatus._member_names_, description="Status of the shopcart", @@ -116,15 +117,37 @@ def read_health(): }, ) +# query string arguments +shopcart_args = reqparse.RequestParser() +shopcart_args.add_argument( + "name", type=str, location="args", required=False, help="List Shopcarts by name" +) +shopcart_args.add_argument( + "status", type=str, location="args", required=False, help="List Shopcarts by status" +) +shopcart_args.add_argument( + "user_id", type=int, location="args", required=False, help="List Shopcarts by User ID" +) -@api.route("/shopcarts/") -@api.param("shopcart_id", "The Shopcart identifier") + +###################################################################### +# PATH: /shopcarts/{id} +###################################################################### +@api.route("/shopcarts/") +@api.param("shopcart_id", "The shopcart identifier") class ShopcartResource(Resource): - """Handles interactions with Shopcarts""" + """ + ShopcartResource class + + Allows the manipulation of a single Shopcart + GET /shopcart{id} - Returns a Shopcart with the id + PUT /shopcart{id} - Update a Shopcart with the id + DELETE /shopcart{id} - Deletes a Shopcart with the id + """ - ###################################################################### + # ------------------------------------------------------------------ # RETRIEVE A SHOPCART - ###################################################################### + # ------------------------------------------------------------------ @api.doc("get_shopcarts") @api.response(404, "Shopcart not found") @api.marshal_with(shopcart_model) @@ -144,36 +167,17 @@ def get(self, shopcart_id): f"Shopcart with id '{shopcart_id}' could not be found.", ) - return jsonify(shopcart.serialize()), status.HTTP_200_OK + return shopcart.serialize(), status.HTTP_200_OK - ###################################################################### - # DELETE A SHOPCART - ###################################################################### - @api.doc("delete_shopcarts") - @api.response(204, "Shopcart deleted") - def delete(self, shopcart_id): - """ - Delete a Shopcart - - This endpoint will delete a Shopcart based the id specified in the path - """ - app.logger.info("Request to delete shopcart with id: %d", shopcart_id) - - shopcart = ShopCart.find(shopcart_id) - if shopcart: - shopcart.delete() - - app.logger.info("Shopcart with ID: %d delete complete.", shopcart_id) - return "", status.HTTP_204_NO_CONTENT - - ###################################################################### - # UPDATE AN EXISTING ShopCart - ###################################################################### + # ------------------------------------------------------------------ + # UPDATE AN EXISTING SHOPCART + # ------------------------------------------------------------------ @api.doc("update_shopcarts") @api.response(404, "Shopcart not found") - @api.response(400, "The posted Shopcart data was not valid") - @api.response(415, "Invalid header content-type") - def update(self, shopcart_id): + @api.response(400, "The posted shopcart data was not valid") + @api.expect(shopcart_model) + @api.marshal_with(shopcart_model) + def put(self, shopcart_id): """ Update a ShopCart @@ -188,24 +192,77 @@ def update(self, shopcart_id): status.HTTP_404_NOT_FOUND, f"ShopCart with id: '{shopcart_id}' was not found.", ) - - shopcart.deserialize(request.get_json()) + app.logger.debug("Payload = %s", api.payload) + data = api.payload + shopcart.deserialize(data) shopcart.id = shopcart_id shopcart.update() app.logger.info("ShopCart with ID: %d updated.", shopcart.id) - return jsonify(shopcart.serialize()), status.HTTP_200_OK + return shopcart.serialize(), status.HTTP_200_OK + # ------------------------------------------------------------------ + # DELETE A SHOPCART + # ------------------------------------------------------------------ + @api.doc("delete_shopcarts") + @api.response(204, "Shopcart deleted") + def delete(self, shopcart_id): + """ + Delete a Shopcart + This endpoint will delete a Shopcart based the id specified in the path + """ + app.logger.info("Request to delete shopcart with id: %d", shopcart_id) + + shopcart = ShopCart.find(shopcart_id) + if shopcart: + shopcart.delete() + app.logger.info("Shopcart with ID: %d delete complete.", shopcart_id) + return "", status.HTTP_204_NO_CONTENT + + +###################################################################### +# PATH: /shopcarts +###################################################################### @api.route("/shopcarts", strict_slashes=False) class ShopcartCollection(Resource): - """Handles interactions with collections of Shopcarts""" + """Handles all interactions with collections of Shopcarts""" + + # ------------------------------------------------------------------ + # LIST ALL SHOPCARTS + # ------------------------------------------------------------------ + @api.doc("list_shopcarts") + @api.expect(shopcart_args, validate=True) + @api.marshal_list_with(shopcart_model) + def get(self): + """List all shop carts""" + app.logger.info("Request for Shop Cart list") + shop_carts = [] + args = shopcart_args.parse_args() + + if args.get("user_id"): + app.logger.info("Filtering by user_id: %s", args.get("user_id")) + shop_carts = ShopCart.find_by_user_id(args.get("user_id")) + elif args.get("name"): + app.logger.info("Filtering by name: %s", args.get("name")) + shop_carts = ShopCart.find_by_name(args.get("name")).all() + elif args.get("status"): + app.logger.info("Filtering by status: %s", args.get("status")) + shop_carts = ShopCart.find_by_status(args.get("status")) + else: + app.logger.info("Returning unfiltered list.") + shop_carts = ShopCart.all() + + app.logger.info("[%s] shopcarts returned", len(shop_carts)) + results = [shop_cart.serialize() for shop_cart in shop_carts] + return results, status.HTTP_200_OK - ###################################################################### - # CREATE A NEW SHOPCART - ###################################################################### + # ------------------------------------------------------------------ + # ADD A NEW SHOPCART + # ------------------------------------------------------------------ @api.doc("create_shopcarts") - @api.response(400, "Invalid shopcart request body") + @api.response(400, "The posted data was not valid") + @api.expect(create_shopcart_model) @api.marshal_with(shopcart_model, code=201) def post(self): """ @@ -217,130 +274,134 @@ def post(self): # Create the shopcart shopcart = ShopCart() - shopcart.deserialize(request.get_json()) + app.logger.debug("Payload = %s", api.payload) + shopcart.deserialize(api.payload) shopcart.create() # Create a message to return - message = shopcart.serialize() - location_url = url_for(ShopcartResource, shopcart_id=shopcart.id, _external=True) - - return jsonify(message), status.HTTP_201_CREATED, {"Location": location_url} - - ###################################################################### - # LIST ALL SHOPCARTS - ###################################################################### - @api.doc("list_shopcarts") - @api.marshal_list_with(shopcart_model) - def get(self): - """List all shop carts""" - app.logger.info("Request for Shop Cart list") - shop_carts = [] + app.logger.info("shopcart with new id [%s] created!", shopcart.id) + location_url = api.url_for(ShopcartResource, shopcart_id=shopcart.id, _external=True) - name = request.args.get("name") - if name: - shop_carts = ShopCart.find_by_name(name) - else: - shop_carts = ShopCart.all() - - results = [shop_cart.serialize() for shop_cart in shop_carts] - - return jsonify(results), status.HTTP_200_OK + return shopcart.serialize(), status.HTTP_201_CREATED, {"Location": location_url} ###################################################################### -# UPDATE A ShopCart STATUS +# PATH: /shopcarts/{shopcart_id}/status ###################################################################### -@app.route("/shopcarts//status", methods=["PATCH"]) -def update_shopcart_status(shopcart_id): - """ - Update a ShopCart status +@api.route("/shopcarts//status") +@api.param("shopcart_id", "The shopcart identifier") +class UpdateStatusResource(Resource): + """update a shopcart status""" - This endpoint will update a ShopCart's status based the body that is posted - """ - app.logger.info("Request to update shopcart with id: %d", shopcart_id) - check_content_type("application/json") + @api.doc("update_shopcart_status") + @api.response(404, "Shopcart not found") + @api.response(409, "The Shopcart is not available") + def patch(self, shopcart_id): + """ + Update a ShopCart status - shopcart = ShopCart.find(shopcart_id) - if not shopcart: - error( - status.HTTP_404_NOT_FOUND, - f"ShopCart with id: '{shopcart_id}' was not found.", - ) + This endpoint will update a ShopCart's status based the body that is posted + """ + app.logger.info("Request to update shopcart with id: %d", shopcart_id) + check_content_type("application/json") - data = request.get_json() - if "status" in data: - shopcart.status = ShopCartStatus[data["status"]] - shopcart.update() - else: - error(status.HTTP_400_BAD_REQUEST, "status field was not found") + shopcart = ShopCart.find(shopcart_id) + if not shopcart: + error( + status.HTTP_404_NOT_FOUND, + f"ShopCart with id: '{shopcart_id}' was not found.", + ) + + app.logger.debug("Payload = %s", api.payload) + data = api.payload + if "status" in data: + shopcart.status = ShopCartStatus[data["status"]] + shopcart.update() + else: + error(status.HTTP_400_BAD_REQUEST, "status field was not found") - app.logger.info( - "ShopCart with ID: %d updated the status %s.", shopcart.id, data["status"] - ) - return jsonify(shopcart.serialize()), status.HTTP_200_OK + app.logger.info( + "ShopCart with ID: %d updated the status %s.", shopcart.id, data["status"] + ) + return shopcart.serialize(), status.HTTP_200_OK ###################################################################### -# SORT SHOPCARTS BY STATUS +# PATH: /shopcarts/status/{status_name} ###################################################################### -@app.route("/shopcarts/status/", methods=["GET"]) -def sort_shopcarts_by_status(status_name): - """ - Sort ShopCarts by Status +@api.route("/shopcarts/status/") +@api.param("shopcart_status", "The shopcart status") +class FindStatusResource(Resource): + """find shopcarts by status""" + + @api.doc("find_shopcart_by_status") + @api.response(404, "status not found") + def get(self, status_name): + """ + Sort ShopCarts by Status - This endpoint will return the ShopCarts sorted by the given status - """ - app.logger.info("Request to sort ShopCarts by Status: %s", status_name) - - # Convert the status string to ShopCartStatus enum - try: - status_enum = ShopCartStatus[status_name.upper()] - except KeyError: - abort( - status.HTTP_400_BAD_REQUEST, - f"Invalid status value: '{status_name}'. Must be one of {[s.name for s in ShopCartStatus]}", - ) + This endpoint will return the ShopCarts sorted by the given status + """ + app.logger.info("Request to sort ShopCarts by Status: %s", status_name) + + # Convert the status string to ShopCartStatus enum + try: + status_enum = ShopCartStatus[status_name.upper()] + except KeyError: + abort( + status.HTTP_400_BAD_REQUEST, + f"Invalid status value: '{status_name}'. Must be one of {[s.name for s in ShopCartStatus]}", + ) - shopcarts = ShopCart.find_by_status(status_enum) - results = [shopcart.serialize() for shopcart in shopcarts] + shopcarts = ShopCart.find_by_status(status_enum) + results = [shopcart.serialize() for shopcart in shopcarts] - return jsonify(results), status.HTTP_200_OK + return results, status.HTTP_200_OK ###################################################################### -# SEARCH SHOPCARTS BY USER_ID +# PATH: /shopcarts/user/{user_id} ###################################################################### -@app.route("/shopcarts/user/", methods=["GET"]) -def search_shopcarts_by_user_id(user_id): - """ - Search ShopCarts by User ID +@api.route("/shopcarts/user/") +@api.param("user_id", "The shopcart user_id") +class UserResource(Resource): + """find shopcarts by user_id""" + + @api.doc("find_shopcart_by_user_id") + @api.response(404, "status not found") + def get(self, user_id): + """ + Search ShopCarts by User ID - This endpoint will return the ShopCarts associated with the given user_id - """ - app.logger.info("Request to search ShopCarts by User ID: %s", user_id) + This endpoint will return the ShopCarts associated with the given user_id + """ + app.logger.info("Request to search ShopCarts by User ID: %s", user_id) - shopcarts = ShopCart.find_by_user_id(user_id) - if not shopcarts: - abort( - status.HTTP_404_NOT_FOUND, - f"No ShopCarts found for User ID '{user_id}'.", - ) + shopcarts = ShopCart.find_by_user_id(user_id) + if not shopcarts: + abort( + status.HTTP_404_NOT_FOUND, + f"No ShopCarts found for User ID '{user_id}'.", + ) - results = [shopcart.serialize() for shopcart in shopcarts] - return jsonify(results), status.HTTP_200_OK + results = [shopcart.serialize() for shopcart in shopcarts] + return results, status.HTTP_200_OK # --------------------------------------------------------------------- # I T E M M E T H O D S # --------------------------------------------------------------------- +###################################################################### +# PATH: /shopcarts/{shopcart_id}/items/{item_id} +###################################################################### @api.route("/shopcarts//items/") @api.param("shopcart_id", "The Shopcart identifier") @api.param("item_id", "The Item identifier") class ItemResource(Resource): """Handles interactions with Shopcart Items""" - ###################################################################### - # RETRIEVE AN ITEM FROM SHOPCART - ###################################################################### + # ------------------------------------------------------------------ + # GET SHOPCART ITEM + # ------------------------------------------------------------------ @api.doc("get_shopcart_items") @api.response(404, "Shopcart not found") @api.response(404, "Item not found") @@ -371,11 +432,11 @@ def get(self, shopcart_id, item_id): f"Item with id '{item_id}' could not be found.", ) - return jsonify(item.serialize()), status.HTTP_200_OK - - ###################################################################### - # DELETE AN ITEM FROM SHOPCART - ###################################################################### + return item.serialize(), status.HTTP_200_OK + + # ------------------------------------------------------------------ + # DELETE SHOPCART ITEM + # ------------------------------------------------------------------ @api.doc("delete_shopcart_items") @api.response(204, "Item deleted") @api.response(404, "Shopcart not found") @@ -405,10 +466,10 @@ def delete(self, shopcart_id, item_id): shopcart.update_total_price() return "", status.HTTP_204_NO_CONTENT - - ###################################################################### - # UPDATE A SHOPCART ITEM - ###################################################################### + + # ------------------------------------------------------------------ + # UPDATE SHOPCART ITEM + # ------------------------------------------------------------------ @api.doc("update_shopcart_item") @api.response(404, "Shopcart not found") @api.response(404, "Item not found") @@ -443,23 +504,26 @@ def put(self, shopcart_id, item_id): ) # Update from the json in the body of the request - item.deserialize(request.get_json()) + item.deserialize(api.payload) item.id = item_id item.update() # update the total price shopcart.update_total_price() - return jsonify(item.serialize()), status.HTTP_200_OK + return item.serialize(), status.HTTP_200_OK +###################################################################### +# PATH: /shopcarts/{shopcart_id}/items +###################################################################### @api.route("/shopcarts//items", strict_slashes=False) @api.param("shopcart_id", "The Shopcart identifier") class ItemCollection(Resource): """Handles interactions with collections of Shopcart Items""" - ###################################################################### - # CREATE A NEW SHOPCART ITEM - ###################################################################### + # ------------------------------------------------------------------ + # CREATE SHOPCART ITEM + # ------------------------------------------------------------------ @api.doc("create_shopcart_item") @api.response(400, "Invalid shopcart item request body") @api.response(404, "Shopcart not found") @@ -482,7 +546,7 @@ def post(self, shopcart_id): # Create item from json data item = ShopCartItem() - item.deserialize(request.get_json()) + item.deserialize(api.payload) # Append item to the shopcart # if the item does exists in the shopcart @@ -503,20 +567,19 @@ def post(self, shopcart_id): shopcart.update_total_price() # Create a message to return - message = item.serialize() - location_url = url_for( - "get_shopcart_items", shopcart_id=shopcart.id, item_id=item.id, _external=True + location_url = api.url_for( + ItemResource, shopcart_id=shopcart.id, item_id=item.id, _external=True ) return ( - jsonify(message), + item.serialize(), status.HTTP_201_CREATED, {"Location": location_url}, ) - - ###################################################################### - # LIST ITEMS IN A SHOPCART - ###################################################################### + + # ------------------------------------------------------------------ + # LIST SHOPCART ITEMS + # ------------------------------------------------------------------ @api.doc("list_shopcart_items") @api.response(404, "Shopcart not found") @api.marshal_list_with(item_model) @@ -557,40 +620,51 @@ def get(self, shopcart_id): results = [item.serialize() for item in filtered_items] app.logger.info("Returning %d items", len(results)) - return jsonify(results), status.HTTP_200_OK + return results, status.HTTP_200_OK ###################################################################### -# RETRIEVE AN ITEM FROM SHOPCART BY PRODUCT ID +# PATH: /shopcarts/{shopcart_id}/products/{product_id} ###################################################################### -@app.route("/shopcarts//products/", methods=["GET"]) -def get_shopcart_items_by_product_id(shopcart_id, product_id): - """ - Get an Item - - This endpoint returns just an item using the product id - """ - app.logger.info( - "Request to retrieve Item %s for ShopCart id: %s", (product_id, shopcart_id) - ) +@api.route("/shopcarts//products/") +@api.param("shopcart_id", "The Shopcart identifier") +@api.param("product_id", "The Product identifier") +class ProductResource(Resource): + """Handles interactions with Product Items""" + # ------------------------------------------------------------------ + # GET SHOPCART ITEM by PRODUCT ID + # ------------------------------------------------------------------ + @api.doc("get_shopcart_items_by_product_id") + @api.response(404, "Shopcart not found") + @api.response(404, "Item not found") + @api.marshal_with(item_model) + def get(self, shopcart_id, product_id): + """ + Get an Item - # Search for the shopcart - shopcart = ShopCart.find(shopcart_id) - if not shopcart: - abort( - status.HTTP_404_NOT_FOUND, - f"ShopCart with ID '{shopcart_id}' could not be found", + This endpoint returns just an item using the product id + """ + app.logger.info( + "Request to retrieve Item %s for ShopCart id: %s", (product_id, shopcart_id) ) - # See if the item exists and abort if it doesn't - item = ShopCartItem.find_by_product_id(product_id) - if not item: - abort( - status.HTTP_404_NOT_FOUND, - f"Product with id '{product_id}' could not be found.", - ) + # Search for the shopcart + shopcart = ShopCart.find(shopcart_id) + if not shopcart: + abort( + status.HTTP_404_NOT_FOUND, + f"ShopCart with ID '{shopcart_id}' could not be found", + ) + + # See if the item exists and abort if it doesn't + item = ShopCartItem.find_by_product_id(product_id) + if not item: + abort( + status.HTTP_404_NOT_FOUND, + f"Product with id '{product_id}' could not be found.", + ) - return jsonify(item.serialize()), status.HTTP_200_OK + return item.serialize(), status.HTTP_200_OK ###################################################################### diff --git a/tests/factories.py b/tests/factories.py index 2427467..7287919 100644 --- a/tests/factories.py +++ b/tests/factories.py @@ -1,24 +1,37 @@ """Test Factory""" +from decimal import Decimal +import random import factory from factory.fuzzy import FuzzyDecimal, FuzzyChoice from service.models import ShopCart, ShopCartItem from service.models.shop_cart import ShopCartStatus +class DecimalMine(factory.fuzzy.BaseFuzzyAttribute): + """Generates a random decimal number between low and high with precision.""" + def __init__(self, low, high, precision='0.01'): + super().__init__() + self.low = Decimal(low) + self.high = Decimal(high) + self.precision = Decimal(precision) + + def fuzz(self): + value = Decimal(random.uniform(float(self.low), float(self.high))).quantize(self.precision) + return value + + # pylint: disable=too-few-public-methods class ShopCartFactory(factory.Factory): """Creates fake shop cart instances""" - class Meta: """Maps factory to data model""" - model = ShopCart - id = factory.Sequence(lambda n: n) user_id = factory.Sequence(lambda n: n) name = factory.Sequence(lambda n: f"sc-{n}") total_price = FuzzyDecimal(0.00, 200.00) + total_price = DecimalMine(0.00, 200.00) status = FuzzyChoice( choices=[ShopCartStatus.ACTIVE, ShopCartStatus.PENDING, ShopCartStatus.INACTIVE] ) @@ -30,7 +43,6 @@ def items( """Creates the Shop Cart Items list""" if not create: return - if extracted: self.items = extracted @@ -38,10 +50,8 @@ def items( # pylint: disable=too-few-public-methods class ShopCartItemFactory(factory.Factory): """Creates fake shop cart item instances""" - class Meta: """Maps factory tp data model""" - model = ShopCartItem id = factory.Sequence(lambda n: n) diff --git a/tests/test_routes.py b/tests/test_routes.py index beae917..b74ed8d 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -4,8 +4,8 @@ import os import logging -from decimal import Decimal from unittest import TestCase +from decimal import Decimal, ROUND_DOWN from wsgi import app from service.common import status from service.models import db, ShopCart @@ -18,6 +18,8 @@ ) BASE_URL = "/api/shopcarts" +BASE_URL_ITEM = "/api/shopcarts" +CONTENT_TYPE_JSON = "application/json" MAX_NUM = 99999 @@ -103,8 +105,8 @@ def test_create_shopcart(self): ) self.assertEqual(new_shopcart["name"], shopcart.name, "name does not match") self.assertEqual( - new_shopcart["total_price"], - str(shopcart.total_price), + float(new_shopcart["total_price"]), + float(shopcart.total_price), "total_price does not match", ) @@ -117,8 +119,8 @@ def test_create_shopcart(self): new_shopcart["user_id"], shopcart.user_id, "user_id does not match" ) self.assertEqual( - new_shopcart["total_price"], - str(shopcart.total_price), + float(new_shopcart["total_price"]), + float(shopcart.total_price), "total_price does not match", ) self.assertEqual(new_shopcart["items"], shopcart.items, "items does not match") @@ -179,8 +181,7 @@ def test_update_shopcart(self): "user_id" ], # Assuming user_id can be updated or is needed for identification "name": "Updated Name", - "total_price": Decimal(new_shopcart["total_price"]) - + 100, # Example of updating the price + "total_price": Decimal(new_shopcart["total_price"]) + Decimal('100'), # Example of updating the price # Include updates to other fields here "status": new_shopcart["status"], } @@ -194,9 +195,9 @@ def test_update_shopcart(self): # Verify that all fields have been updated correctly self.assertEqual(updated_shopcart["name"], update_payload["name"]) - self.assertEqual( - updated_shopcart["total_price"], str(update_payload["total_price"]) - ) + expected_price = update_payload["total_price"].quantize(Decimal('.001'), rounding=ROUND_DOWN) + actual_price = Decimal(updated_shopcart["total_price"]).quantize(Decimal('.001'), rounding=ROUND_DOWN) + self.assertEqual(actual_price, expected_price) def test_update_shop_cart_with_invalid_fields(self): """It should not update a shopcart with invalid fields and maintain required fields""" @@ -384,16 +385,16 @@ def test_sort_shopcarts_by_invalid_status(self): resp = self.client.get(f"{BASE_URL}/status/invalid") self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) - # --------------------------------------------------------------------- - # I T E M M E T H O D S - # --------------------------------------------------------------------- + # # --------------------------------------------------------------------- + # # I T E M M E T H O D S + # # --------------------------------------------------------------------- def test_create_shopcart_item(self): """It should add an item to a shop cart""" shop_cart = self._create_shopcarts(1)[0] item = ShopCartItemFactory() resp = self.client.post( - f"{BASE_URL}/{shop_cart.id}/items", + f"{BASE_URL_ITEM}/{shop_cart.id}/items", json=item.serialize(), content_type="application/json", ) @@ -403,7 +404,7 @@ def test_create_shopcart_item(self): self.assertEqual(data["product_id"], item.product_id) self.assertEqual(data["shop_cart_id"], shop_cart.id) self.assertEqual(data["quantity"], item.quantity) - self.assertEqual(data["price"], str(item.price)) + self.assertEqual(data["price"], float(item.price)) def test_create_shopcart_duplicate_items(self): """when adding an item to a shop cart, @@ -414,7 +415,7 @@ def test_create_shopcart_duplicate_items(self): item = ShopCartItemFactory() item.quantity = 2 resp = self.client.post( - f"{BASE_URL}/{shop_cart.id}/items", + f"{BASE_URL_ITEM}/{shop_cart.id}/items", json=item.serialize(), content_type="application/json", ) @@ -423,7 +424,7 @@ def test_create_shopcart_duplicate_items(self): data_1 = resp.get_json() resp = self.client.post( - f"{BASE_URL}/{shop_cart.id}/items", + f"{BASE_URL_ITEM}/{shop_cart.id}/items", json=item.serialize(), content_type="application/json", ) @@ -437,7 +438,7 @@ def test_get_shopcart_item(self): shopcart = self._create_shopcarts(1)[0] item = ShopCartItemFactory() resp = self.client.post( - f"{BASE_URL}/{shopcart.id}/items", + f"{BASE_URL_ITEM}/{shopcart.id}/items", json=item.serialize(), content_type="application/json", ) @@ -449,7 +450,7 @@ def test_get_shopcart_item(self): # retrieve it back resp = self.client.get( - f"{BASE_URL}/{shopcart.id}/items/{item_id}", + f"{BASE_URL_ITEM}/{shopcart.id}/items/{item_id}", content_type="application/json", ) self.assertEqual(resp.status_code, status.HTTP_200_OK) @@ -460,7 +461,7 @@ def test_get_shopcart_item(self): self.assertEqual(data["name"], item.name) self.assertEqual(data["product_id"], item.product_id) self.assertEqual(data["quantity"], item.quantity) - self.assertEqual(data["price"], str(item.price)) + self.assertEqual(data["price"], float(item.price)) def test_get_shopcart_item_when_no_shopcart(self): """It should Get an error when a shopcart id does not exist @@ -469,7 +470,7 @@ def test_get_shopcart_item_when_no_shopcart(self): shopcart = self._create_shopcarts(1)[0] item = ShopCartItemFactory() resp = self.client.post( - f"{BASE_URL}/{shopcart.id}/items", + f"{BASE_URL_ITEM}/{shopcart.id}/items", json=item.serialize(), content_type="application/json", ) @@ -481,7 +482,7 @@ def test_get_shopcart_item_when_no_shopcart(self): # retrieve it back resp = self.client.get( - f"{BASE_URL}/{shopcart.id + MAX_NUM}/items/{item_id}", + f"{BASE_URL_ITEM}/{shopcart.id + MAX_NUM}/items/{item_id}", content_type="application/json", ) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) @@ -492,7 +493,7 @@ def test_get_shopcart_item_by_product_id(self): shopcart = self._create_shopcarts(1)[0] item = ShopCartItemFactory() resp = self.client.post( - f"{BASE_URL}/{shopcart.id}/items", + f"{BASE_URL_ITEM}/{shopcart.id}/items", json=item.serialize(), content_type="application/json", ) @@ -504,7 +505,7 @@ def test_get_shopcart_item_by_product_id(self): # retrieve it back resp = self.client.get( - f"{BASE_URL}/{shopcart.id}/products/{product_id}", + f"{BASE_URL_ITEM}/{shopcart.id}/products/{product_id}", content_type="application/json", ) self.assertEqual(resp.status_code, status.HTTP_200_OK) @@ -515,11 +516,11 @@ def test_get_shopcart_item_by_product_id(self): self.assertEqual(data["name"], item.name) self.assertEqual(data["product_id"], item.product_id) self.assertEqual(data["quantity"], item.quantity) - self.assertEqual(data["price"], str(item.price)) + self.assertEqual(data["price"], float(item.price)) # when shopcart does not exist resp = self.client.get( - f"{BASE_URL}/{shopcart.id+ MAX_NUM}/products/{product_id}", + f"{BASE_URL_ITEM}/{shopcart.id+ MAX_NUM}/products/{product_id}", content_type="application/json", ) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) @@ -533,7 +534,7 @@ def test_delete_shopcart_item(self): shopcart = self._create_shopcarts(1)[0] item = ShopCartItemFactory() resp = self.client.post( - f"{BASE_URL}/{shopcart.id}/items", + f"{BASE_URL_ITEM}/{shopcart.id}/items", json=item.serialize(), content_type="application/json", ) @@ -544,13 +545,13 @@ def test_delete_shopcart_item(self): # send delete request resp = self.client.delete( - f"{BASE_URL}/{shopcart.id}/items/{item_id}", + f"{BASE_URL_ITEM}/{shopcart.id}/items/{item_id}", content_type="application/json", ) self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) resp = self.client.get( - f"{BASE_URL}/{shopcart.id}/items/{item_id}", + f"{BASE_URL_ITEM}/{shopcart.id}/items/{item_id}", content_type="application/json", ) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) @@ -561,7 +562,7 @@ def test_delete_shopcart_item_when_no_shopcart(self): shopcart = self._create_shopcarts(1)[0] item = ShopCartItemFactory() resp = self.client.post( - f"{BASE_URL}/{shopcart.id}/items", + f"{BASE_URL_ITEM}/{shopcart.id}/items", json=item.serialize(), content_type="application/json", ) @@ -573,7 +574,7 @@ def test_delete_shopcart_item_when_no_shopcart(self): # try to delete an non-exist shopcart resp = self.client.delete( - f"{BASE_URL}/{shopcart.id + MAX_NUM}/items/{item_id}", + f"{BASE_URL_ITEM}/{shopcart.id + MAX_NUM}/items/{item_id}", content_type="application/json", ) self.assertEqual(resp.status_code, status.HTTP_404_NOT_FOUND) @@ -794,11 +795,6 @@ def test_method_not_allowed(self): resp = self.client.put(BASE_URL, json={"not": "today"}) self.assertEqual(resp.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - def test_bad_request(self): - """It should not Create when sending the wrong data""" - resp = self.client.post(BASE_URL, json={"name": "not enough data"}) - self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) - def test_shopcart_total_price_with_new_item(self): """ It should update the shopcart total price @@ -828,7 +824,7 @@ def test_shopcart_total_price_with_new_item(self): data = resp.get_json() self.assertEqual( data["total_price"], - str(item_1.price * item_1.quantity + item_2.price * item_2.quantity), + float(item_1.price * item_1.quantity + item_2.price * item_2.quantity), ) def test_shopcart_total_price_with_delete_item(self): @@ -876,7 +872,7 @@ def test_shopcart_total_price_with_delete_item(self): ) self.assertEqual(resp.status_code, status.HTTP_200_OK) data = resp.get_json() - self.assertEqual(data["total_price"], str(item_2.price * item_2.quantity)) + self.assertEqual(data["total_price"], float(item_2.price * item_2.quantity)) def test_shopcart_total_price_with_update_item(self): """ @@ -912,11 +908,11 @@ def test_shopcart_total_price_with_update_item(self): ) self.assertEqual(resp.status_code, status.HTTP_200_OK) data = resp.get_json() - self.assertEqual(data["total_price"], str(item.price * 3)) + self.assertEqual(data["total_price"], float(item.price * 3)) def test_read_health(self): """It should return 200 status and OK when testing health""" response = self.client.get("/health") self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.json, {"status": "OK"}) + self.assertEqual(response.json, {"status": "OK"}) \ No newline at end of file diff --git a/tests/test_shop_cart.py b/tests/test_shop_cart.py index ad43183..3d5ccc3 100644 --- a/tests/test_shop_cart.py +++ b/tests/test_shop_cart.py @@ -172,14 +172,14 @@ def test_serialize_shop_cart(self): self.assertEqual(data["id"], shop_cart.id) self.assertEqual(data["user_id"], shop_cart.user_id) self.assertEqual(data["name"], shop_cart.name) - self.assertEqual(data["total_price"], shop_cart.total_price) + self.assertEqual(data["total_price"], float(shop_cart.total_price)) self.assertEqual(len(data["items"]), 1) items = data["items"] self.assertEqual(items[0]["id"], shop_cart_item.id) self.assertEqual(items[0]["product_id"], shop_cart_item.product_id) self.assertEqual(items[0]["shop_cart_id"], shop_cart_item.shop_cart_id) self.assertEqual(items[0]["quantity"], shop_cart_item.quantity) - self.assertEqual(items[0]["price"], shop_cart_item.price) + self.assertEqual(items[0]["price"], float(shop_cart_item.price)) def test_deserialize_shop_cart(self): """It should deserialize a Shop Cart""" diff --git a/tests/test_shop_cart_item.py b/tests/test_shop_cart_item.py index 30bd84f..31f4670 100644 --- a/tests/test_shop_cart_item.py +++ b/tests/test_shop_cart_item.py @@ -125,7 +125,7 @@ def test_serialize_shop_cart_item(self): self.assertEqual(data["product_id"], shop_cart_item.product_id) self.assertEqual(data["shop_cart_id"], shop_cart_item.shop_cart_id) self.assertEqual(data["quantity"], shop_cart_item.quantity) - self.assertEqual(data["price"], shop_cart_item.price) + self.assertEqual(data["price"], float(shop_cart_item.price)) def test_deserialize_shop_cart_item(self): """It should deserialize a Shop Cart Item""" @@ -137,7 +137,7 @@ def test_deserialize_shop_cart_item(self): self.assertEqual(shop_cart_item.shop_cart_id, data.shop_cart_id) self.assertEqual(shop_cart_item.product_id, data.product_id) self.assertEqual(shop_cart_item.quantity, data.quantity) - self.assertEqual(shop_cart_item.price, data.price) + self.assertEqual(shop_cart_item.price, float(data.price)) # # # + + + + + + + + + + + + + SAD PATHS + + + + + + + + + + + + + + + def test_deserialize_missing_data(self):