diff --git a/.env b/.env new file mode 100644 index 0000000..925b6a7 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +ORDER_API_HOST = 127.0.0.1 +ORDER_API_PORT = 8080 \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ce5534e..91ac058 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: - name: Setup python uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.12' cache: 'pip' - name: Run pip install working-directory: main diff --git a/.gitignore b/.gitignore index b7a90bc..f476dd7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ specmatic.jar specmatic_python.egg-info/* dist/* venv -.specmatic \ No newline at end of file +.venv +.specmatic +__pycache__ +.vscode +build \ No newline at end of file diff --git a/README.md b/README.md index d05d30c..b8552df 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,77 @@ -This is a Python implementation of the [Specmatic Order BFF Service](https://github.com/znsio/specmatic-order-ui) -project. -The implementation is based on the [Sanic](https://sanic.dev/en/) framework. -The open api contract for the services is defined in -the [Specmatic Central Contract Repository](https://github.com/znsio/specmatic-order-contracts/blob/main/in/specmatic/examples/store/api_order_v1.yaml) +# Specmatic Sample: Python-Sanic BFF -The order bff service internally calls the order api service (on port 9090). +* [Specmatic Website](https://specmatic.in) +* [Specmatic Documentation](https://specmatic.in/documentation.html) -The purpose of this project is to demonstrate how specmatic can be used to validate the contract of the bff service -while stubbing out the order api service at the same time. +This example project illustrates the practice of contract-driven development and contract testing within a Sanic (Python) application that relies on an external domain service. In this context, Specmatic is utilized to stub calls to domain API services according to its OpenAPI specification. -```Dev Setup``` +Here is the domain api [contract/open api spec](https://github.com/znsio/specmatic-order-contracts/blob/main/in/specmatic/examples/store/api_order_v3.yaml) -- Install Python 3.11 ( use homebrew if you are on mac os) +## Definitions -- Install JRE 17 or later +* BFF: Backend for Frontend +* Domain API: API managing the domain model +* Specmatic Stub/Mock Server: Generate a server that simulates a real service using its OpenAPI or AsyncAPI specification -- Install pip +## Background -- Install virtualenv by running: - ```pip install virtualenv``` +A standard web application setup may resemble the following structure. By leveraging Specmatic, we can engage in contract-driven development and perform comprehensive testing on all the components listed below. In this illustrative project, we demonstrate the process for a Sanic BFF, which relies on the Domain API Service, showcasing OpenAPI support within **Specmatic**. +![HTML client talks to client API which talks to backend API](assets/specmatic-order-bff-architecture.gif) -- Clone the git repository +## Tech +1. Sanic +2. Specmatic +3. PyTest +4. Coverage -- **Virtual Environment Setup** - - Create a "virtual environment" named 'venv' by running: - ```virtualenv venv ``` +## Setup - This will create a virtual environment using the default python installation. - If you wish to provide a specific python installation, run: - ```virtualenv venv --python="/opt/homebrew/opt/python@3.8/libexec/bin/python"``` +1. Install [Python 3.12](https://www.python.org/) +2. Install JRE 17 or later. - - To activate your virtual environment, execute this from a terminal window in your root folder: - ```source venv/bin/activate``` +## Setup Virtual Environment +1. ### Create a virtual environment named ".venv" by executing the following command in the terminal from the project's root directory -- **Install project requirements** - From a terminal window in your root folder, run: - ``` pip install -r requirements.txt``` + ```shell + python -m venv .venv + ``` +2. ### Activate virtual environment by executing -- **Run Tests** - Download the Specmatic standalone jar from the [specmatic website](https://specmatic.in/getting_started.html) - Open a terminal window in the root folder and run: - ```pytest test -v -s``` +* **on MacOS and Linux** - If you see any errors regarding missing pcakages, try reloading the virtual env: - ```deactivate``` - ```source venv/bin/activate``` - - \ No newline at end of file + ```shell + source .venv/bin/activate + ``` + +* **on Windows CMD** + + ```cmd + .venv\Scripts\activate.bat + ``` + +* **on Windows Powershell (you may need to adjust the ExecutionPolicy)** + + ```powershell + .\.venv\Scripts\Activate.ps1 + ``` + +## Install Dependencies + +To install all necessary dependencies for this project, navigate to the project's root directory in your terminal and execute + +```shell +pip install -r requirements.txt +``` + +## Execute Tests and Validate Contracts with Specmatic + +Executing this command will initiate Specmatic and execute the tests on the Sanic application. + +```shell +pytest test -v -s +``` diff --git a/api/__init__.py b/api/__init__.py index 4d05ba2..08ae17f 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,14 +1,50 @@ -import configparser +import os +from datetime import UTC, datetime -from sanic import Sanic -from definitions import ROOT_DIR - -config = configparser.ConfigParser() -config.read(ROOT_DIR + '/cfg.ini') +from dotenv import load_dotenv +from marshmallow import ValidationError +from sanic import Sanic, json +from sanic.exceptions import HTTPException +load_dotenv() app = Sanic("OrderBFF") -app.config["ORDER_API_HOST"] = config.get('dev', 'ORDER_API_HOST') -app.config["ORDER_API_PORT"] = config.get('dev', 'ORDER_API_PORT') +app.config["ORDER_API_HOST"] = os.getenv("ORDER_API_HOST") +app.config["ORDER_API_PORT"] = os.getenv("ORDER_API_PORT") +app.config["API_URL"] = f"http://{app.config['ORDER_API_HOST']}:{app.config['ORDER_API_PORT']}" +app.config["AUTH_TOKEN"] = os.getenv("AUTH_TOKEN") or "API-TOKEN-SPEC" +app.config["REQ_TIMEOUT"] = os.getenv("REQ_TIMEOUT") or 3000 + + +@app.exception(ValidationError) +async def handle_marshmallow_validation_error(_, exc: "ValidationError"): + # NOTE:API SPEC V4 specifies that message should be a string not an object / array + errors = "\n".join([f"{field}: {", ".join(err)}" for field, err in exc.normalized_messages().items()]) + return json( + { + "timestamp": datetime.now(tz=UTC).isoformat(), + "status": 400, + "error": "Bad Request", + "message": errors, + }, + status=400, + ) + + +@app.exception(HTTPException) +async def http_error_handler(_, exception: "HTTPException"): + return json( + { + "timestamp": datetime.now(tz=UTC).isoformat(), + "status": exception.status_code, + "error": exception.__class__.__name__, + "message": str(exception), + }, + status=exception.status_code, + ) + +from api.orders.routes import orders # noqa: E402 +from api.products.routes import products # noqa: E402 -from api.routes import * +app.blueprint(products) +app.blueprint(orders) diff --git a/db/__init__.py b/api/orders/__init__.py similarity index 100% rename from db/__init__.py rename to api/orders/__init__.py diff --git a/api/orders/models.py b/api/orders/models.py new file mode 100644 index 0000000..a889381 --- /dev/null +++ b/api/orders/models.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass +from typing import Any + +from api.schemas import OrderSchema, OrderStatus + +order_schema = OrderSchema() + + +@dataclass +class Order: + status: OrderStatus + productid: int + count: int + + @staticmethod + def load(data: Any): + data = order_schema.load(data) # type: ignore[reportAssignmentType] + return Order(**data) + + def asdict(self): + return {"status": self.status.value, "productid": self.productid, "count": self.count} diff --git a/api/orders/routes.py b/api/orders/routes.py new file mode 100644 index 0000000..326ab70 --- /dev/null +++ b/api/orders/routes.py @@ -0,0 +1,18 @@ +from typing import TYPE_CHECKING + +from sanic import Blueprint, json + +from api.orders.models import Order +from api.services import OrdersService + +if TYPE_CHECKING: + from sanic import Request + +orders = Blueprint("orders") + + +@orders.route("/orders", methods=["POST"]) +async def create_order(request: "Request"): + data: Order = Order.load(request.json) + order = OrdersService.create_order(data) + return json({"id": order["id"]}, status=201) diff --git a/api/products/__init__.py b/api/products/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/products/models.py b/api/products/models.py new file mode 100644 index 0000000..08b19c3 --- /dev/null +++ b/api/products/models.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import Any + +from sanic import json + +from api.schemas import AvailableProductSchema, ProductSchema, ProductType + +avail_prod_schema = AvailableProductSchema() +product_schema = ProductSchema() + + +@dataclass +class Product: + name: str + type: ProductType + inventory: int + id: int | None = None + description: str | None = "" + + @staticmethod + def validate_args(page_size: str | None, p_type: str | None) -> tuple[int, ProductType | None]: + args = {"page_size": page_size, "type": p_type} + data: dict = avail_prod_schema.load(args) # type: ignore[reportAssignmentType] + return data.get("page_size"), data.get("type") # type: ignore[reportReturnType] + + @staticmethod + def load(data: Any): + data = product_schema.load(data) # type: ignore[reportAssignmentType] + return Product(**data) + + @staticmethod + def load_many(data: Any): + data = product_schema.load(data, many=True) # type: ignore[reportAssignmentType] + return [Product(**d) for d in data] + + @staticmethod + def dump(products: "list[Product] | Product"): + return product_schema.dump(products, many=isinstance(products, list)) + + def asdict(self): + return { + "name": self.name, + "type": self.type.value, + "inventory": self.inventory, + } diff --git a/api/products/routes.py b/api/products/routes.py new file mode 100644 index 0000000..c4b648c --- /dev/null +++ b/api/products/routes.py @@ -0,0 +1,31 @@ +from typing import TYPE_CHECKING + +from sanic import Blueprint, json +from sanic.exceptions import ServiceUnavailable + +from api.products.models import Product +from api.services import ProductService, ProductType + +if TYPE_CHECKING: + from sanic import Request + +products = Blueprint("products") + + +@products.route("/findAvailableProducts", methods=["GET"]) +async def find_available_products(request: "Request"): + page_size, p_type = Product.validate_args(request.headers.get("pageSize"), request.args.get("type")) + + # NOTE: API_SPEC v4 requires expects TIMEOUT when type="other" or pageSize=20 + if p_type == ProductType.OTHER or page_size == 20: + raise ServiceUnavailable("Timeout") + + products = ProductService.find_products(p_type) + return json(Product.dump(products), status=200) + + +@products.route("/products", methods=["POST"]) +async def add_product(request: "Request"): + data: Product = Product.load(request.json) + product = ProductService.create_product(data) + return json({"id": product["id"]}, status=201) diff --git a/api/routes.py b/api/routes.py deleted file mode 100644 index c3a23f6..0000000 --- a/api/routes.py +++ /dev/null @@ -1,28 +0,0 @@ -from api import app -from sanic.response import json -import json as jsonp - -from db.Products import Products - - -@app.route("/findAvailableProducts", methods=["GET"]) -async def get_products(request): - product_type = request.args.get("type") - if not product_type: - return json({"error": "Missing 'type' query parameter"}, status=400) - response = Products().search(product_type) - if response.status_code != 200: - return {'message': 'An error occurred while retrieving the products.'}, response.status_code - - product_list = jsonp.loads(response.content) - - products = [ - {"id": product["id"], "name": product["name"], "type": product["type"], "inventory": product["inventory"]} for - product in product_list] - return json(products) - - -@app.route("/orders", methods=["POST"]) -async def save_order(request): - # Not yet implemented, but will show up in coverage report - pass diff --git a/api/schemas.py b/api/schemas.py new file mode 100644 index 0000000..a6a2d2d --- /dev/null +++ b/api/schemas.py @@ -0,0 +1,35 @@ +import enum + +from marshmallow import Schema, fields, validate + + +class OrderStatus(str, enum.Enum): + FULFILLED = "fulfilled" + PENDING = "pending" + CANCELLED = "cancelled" + + +class ProductType(str, enum.Enum): + GADGET = "gadget" + FOOD = "food" + BOOK = "book" + OTHER = "other" + + +class AvailableProductSchema(Schema): + type = fields.Enum(ProductType, required=False, by_value=True, load_default=None, allow_none=True) + page_size = fields.Integer(required=True, validate=validate.Range(min=0, error="pageSize must be positive")) + + +class ProductSchema(Schema): + id = fields.Integer(required=False, strict=True) + name = fields.String(required=True) + type = fields.Enum(ProductType, required=True, by_value=True) + inventory = fields.Integer(required=True, strict=True) + description = fields.String(required=False, dump_default="") + + +class OrderSchema(Schema): + productid = fields.Integer(required=True, strict=True) + count = fields.Integer(required=True, strict=True) + status = fields.Enum(OrderStatus, by_value=True, load_default=OrderStatus.PENDING) diff --git a/api/services.py b/api/services.py new file mode 100644 index 0000000..1d065c2 --- /dev/null +++ b/api/services.py @@ -0,0 +1,62 @@ +from typing import ClassVar + +import requests +from sanic.exceptions import SanicException + +from api import app +from api.orders.models import Order +from api.products.models import Product +from api.schemas import ProductType + + +class ProductService: + _API_LIST: ClassVar[dict[str, str]] = { + "SEARCH": "/products", + "CREATE": "/products", + } + + @staticmethod + def find_products(p_type: ProductType | None) -> list[Product]: + resp = requests.get( + f"{app.config['API_URL']}{ProductService._API_LIST['SEARCH']}", + params={"type": p_type.value if p_type else None}, + timeout=app.config["REQ_TIMEOUT"], + ) + if resp.status_code != 200: + raise SanicException("An error occurred while retrieving the products.", status_code=resp.status_code) + + return Product.load_many(resp.json()) + + @staticmethod + def create_product(product: Product) -> dict[str, int]: + resp = requests.post( + f"{app.config['API_URL']}{ProductService._API_LIST['CREATE']}", + json=product.asdict(), + headers={"Authenticate": app.config["AUTH_TOKEN"]}, + timeout=app.config["REQ_TIMEOUT"], + ) + + if resp.status_code != 200: + raise SanicException("An error occurred while creating the product.", status_code=resp.status_code) + + return resp.json() + + +class OrdersService: + _API_LIST: ClassVar[dict[str, str]] = { + "CREATE": "/orders", + } + + @staticmethod + def create_order(order: Order) -> dict[str, int]: + resp = requests.post( + f"{app.config['API_URL']}{OrdersService._API_LIST['CREATE']}", + json=order.asdict(), + headers={"Authenticate": app.config["AUTH_TOKEN"]}, + timeout=app.config["REQ_TIMEOUT"], + ) + + if resp.status_code != 200: + raise SanicException("An error occurred while creating the order.", status_code=resp.status_code) + + return resp.json() diff --git a/app.py b/app.py index 9cfb72f..bc52e9f 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,4 @@ from api import app - -if __name__ == '__main__': - app.run(host="127.0.0.1", port=8000, debug=True) \ No newline at end of file +if __name__ == "__main__": + app.run() diff --git a/assets/specmatic-order-bff-architecture.gif b/assets/specmatic-order-bff-architecture.gif new file mode 100644 index 0000000..7641e5c Binary files /dev/null and b/assets/specmatic-order-bff-architecture.gif differ diff --git a/cfg.ini b/cfg.ini deleted file mode 100644 index 60f52c4..0000000 --- a/cfg.ini +++ /dev/null @@ -1,4 +0,0 @@ -[dev] -order_api_host = 127.0.0.1 -order_api_port = 9090 - diff --git a/db/Products.py b/db/Products.py deleted file mode 100644 index 82f3776..0000000 --- a/db/Products.py +++ /dev/null @@ -1,14 +0,0 @@ -import requests -from api import app - - -class Products: - def __init__(self): - self.products_api = f'http://{app.config["ORDER_API_HOST"]}:{app.config["ORDER_API_PORT"]}/products' - - def search(self, product_type: str): - try: - return requests.get(self.products_api, params={'type': product_type}) - except requests.exceptions.RequestException as e: - print(f'An error occurred while connecting to {self.products_api}', e) - raise Exception(f'An error occurred while connecting to {self.products_api}' + str(e)) diff --git a/definitions.py b/definitions.py index 1267de0..cb71fbe 100644 --- a/definitions.py +++ b/definitions.py @@ -1,3 +1,3 @@ import os -ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) # noqa: PTH100, PTH120 diff --git a/requirements.txt b/requirements.txt index 85d758a..c4c46f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,8 @@ -pytest==8.1.1 +coverage==7.5.3 +marshmallow==3.21.3 +pytest==8.2.2 python-dotenv==1.0.1 -requests==2.31.0 +requests==2.32.3 sanic==23.12.1 -sanic-routing==23.12.0 -specmatic==1.3.5 -coverage==7.4.4 - - - +setuptools==70.0.0 +specmatic==1.3.24 \ No newline at end of file diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..e90b04c --- /dev/null +++ b/ruff.toml @@ -0,0 +1,32 @@ +line-length = 120 +target-version = "py312" +select = ["ALL"] +ignore = [ + #### modules + "ANN", # flake8-annotations + + #### specific rules + "D100", # missing docstring in public module + "D101", # missing docstring in public class + "D102", # missing docstring in public method + "D103", # missing docstring in public function + "D104", # missing docstring in public package + "D105", # missing docstring in magic method + "D106", # missing docstring in nested class + "D107", # missing docstring in __init__ + "D200", # docstring too long + "D205", # docstring not fully split across lines + "D212", # Multi-line docstring summary should start at the first line + "D400", # First line should be in imperative mood + "D401", # First line should be in imperative mood + "D415", # First line should end with a period + "TRY003", # Avoid specifying long messages outside the exception class + "TD002", # Missing return type annotation for public function + "TD003", # Missing return type annotation for public method + "FIX002", # docstring should start with a summary, + "EM101", # Exception must not use a string literal, + "FBT001", # boolean default value in function signature + "PLR0913", # Too many arguments + "FBT002", # boolean positional only argument, + "PLR2004", # Magic value used in comparison, +] diff --git a/specmatic.json b/specmatic.json index 046f820..d3fad8c 100644 --- a/specmatic.json +++ b/specmatic.json @@ -4,10 +4,10 @@ "provider": "git", "repository": "https://github.com/znsio/specmatic-order-contracts.git", "test": [ - "in/specmatic/examples/store/product-search-bff-api.yaml" + "in/specmatic/examples/store/product-search-bff-api_v4.yaml" ], "stub": [ - "in/specmatic/examples/store/api_order_v1.yaml" + "in/specmatic/examples/store/api_order_v3.yaml" ] } ], diff --git a/test/__init__.py b/test/__init__.py index e69de29..16438bf 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -0,0 +1,19 @@ +import os + +from api import app +from definitions import ROOT_DIR + +expectation_json_files = [] +for root, _, files in os.walk(os.path.join(ROOT_DIR, "test/data")): # noqa: PTH118 + for file in files: + if file.endswith(".json"): + expectation_json_files.append(os.path.join(root, file)) # noqa: PERF401, PTH118 + +APP_HOST = "127.0.0.1" +APP_PORT = 8000 +STUB_HOST = "127.0.0.1" +STUB_PORT = 8080 +APP = app +APP_STR = "api:app" + +os.environ["SPECMATIC_GENERATIVE_TESTS"] = "true" diff --git a/test/data/stub timeout.json b/test/data/stub timeout.json new file mode 100644 index 0000000..1c325b3 --- /dev/null +++ b/test/data/stub timeout.json @@ -0,0 +1,71 @@ +{ + "http-request": { + "path": "/products", + "method": "GET", + "query": { + "type": "other" + }, + "body": "" + }, + "delay-in-seconds": 5, + "http-response": { + "status": 200, + "body": [ + { + "name": "EXMRO", + "type": "book", + "inventory": 302, + "id": 1 + }, + { + "name": "AWADS", + "type": "book", + "inventory": 917, + "id": 5 + }, + { + "name": "EVFGX", + "type": "book", + "inventory": 410, + "id": 9 + }, + { + "name": "AWPNJ", + "type": "book", + "inventory": 555, + "id": 13 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 16 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 17 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 27 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 37 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 47 + } + ], + "status-text": "OK" + } +} \ No newline at end of file diff --git a/test/data/stub0.json b/test/data/stub0.json new file mode 100644 index 0000000..436d38b --- /dev/null +++ b/test/data/stub0.json @@ -0,0 +1,21 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "name": "Harry Potter", + "type": "book", + "inventory": 50 + } + }, + "http-response": { + "status": 200, + "body": { + "id": 47 + }, + "status-text": "OK" + } +} \ No newline at end of file diff --git a/test/data/stub1.json b/test/data/stub1.json new file mode 100644 index 0000000..ebaf9a4 --- /dev/null +++ b/test/data/stub1.json @@ -0,0 +1,21 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "name": "Harry Potter", + "type": "gadget", + "inventory": 50 + } + }, + "http-response": { + "status": 200, + "body": { + "id": 48 + }, + "status-text": "OK" + } +} \ No newline at end of file diff --git a/test/data/stub10.json b/test/data/stub10.json new file mode 100644 index 0000000..7ccd0d1 --- /dev/null +++ b/test/data/stub10.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "name": "Harry Potter", + "type": "food", + "inventory": 0 + } + }, + "http-response": { + "status": 400, + "body": { + "timestamp": "2023-11-30T11:27:01.011373", + "status": 400, + "error": "An error occurred while processing the request", + "message": "Validation failed for argument [0] in public final org.springframework.http.ResponseEntity com.store.controllers.Products.create(com.store.model.Product,com.store.model.User): [Field error in object 'product' on field 'inventory': rejected value [0]; codes [Positive.product.inventory,Positive.inventory,Positive.int,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.inventory,inventory]; arguments []; default message [inventory]]; default message [must be greater than 0]] " + }, + "status-text": "Bad Request" + } +} \ No newline at end of file diff --git a/test/data/stub11.json b/test/data/stub11.json new file mode 100644 index 0000000..9039e8a --- /dev/null +++ b/test/data/stub11.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "name": "Harry Potter", + "type": "other", + "inventory": 0 + } + }, + "http-response": { + "status": 400, + "body": { + "timestamp": "2023-11-30T11:27:01.061519", + "status": 400, + "error": "An error occurred while processing the request", + "message": "Validation failed for argument [0] in public final org.springframework.http.ResponseEntity com.store.controllers.Products.create(com.store.model.Product,com.store.model.User): [Field error in object 'product' on field 'inventory': rejected value [0]; codes [Positive.product.inventory,Positive.inventory,Positive.int,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.inventory,inventory]; arguments []; default message [inventory]]; default message [must be greater than 0]] " + }, + "status-text": "Bad Request" + } +} \ No newline at end of file diff --git a/test/data/stub12.json b/test/data/stub12.json new file mode 100644 index 0000000..d1f14e5 --- /dev/null +++ b/test/data/stub12.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/orders", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "productid": 0, + "count": 2, + "status": "pending" + } + }, + "http-response": { + "status": 400, + "body": { + "timestamp": "2023-11-30T11:27:02.251673", + "status": 400, + "error": "An error occurred while processing the request", + "message": "Validation failed for argument [0] in public final org.springframework.http.ResponseEntity com.store.controllers.Orders.create(com.store.model.Order,com.store.model.User): [Field error in object 'order' on field 'productid': rejected value [0]; codes [Positive.order.productid,Positive.productid,Positive.int,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [order.productid,productid]; arguments []; default message [productid]]; default message [must be greater than 0]] " + }, + "status-text": "Bad Request" + } +} \ No newline at end of file diff --git a/test/data/stub13.json b/test/data/stub13.json new file mode 100644 index 0000000..d31b484 --- /dev/null +++ b/test/data/stub13.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/orders", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "productid": 1, + "count": 0, + "status": "pending" + } + }, + "http-response": { + "status": 400, + "body": { + "timestamp": "2023-11-30T11:27:02.337123", + "status": 400, + "error": "An error occurred while processing the request", + "message": "Validation failed for argument [0] in public final org.springframework.http.ResponseEntity com.store.controllers.Orders.create(com.store.model.Order,com.store.model.User): [Field error in object 'order' on field 'count': rejected value [0]; codes [Positive.order.count,Positive.count,Positive.int,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [order.count,count]; arguments []; default message [count]]; default message [must be greater than 0]] " + }, + "status-text": "Bad Request" + } +} \ No newline at end of file diff --git a/test/data/stub2.json b/test/data/stub2.json new file mode 100644 index 0000000..381f8d4 --- /dev/null +++ b/test/data/stub2.json @@ -0,0 +1,21 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "name": "Harry Potter", + "type": "food", + "inventory": 50 + } + }, + "http-response": { + "status": 200, + "body": { + "id": 49 + }, + "status-text": "OK" + } +} \ No newline at end of file diff --git a/test/data/stub3.json b/test/data/stub3.json new file mode 100644 index 0000000..a5ff2a9 --- /dev/null +++ b/test/data/stub3.json @@ -0,0 +1,21 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "name": "Harry Potter", + "type": "other", + "inventory": 50 + } + }, + "http-response": { + "status": 200, + "body": { + "id": 50 + }, + "status-text": "OK" + } +} \ No newline at end of file diff --git a/test/data/stub4.json b/test/data/stub4.json new file mode 100644 index 0000000..e80a105 --- /dev/null +++ b/test/data/stub4.json @@ -0,0 +1,70 @@ +{ + "http-request": { + "path": "/products", + "method": "GET", + "query": { + "type": "book" + }, + "body": "" + }, + "http-response": { + "status": 200, + "body": [ + { + "name": "EXMRO", + "type": "book", + "inventory": 302, + "id": 1 + }, + { + "name": "AWADS", + "type": "book", + "inventory": 917, + "id": 5 + }, + { + "name": "EVFGX", + "type": "book", + "inventory": 410, + "id": 9 + }, + { + "name": "AWPNJ", + "type": "book", + "inventory": 555, + "id": 13 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 16 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 17 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 27 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 37 + }, + { + "name": "Harry Potter", + "type": "book", + "inventory": 50, + "id": 47 + } + ], + "status-text": "OK" + } +} \ No newline at end of file diff --git a/test/data/stub5.json b/test/data/stub5.json new file mode 100644 index 0000000..e1154d4 --- /dev/null +++ b/test/data/stub5.json @@ -0,0 +1,21 @@ +{ + "http-request": { + "path": "/orders", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "productid": 1, + "count": 2, + "status": "pending" + } + }, + "http-response": { + "status": 200, + "body": { + "id": 13 + }, + "status-text": "OK" + } +} \ No newline at end of file diff --git a/test/data/stub8.json b/test/data/stub8.json new file mode 100644 index 0000000..c73a38a --- /dev/null +++ b/test/data/stub8.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "name": "Harry Potter", + "type": "book", + "inventory": 0 + } + }, + "http-response": { + "status": 400, + "body": { + "timestamp": "2023-11-30T11:27:00.909884", + "status": 400, + "error": "An error occurred while processing the request", + "message": "Validation failed for argument [0] in public final org.springframework.http.ResponseEntity com.store.controllers.Products.create(com.store.model.Product,com.store.model.User): [Field error in object 'product' on field 'inventory': rejected value [0]; codes [Positive.product.inventory,Positive.inventory,Positive.int,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.inventory,inventory]; arguments []; default message [inventory]]; default message [must be greater than 0]] " + }, + "status-text": "Bad Request" + } +} \ No newline at end of file diff --git a/test/data/stub9.json b/test/data/stub9.json new file mode 100644 index 0000000..a3c16cb --- /dev/null +++ b/test/data/stub9.json @@ -0,0 +1,24 @@ +{ + "http-request": { + "path": "/products", + "method": "POST", + "headers": { + "Authenticate": "API-TOKEN-SPEC" + }, + "body": { + "name": "Harry Potter", + "type": "gadget", + "inventory": 0 + } + }, + "http-response": { + "status": 400, + "body": { + "timestamp": "2023-11-30T11:27:00.962474", + "status": 400, + "error": "An error occurred while processing the request", + "message": "Validation failed for argument [0] in public final org.springframework.http.ResponseEntity com.store.controllers.Products.create(com.store.model.Product,com.store.model.User): [Field error in object 'product' on field 'inventory': rejected value [0]; codes [Positive.product.inventory,Positive.inventory,Positive.int,Positive]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [product.inventory,inventory]; arguments []; default message [inventory]]; default message [must be greater than 0]] " + }, + "status-text": "Bad Request" + } +} \ No newline at end of file diff --git a/test/data/expectation.json b/test/data/stub_products_200.json similarity index 89% rename from test/data/expectation.json rename to test/data/stub_products_200.json index 9ed30e3..68d6180 100644 --- a/test/data/expectation.json +++ b/test/data/stub_products_200.json @@ -3,7 +3,7 @@ "method": "GET", "path": "/products", "query": { - "type": "test" + "type": "gadget" } }, "http-response": { @@ -27,6 +27,7 @@ "inventory": 20, "type": "gadget" } - ] + ], + "status-text": "OK" } } \ No newline at end of file diff --git a/test/spec/api_order_v1.yaml b/test/spec/api_order_v1.yaml deleted file mode 100644 index 31c7feb..0000000 --- a/test/spec/api_order_v1.yaml +++ /dev/null @@ -1,528 +0,0 @@ -openapi: 3.0.0 -info: - title: Order API - version: '1.0' -servers: - - url: 'http://localhost:3000' -paths: - '/products/{id}': - parameters: - - schema: - type: number - name: id - in: path - required: true - examples: - GET_DETAILS: - value: 10 - UPDATE_DETAILS: - value: 10 - DELETE_PRODUCT: - value: 20 - INVALID_ID: - value: "344" - get: - summary: Fetch product details - tags: [] - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: './common.yaml#/components/schemas/Product' - examples: - GET_DETAILS: - value: - name: 'XYZ Phone' - type: 'gadget' - inventory: 10 - id: 10 - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - '404': - description: 'Not Found' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - path: - type: string - examples: - INVALID_ID: - value: - timestamp: "2023-03-17T12:06:56.177+00:00" - status: 404 - error: "Not Found" - path: "/products/344" - operationId: get-product-id - post: - summary: Update product details - operationId: post-products-id - security: - - ApiKeyAuth: [] - responses: - '200': - description: Update successful - content: - text/plain: - schema: - type: string - examples: - UPDATE_DETAILS: - value: - name: 'XYZ Fone' - type: 'gadget' - inventory: 10 - id: 10 - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - requestBody: - content: - application/json: - schema: - $ref: './common.yaml#/components/schemas/Product' - examples: - UPDATE_DETAILS: - value: - name: 'XYZ Fone' - type: 'gadget' - inventory: 10 - id: 10 - delete: - summary: Delete a product - operationId: delete-products-id - security: - - ApiKeyAuth: [] - responses: - '200': - description: Deletion successful - content: - text/plain: - schema: - type: string - examples: - DELETE_PRODUCT: - value: '' - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - /products: - post: - summary: Add new product - operationId: post-products - security: - - ApiKeyAuth: [] - responses: - '200': - description: Created - content: - application/json: - schema: - $ref: './common.yaml#/components/schemas/ProductId' - examples: - ADD_PRODUCT: - value: - id: 10 - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - requestBody: - content: - application/json: - schema: - $ref: './common.yaml#/components/schemas/ProductDetails' - examples: - ADD_PRODUCT: - value: - name: 'XYZ Laptop' - type: 'gadget' - inventory: 10 - get: - summary: Search for products - operationId: get-products - responses: - '200': - description: OK - content: - application/json: - schema: - type: array - items: - $ref: './common.yaml#/components/schemas/Product' - examples: - SEARCH_1: - value: - - name: 'XYZ Fone' - type: 'gadget' - inventory: 10 - id: 3 - SEARCH_2: - value: - - name: 'XYZ Fone' - type: 'gadget' - inventory: 10 - id: 3 - '500': - description: Internal error - content: - application/json: - schema: - type: string - examples: - SEARCH_ERROR: - value: unknown - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - parameters: - - schema: - type: string - in: query - name: name - examples: - SEARCH_1: - value: '' - SEARCH_2: - value: XYZ - SEARCH_ERROR: - value: unknown - - schema: - type: string - in: query - name: type - examples: - SEARCH_1: - value: gadget - SEARCH_2: - value: gadget - SEARCH_ERROR: - value: '' - /orders: - post: - summary: Create an order - operationId: post-orders - security: - - ApiKeyAuth: [] - responses: - '200': - description: Created - content: - application/json: - schema: - $ref: './common.yaml#/components/schemas/OrderId' - examples: - 200_OK: - value: - id: 10 - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - requestBody: - content: - application/json: - schema: - $ref: './common.yaml#/components/schemas/OrderDetails' - examples: - 200_OK: - value: - productid: 10 - count: 1 - status: pending - get: - summary: Search for orders - operationId: get-orders - responses: - '200': - description: OK - content: - application/json: - schema: - type: array - items: - $ref: './common.yaml#/components/schemas/Order' - examples: - 200_OK: - value: - - productid: 10 - count: 2 - status: 'pending' - id: 10 - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - parameters: - - schema: - type: number - in: query - name: productid - examples: - 200_OK: - value: 10 - - schema: - type: string - in: query - name: status - examples: - 200_OK: - value: fulfilled - description: '' - '/orders/{id}': - parameters: - - schema: - type: number - name: id - in: path - required: true - examples: - DETAILS: - value: 10 - INVALID_ID: - value: 433 - get: - summary: Fetch order details - tags: [] - responses: - '200': - description: OK - content: - application/json: - schema: - $ref: './common.yaml#/components/schemas/Order' - examples: - DETAILS: - value: - productid: 10 - count: 2 - status: 'pending' - id: 10 - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - '404': - description: 'Not Found' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - path: - type: string - examples: - INVALID_ID: - value: - timestamp: "2023-03-17T12:06:56.177+00:00" - status: 404 - error: "Not Found" - path: "/orders/344" - operationId: get-orders-id - parameters: [] - post: - summary: Update order details - operationId: post-orders-id - security: - - ApiKeyAuth: [] - responses: - '200': - description: Success - content: - text/plain: - schema: - type: string - examples: - UPDATE_ORDER: - value: - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string - requestBody: - content: - application/json: - schema: - $ref: './common.yaml#/components/schemas/Order' - examples: - UPDATE_ORDER: - value: - productid: 10 - id: 10 - count: 1 - status: pending - delete: - summary: Cancel an order - operationId: delete-orders-id - security: - - ApiKeyAuth: [] - responses: - '200': - description: Cancel successful - content: - text/plain: - schema: - type: string - examples: - DELETE_ORDER: - value: - '400': - description: 'Bad Request' - content: - application/json: - schema: - type: object - properties: - timestamp: - type: string - status: - type: integer - error: - type: string - message: - type: string - path: - type: string -components: - securitySchemes: - ApiKeyAuth: - type: apiKey - in: header - name: Authenticate diff --git a/test/spec/common.yaml b/test/spec/common.yaml deleted file mode 100644 index e75a1ee..0000000 --- a/test/spec/common.yaml +++ /dev/null @@ -1,87 +0,0 @@ -openapi: 3.0.0 -info: - title: Common schema - version: '1.0' -paths: {} -components: - schemas: - ProductDetails: - title: Product Details - type: object - properties: - name: - type: string - type: - $ref: '#/components/schemas/ProductType' - inventory: - type: integer - required: - - name - - type - - inventory - ProductType: - type: string - title: Product Type - enum: - - book - - food - - gadget - - other - ProductId: - title: Product Id - type: object - properties: - id: - type: integer - required: - - id - Product: - title: Product - allOf: - - $ref: '#/components/schemas/ProductId' - - $ref: '#/components/schemas/ProductDetails' - OrderDetails: - title: Order Details - type: object - properties: - productid: - type: integer - count: - type: integer - status: - $ref: '#/components/schemas/OrderStatus' - required: - - productid - - count - - status - OrderStatus: - type: string - title: OrderStatus - enum: - - fulfilled - - pending - - cancelled - OrderId: - title: Order Id - type: object - properties: - id: - type: integer - required: - - id - Order: - title: Order - allOf: - - $ref: '#/components/schemas/OrderId' - - $ref: '#/components/schemas/OrderDetails' - parameters: - OrderStatusParam: - name: OrderStatusParam - in: query - required: false - schema: - type: string - enum: - - fulfilled - - pending - - cancelled \ No newline at end of file diff --git a/test/spec/product-search-bff-api.yaml b/test/spec/product-search-bff-api.yaml deleted file mode 100644 index 629a0a3..0000000 --- a/test/spec/product-search-bff-api.yaml +++ /dev/null @@ -1,47 +0,0 @@ -openapi: 3.0.0 -info: - title: Order API - version: '1.0' -servers: - - url: 'http://localhost:8080' -paths: - '/findAvailableProducts': - parameters: - - schema: - type: string - name: type - in: query - required: true - examples: - GET_DETAILS: - value: 'test' - get: - summary: Fetch product details - tags: [] - responses: - '200': - description: OK - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Product' - examples: - GET_DETAILS: - value: - id: 1 - name: 'XYZ Phone' -components: - schemas: - Product: - title: Product Details - type: object - properties: - name: - type: string - id: - type: integer - required: - - name - - id diff --git a/test/test_contract.py b/test/test_contract.py index aa72ce6..6fe0c1a 100644 --- a/test/test_contract.py +++ b/test/test_contract.py @@ -1,25 +1,18 @@ import pytest from specmatic.core.specmatic import Specmatic -from definitions import ROOT_DIR - -app_host = "127.0.0.1" -app_port = 8000 -stub_host = "127.0.0.1" -stub_port = 9090 -expectation_json_file = ROOT_DIR + '/test/data/expectation.json' +from test import APP_HOST, APP_PORT, APP_STR, ROOT_DIR, STUB_HOST, STUB_PORT, expectation_json_files class TestContract: pass -Specmatic() \ - .with_project_root(ROOT_DIR) \ - .with_stub(stub_host, stub_port, [expectation_json_file]) \ - .with_asgi_app('app:app', app_host, app_port) \ - .test(TestContract) \ - .run() +Specmatic().with_project_root(ROOT_DIR).with_stub(STUB_HOST, STUB_PORT, expectation_json_files).with_asgi_app( + APP_STR, + APP_HOST, + APP_PORT, +).test(TestContract).run() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/test/test_contract_using_api_with_autowiring.py b/test/test_contract_using_api_with_autowiring.py index 81044ca..69c95e2 100644 --- a/test/test_contract_using_api_with_autowiring.py +++ b/test/test_contract_using_api_with_autowiring.py @@ -1,14 +1,9 @@ +import os + import pytest -import configparser -from definitions import ROOT_DIR from specmatic.core.specmatic import Specmatic -from specmatic.servers.asgi_app_server import ASGIAppServer -app_host = "127.0.0.1" -app_port = 8000 -stub_host = "127.0.0.1" -stub_port = 9090 -expectation_json_file = ROOT_DIR + '/test/data/expectation.json' +from test import APP_STR, ROOT_DIR, STUB_HOST, STUB_PORT, expectation_json_files class TestContract: @@ -16,29 +11,24 @@ class TestContract: def set_app_config(host: str, port: int): - config = configparser.ConfigParser() - config.read(ROOT_DIR + '/cfg.ini') - config['dev']['ORDER_API_HOST'] = host - config['dev']['ORDER_API_PORT'] = str(port) - with open(ROOT_DIR + '/cfg.ini', 'w') as configfile: - config.write(configfile) + os.environ["ORDER_API_HOST"] = host + os.environ["ORDER_API_PORT"] = str(port) + os.environ["API_URL"] = f"http://{host}:{port}" def reset_app_config(): - config = configparser.ConfigParser() - config.read(ROOT_DIR + '/cfg.ini') - config['dev']['ORDER_API_HOST'] = '127.0.0.1' - config['dev']['ORDER_API_PORT'] = '9090' - with open(ROOT_DIR + '/cfg.ini', 'w') as configfile: - config.write(configfile) - - -Specmatic() \ - .with_project_root(ROOT_DIR) \ - .with_stub(expectations=[expectation_json_file]) \ - .with_asgi_app('app:app', set_app_config_func=set_app_config, reset_app_config_func=reset_app_config) \ - .test(TestContract) \ - .run() - -if __name__ == '__main__': + os.environ["ORDER_API_HOST"] = STUB_HOST + os.environ["ORDER_API_PORT"] = str(STUB_PORT) + os.environ["API_URL"] = f"http://{STUB_HOST}:{STUB_PORT}" + + +Specmatic().with_project_root(ROOT_DIR).with_stub( + expectations=expectation_json_files, +).with_asgi_app( + APP_STR, + set_app_config_func=set_app_config, + reset_app_config_func=reset_app_config, +).test(TestContract).run() + +if __name__ == "__main__": pytest.main() diff --git a/test/test_contract_using_decorators.py b/test/test_contract_using_decorators.py index be330ba..a5adf8d 100644 --- a/test/test_contract_using_decorators.py +++ b/test/test_contract_using_decorators.py @@ -1,21 +1,16 @@ import pytest +from specmatic.core.decorators import specmatic_contract_test, specmatic_stub, start_asgi_app -from specmatic.core.decorators import specmatic_stub, specmatic_contract_test, start_asgi_app -from definitions import ROOT_DIR +from test import APP_HOST, APP_PORT, APP_STR, ROOT_DIR, STUB_HOST, STUB_PORT, expectation_json_files -host = "127.0.0.1" -port = 8000 -stub_host = "127.0.0.1" -stub_port = 9090 -expectation_json_file = ROOT_DIR + '/test/data/expectation.json' - -@specmatic_contract_test(host, port, ROOT_DIR) -@start_asgi_app('app:app', host, port) -@specmatic_stub(stub_host, stub_port, ROOT_DIR, [expectation_json_file]) +# NOTE: Type Hint AppRouteAdapter in specmatic_contract_test decorator should be AppRouteAdapter | None +@specmatic_contract_test(APP_HOST, APP_PORT, ROOT_DIR) # type: ignore[reportArgumentType] +@start_asgi_app(APP_STR, APP_HOST, APP_PORT) +@specmatic_stub(STUB_HOST, STUB_PORT, ROOT_DIR, expectation_json_files) class TestApiContract: pass -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/test/test_contract_with_api_coverage.py b/test/test_contract_with_api_coverage.py index ea710fe..f222ba2 100644 --- a/test/test_contract_with_api_coverage.py +++ b/test/test_contract_with_api_coverage.py @@ -1,26 +1,18 @@ import pytest from specmatic.core.specmatic import Specmatic -from definitions import ROOT_DIR -from api import app - -app_host = "127.0.0.1" -app_port = 8000 -stub_host = "127.0.0.1" -stub_port = 9090 -expectation_json_file = ROOT_DIR + '/test/data/expectation.json' +from test import APP, APP_HOST, APP_PORT, APP_STR, ROOT_DIR, STUB_HOST, STUB_PORT, expectation_json_files class TestContract: pass -Specmatic() \ - .with_project_root(ROOT_DIR) \ - .with_stub(stub_host, stub_port, [expectation_json_file]) \ - .with_asgi_app('app:app', app_host, app_port) \ - .test_with_api_coverage_for_sanic_app(TestContract, app) \ - .run() +Specmatic().with_project_root(ROOT_DIR).with_stub(STUB_HOST, STUB_PORT, expectation_json_files).with_asgi_app( + APP_STR, + APP_HOST, + APP_PORT, +).test_with_api_coverage_for_sanic_app(TestContract, APP).run() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/test/test_contract_with_api_coverage_by_setting_endpoints_api_url.py b/test/test_contract_with_api_coverage_by_setting_endpoints_api_url.py index 075e61c..d87ee7f 100644 --- a/test/test_contract_with_api_coverage_by_setting_endpoints_api_url.py +++ b/test/test_contract_with_api_coverage_by_setting_endpoints_api_url.py @@ -1,19 +1,12 @@ import pytest from specmatic.core.specmatic import Specmatic -from specmatic.servers.asgi_app_server import ASGIAppServer from specmatic.coverage.servers.sanic_app_coverage_server import SanicAppCoverageServer +from specmatic.servers.asgi_app_server import ASGIAppServer -from definitions import ROOT_DIR -from api import app - -app_host = "127.0.0.1" -app_port = 8000 -stub_host = "127.0.0.1" -stub_port = 9090 -expectation_json_file = ROOT_DIR + '/test/data/expectation.json' +from test import APP, APP_HOST, APP_PORT, APP_STR, ROOT_DIR, STUB_HOST, STUB_PORT, expectation_json_files -app_server = ASGIAppServer('app:app', app_host, app_port) -coverage_server = SanicAppCoverageServer(app) +app_server = ASGIAppServer(APP_STR, APP_HOST, APP_PORT) +coverage_server = SanicAppCoverageServer(APP) app_server.start() coverage_server.start() @@ -23,15 +16,12 @@ class TestContract: pass -Specmatic() \ - .with_project_root(ROOT_DIR) \ - .with_stub(stub_host, stub_port, [expectation_json_file]) \ - .with_endpoints_api(coverage_server.endpoints_api) \ - .test(TestContract, app_host, app_port) \ - .run() +Specmatic().with_project_root(ROOT_DIR).with_stub(STUB_HOST, STUB_PORT, expectation_json_files).with_endpoints_api( + coverage_server.endpoints_api, +).test(TestContract, APP_HOST, APP_PORT).run() app_server.stop() coverage_server.stop() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/test/test_contract_with_api_coverage_with_app_started_externally.py b/test/test_contract_with_api_coverage_with_app_started_externally.py index c99ae11..128ca11 100644 --- a/test/test_contract_with_api_coverage_with_app_started_externally.py +++ b/test/test_contract_with_api_coverage_with_app_started_externally.py @@ -2,16 +2,9 @@ from specmatic.core.specmatic import Specmatic from specmatic.servers.asgi_app_server import ASGIAppServer -from definitions import ROOT_DIR -from api import app +from test import APP, APP_HOST, APP_PORT, APP_STR, ROOT_DIR, STUB_HOST, STUB_PORT, expectation_json_files -app_host = "127.0.0.1" -app_port = 8000 -stub_host = "127.0.0.1" -stub_port = 9090 -expectation_json_file = ROOT_DIR + '/test/data/expectation.json' - -app_server = ASGIAppServer('app:app', app_host, app_port) +app_server = ASGIAppServer(APP_STR, APP_HOST, APP_PORT) app_server.start() @@ -19,13 +12,13 @@ class TestContract: pass -Specmatic() \ - .with_project_root(ROOT_DIR) \ - .with_stub(stub_host, stub_port, [expectation_json_file]) \ - .test_with_api_coverage_for_sanic_app(TestContract, app, app_host, app_port) \ - .run() +Specmatic().with_project_root(ROOT_DIR).with_stub( + STUB_HOST, + STUB_PORT, + expectation_json_files, +).test_with_api_coverage_for_sanic_app(TestContract, APP, APP_HOST, APP_PORT).run() app_server.stop() -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/test_contract_with_coverage.py b/test_contract_with_coverage.py deleted file mode 100644 index aa72ce6..0000000 --- a/test_contract_with_coverage.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest -from specmatic.core.specmatic import Specmatic - -from definitions import ROOT_DIR - -app_host = "127.0.0.1" -app_port = 8000 -stub_host = "127.0.0.1" -stub_port = 9090 -expectation_json_file = ROOT_DIR + '/test/data/expectation.json' - - -class TestContract: - pass - - -Specmatic() \ - .with_project_root(ROOT_DIR) \ - .with_stub(stub_host, stub_port, [expectation_json_file]) \ - .with_asgi_app('app:app', app_host, app_port) \ - .test(TestContract) \ - .run() - -if __name__ == '__main__': - pytest.main()