Skip to content

Commit

Permalink
feat: refactored tests to make them easier to maintain (#215)
Browse files Browse the repository at this point in the history
  • Loading branch information
TeKrop authored Nov 10, 2024
1 parent 46ed1f0 commit 20341ed
Show file tree
Hide file tree
Showing 127 changed files with 1,375 additions and 208,517 deletions.
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,7 @@ clean: down ## Clean up Docker environment
lock: ## Update lock file
uv lock

.PHONY: help build start lint format shell exec test up down clean lock
update_test_fixtures: ## Update test fixtures (heroes, players, etc.)
$(DOCKER RUN) uv run python -m tests.update_test_fixtures $(PARAMS)

.PHONY: help build start lint format shell exec test up down clean lock update_test_fixtures
8 changes: 8 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ class Settings(BaseSettings):
# Discord Webhook URL
discord_webhook_url: str = ""

# Error message to be displayed to API users
internal_server_error_message: str = (
"An internal server error occurred during the process. The developer "
"received a notification, but don't hesitate to create a GitHub "
"issue if you want any news concerning the bug resolution : "
"https://github.com/TeKrop/overfast-api/issues"
)

############
# LOCAL
############
Expand Down
11 changes: 3 additions & 8 deletions app/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def overfast_internal_error(url: str, error: Exception) -> HTTPException:
# If we're using a profiler, it means we're debugging, raise the error
# directly in order to have proper backtrace in logs
if settings.profiler:
raise error
raise error # pragma: no cover

# Else, send a message to the given channel using Discord Webhook URL
send_discord_webhook_message(
Expand All @@ -68,12 +68,7 @@ def overfast_internal_error(url: str, error: Exception) -> HTTPException:

return HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=(
"An internal server error occurred during the process. The developer "
"received a notification, but don't hesitate to create a GitHub "
"issue if you want any news concerning the bug resolution : "
"https://github.com/TeKrop/overfast-api/issues"
),
detail=settings.internal_server_error_message,
)


Expand All @@ -85,7 +80,7 @@ def send_discord_webhook_message(message: str) -> httpx.Response | None:
logger.error(message)
return None

return httpx.post(
return httpx.post( # pragma: no cover
settings.discord_webhook_url, data={"content": message}, timeout=10
)

Expand Down
116 changes: 65 additions & 51 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from fastapi.responses import JSONResponse
from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException as StarletteHTTPException

from .config import settings
Expand Down Expand Up @@ -45,7 +44,6 @@ async def lifespan(_: FastAPI): # pragma: no cover
await overfast_client.aclose()


app = FastAPI(title="OverFast API", docs_url=None, redoc_url=None, lifespan=lifespan)
description = f"""OverFast API provides comprehensive data on Overwatch 2 heroes,
game modes, maps, and player statistics by scraping Blizzard pages. Developed with
the efficiency of **FastAPI** and **Selectolax**, it leverages **nginx** as a
Expand Down Expand Up @@ -74,72 +72,85 @@ async def lifespan(_: FastAPI): # pragma: no cover
- Integer and float string representations are converted to their respective types
"""

app = FastAPI(
title="OverFast API",
description=description,
version=settings.app_version,
openapi_tags=[
{
"name": RouteTag.HEROES,
"description": "Overwatch heroes details : lore, abilities, etc.",
"externalDocs": {
"description": "Blizzard heroes page, source of the information",
"url": "https://overwatch.blizzard.com/en-us/heroes/",
},
},
{
"name": RouteTag.GAMEMODES,
"description": "Overwatch gamemodes details",
"externalDocs": {
"description": "Overwatch home page, source of the information",
"url": "https://overwatch.blizzard.com/en-us/",
},
},
{
"name": RouteTag.MAPS,
"description": "Overwatch maps details",
},
{
"name": RouteTag.PLAYERS,
"description": players_section_description,
"externalDocs": {
"description": "Blizzard profile pages, source of the information",
"url": "https://overwatch.blizzard.com/en-us/search/",
},
},
],
servers=[{"url": settings.app_base_url, "description": "Production server"}],
docs_url=None,
redoc_url=None,
lifespan=lifespan,
contact={
"name": 'Valentin "TeKrop" PORCHET',
"url": "https://github.com/TeKrop/overfast-api",
"email": "[email protected]",
},
license_info={
"name": "MIT",
"url": "https://github.com/TeKrop/overfast-api/blob/main/LICENSE",
},
)

# Add customized OpenAPI specs with app logo


def custom_openapi(): # pragma: no cover
if app.openapi_schema:
return app.openapi_schema

openapi_schema = get_openapi(
title="OverFast API",
description=description,
version=settings.app_version,
contact={
"name": 'Valentin "TeKrop" PORCHET',
"url": "https://github.com/TeKrop/overfast-api",
"email": "[email protected]",
},
license_info={
"name": "MIT",
"url": "https://github.com/TeKrop/overfast-api/blob/main/LICENSE",
},
title=app.title,
description=app.description,
version=app.version,
contact=app.contact,
license_info=app.license_info,
routes=app.routes,
tags=[
{
"name": RouteTag.HEROES,
"description": "Overwatch heroes details : lore, abilities, etc.",
"externalDocs": {
"description": "Blizzard heroes page, source of the information",
"url": "https://overwatch.blizzard.com/en-us/heroes/",
},
},
{
"name": RouteTag.GAMEMODES,
"description": "Overwatch gamemodes details",
"externalDocs": {
"description": "Overwatch home page, source of the information",
"url": "https://overwatch.blizzard.com/en-us/",
},
},
{
"name": RouteTag.MAPS,
"description": "Overwatch maps details",
},
{
"name": RouteTag.PLAYERS,
"description": players_section_description,
"externalDocs": {
"description": "Blizzard profile pages, source of the information",
"url": "https://overwatch.blizzard.com/en-us/search/",
},
},
],
servers=[{"url": settings.app_base_url, "description": "Production server"}],
tags=app.openapi_tags,
servers=app.servers,
)
openapi_schema["info"]["x-logo"] = {
"url": "https://files.tekrop.fr/overfast_api_logo_full_1000.png",
"altText": "OverFast API Logo",
}

app.openapi_schema = openapi_schema
return app.openapi_schema


app.openapi = custom_openapi

app.mount("/static", StaticFiles(directory="static"), name="static")

app.logger = logger
logger.info("OverFast API... Online !")
logger.info("Version : {}", settings.app_version)
# Add custom exception handlers for Starlet HTTP exceptions, but also
# for Pydantic Validation Errors


@app.exception_handler(StarletteHTTPException)
Expand Down Expand Up @@ -182,7 +193,7 @@ async def overridden_swagger():


# Add supported profiler as middleware
if settings.profiler:
if settings.profiler: # pragma: no cover
supported_profilers = {
Profiler.MEMRAY: MemrayInMemoryMiddleware,
Profiler.PYINSTRUMENT: PyInstrumentMiddleware,
Expand All @@ -205,3 +216,6 @@ async def overridden_swagger():
app.include_router(gamemodes.router, prefix="/gamemodes")
app.include_router(maps.router, prefix="/maps")
app.include_router(players.router, prefix="/players")

logger.info("OverFast API... Online !")
logger.info("Version : {}", settings.app_version)
10 changes: 5 additions & 5 deletions app/middlewares.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import pyinstrument


class OverFastMiddleware(BaseHTTPMiddleware):
class OverFastMiddleware(BaseHTTPMiddleware): # pragma: no cover
async def dispatch(
self, request: Request, call_next: Callable
) -> HTMLResponse | JSONResponse:
Expand All @@ -28,7 +28,7 @@ async def dispatch(
return await self._dispatch(request, call_next)


class MemrayInMemoryMiddleware(OverFastMiddleware):
class MemrayInMemoryMiddleware(OverFastMiddleware): # pragma: no cover
async def _dispatch(self, request: Request, call_next: Callable) -> HTMLResponse:
# Create an temporary file
with tempfile.NamedTemporaryFile(suffix=".bin", delete=False) as tmp_bin_file:
Expand Down Expand Up @@ -77,15 +77,15 @@ def generate_html_report(self, tmp_bin_path: Path) -> str:
return html_content


class PyInstrumentMiddleware(OverFastMiddleware):
class PyInstrumentMiddleware(OverFastMiddleware): # pragma: no cover
async def _dispatch(self, request: Request, call_next: Callable) -> HTMLResponse:
with pyinstrument.Profiler(interval=0.001, async_mode="enabled") as profiler:
await call_next(request)

return HTMLResponse(profiler.output_html())


class TraceMallocMiddleware(OverFastMiddleware):
class TraceMallocMiddleware(OverFastMiddleware): # pragma: no cover
def __init__(self, app: FastAPI):
super().__init__(app)
tracemalloc.start()
Expand Down Expand Up @@ -118,7 +118,7 @@ async def _dispatch(self, request: Request, call_next: Callable) -> JSONResponse
return JSONResponse(content=memory_report)


class ObjGraphMiddleware(OverFastMiddleware):
class ObjGraphMiddleware(OverFastMiddleware): # pragma: no cover
async def _dispatch(self, request: Request, call_next: Callable) -> JSONResponse:
# Capture common object types before processing
objects_before = objgraph.most_common_types(limit=10)
Expand Down
11 changes: 3 additions & 8 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from pydantic import BaseModel, Field

from app.config import settings


class BlizzardErrorMessage(BaseModel):
error: str = Field(
Expand All @@ -15,14 +17,7 @@ class InternalServerErrorMessage(BaseModel):
error: str = Field(
...,
description="Message describing the internal server error",
examples=[
(
"An internal server error occurred during the process. The developer "
"received a notification, but don't hesitate to create a GitHub "
"issue if you want any news concerning the bug resolution : "
"https://github.com/TeKrop/overfast-api/issues"
),
],
examples=[settings.internal_server_error_message],
)


Expand Down
13 changes: 0 additions & 13 deletions app/players/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,6 @@
FLOAT_PATTERN = re.compile(r"^-?\d+(,\d+)*\.\d+$")


# List of players used for testing
players_ids = [
"copypasting-1216", # Player with an empty hero career stats (lucio)
"Dekk-2677", # Classic profile without rank
"KIRIKO-21253", # Profile with rank on only two roles
"Player-1112937", # Console player
"quibble-11594", # Profile without endorsement
"TeKrop-2217", # Classic profile
"Unknown-1234", # No player
"JohnV1-1190", # Player without any title ingame
]


@cache
def get_hero_name(hero_key: HeroKey) -> str:
"""Get a hero name based on the CSV file"""
Expand Down
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "overfast-api"
version = "3.4.0"
version = "3.5.0"
description = "Overwatch API giving data about heroes, maps, and players statistics."
license = {file = "LICENSE"}
authors = [
Expand All @@ -12,7 +12,6 @@ dependencies = [
"fastapi[standard]==0.115.*",
"httpx[http2]==0.27.*",
"loguru==0.7.*",
"lxml==5.3.*",
"redis==5.2.*",
"pydantic==2.9.*",
"pydantic-settings==2.6.*",
Expand Down
34 changes: 20 additions & 14 deletions tests/fixtures/html/heroes.html

Large diffs are not rendered by default.

34 changes: 20 additions & 14 deletions tests/fixtures/html/heroes/ana.html

Large diffs are not rendered by default.

34 changes: 20 additions & 14 deletions tests/fixtures/html/heroes/ashe.html

Large diffs are not rendered by default.

38 changes: 22 additions & 16 deletions tests/fixtures/html/heroes/baptiste.html

Large diffs are not rendered by default.

34 changes: 20 additions & 14 deletions tests/fixtures/html/heroes/bastion.html

Large diffs are not rendered by default.

Loading

0 comments on commit 20341ed

Please sign in to comment.