Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ TEST_SECTION="Section F"
# 1 and PES University (Ring Road), or 2 and PES University (Electronic City)
TEST_CAMPUS_CODE=2
TEST_CAMPUS="PES University (Electronic City)"
# Environment variable PESU_AUTH_ENVIRONMENT can take on values in ("development", "staging", "production"). defaults to "development" when not set

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue asks for a commented-out line. It also shouldn't go at the end of the TEST_* profile block. That file's header says the application never reads it, so give it its own section that says the app reads it from its environment:

Suggested change
# Environment variable PESU_AUTH_ENVIRONMENT can take on values in ("development", "staging", "production"). defaults to "development" when not set
# Not a test variable: the app reads it from its environment and reports it on /health. One of
# development, staging or production, and development when unset; any other value fails startup.
# PESU_AUTH_ENVIRONMENT=development

19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,20 @@ headers and no body, for monitors that check that way.

#### Response Object

| **Field** | **Type** | **Description** |
| ----------- | ---------- | ------------------------------------------------------------------- |
| `status` | `boolean` | `true` if healthy, `false` if there was an error |
| `message` | `str` | "ok" if healthy, error message otherwise |
| `timestamp` | `datetime` | A timezone offset timestamp indicating the time of the health check |
| **Field** | **Type** | **Description** |
| ------------- | ---------- | ------------------------------------------------------------------- |
| `status` | `boolean` | `true` if healthy, `false` if there was an error |
| `message` | `str` | "ok" if healthy, error message otherwise |
| `timestamp` | `datetime` | A timezone offset timestamp indicating the time of the health check |
| `version` | `str` | The version of pesu-auth that is running |
| `environment` | `str` | gets its value from `PESU_AUTH_ENVIRONMENT` |

`PESU_AUTH_ENVIRONMENT`
An explicit environment variable. Its value is read once at startup.

| **Allowed values** | **Default value** |
| ---------------------------------------- | ----------------- |
| `development`, `staging` or `production` | `development` |

### `/metrics`

Expand Down
16 changes: 12 additions & 4 deletions app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
import uvicorn
from fastapi import Depends, FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, RedirectResponse
from fastapi.requests import Request
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, RedirectResponse, Response
from fastapi.routing import APIRoute

if TYPE_CHECKING:
from collections.abc import AsyncGenerator

from fastapi.requests import Request
from fastapi.responses import Response
from fastapi.response import Response
Comment on lines +18 to +26

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fastapi.response doesn't exist (import fastapi.response raises ModuleNotFoundError). This only goes unnoticed because it's under TYPE_CHECKING. Request is also imported twice (lines 18 and 25), and Response is imported at runtime though it's only used in annotations.

health() doesn't need the request parameter: app is already in module scope, so it can read app.state.environment directly. Then these imports can go back to what they were:

Suggested change
from fastapi.requests import Request
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, RedirectResponse, Response
from fastapi.routing import APIRoute
if TYPE_CHECKING:
from collections.abc import AsyncGenerator
from fastapi.requests import Request
from fastapi.responses import Response
from fastapi.response import Response
from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse, RedirectResponse
from fastapi.routing import APIRoute
if TYPE_CHECKING:
from collections.abc import AsyncGenerator
from fastapi.requests import Request
from fastapi.responses import Response

Apply this together with the suggestions on lines 338 and 349 (Add suggestion to batch). If only this one is applied, FastAPI can no longer resolve request: Request and treats it as a required query parameter, so /health answers 400.

from starlette.middleware.base import RequestResponseEndpoint

from pydantic import ValidationError
Expand All @@ -43,7 +44,7 @@
)
from app.metrics.middleware import record_request_metrics
from app.metrics.prometheus import PROMETHEUS_CONTENT_TYPE, MetricsFormat, render_prometheus
from app.models import MetricsModel, RequestModel, ResponseModel
from app.models import HealthModel, MetricsModel, RequestModel, ResponseModel
from app.pesu import PESUAcademy

IST = ZoneInfo("Asia/Kolkata")
Expand Down Expand Up @@ -85,6 +86,9 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
# Startup
metrics.increment(LIFESPAN_EVENTS, event="startup")
logging.info("PESUAuth API startup")
if os.environ.get("PESU_AUTH_ENVIRONMENT", "development") not in ("development", "staging", "production"):
raise ValueError("Invaild enivronment value. Value must be development, staging or production")
app.state.environment = os.environ.get("PESU_AUTH_ENVIRONMENT", "development")
Comment on lines +89 to +91

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things here:

  • The variable is read twice. Read it once into a local.
  • Fix the typos ("Invaild enivronment") and include the value that was rejected, so whoever deployed it can see what's wrong.
  • A comment saying why this is checked at startup helps the next reader.
Suggested change
if os.environ.get("PESU_AUTH_ENVIRONMENT", "development") not in ("development", "staging", "production"):
raise ValueError("Invaild enivronment value. Value must be development, staging or production")
app.state.environment = os.environ.get("PESU_AUTH_ENVIRONMENT", "development")
# Read once, so that a bad deploy fails here instead of /health reporting a wrong value
environment = os.environ.get("PESU_AUTH_ENVIRONMENT", "development")
if environment not in ("development", "staging", "production"):
raise ValueError(f"PESU_AUTH_ENVIRONMENT must be development, staging or production, not {environment!r}")
app.state.environment = environment

The test on lines 48-51 of tests/unit/test_health.py checks the old message; there's a suggestion there to match this one, so apply both together (Add suggestion to batch). Optionally, the three values are now listed here and in HealthModel's Literal; a shared Literal alias, checked with typing.get_args, would keep the two from drifting apart.


yield

Expand Down Expand Up @@ -326,19 +330,23 @@ async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONR
@app.head("/health", include_in_schema=False)
@app.get(
"/health",
response_model=HealthModel,
response_class=JSONResponse,
responses=health_docs.response_examples,
tags=["Monitoring"],
)
async def health() -> JSONResponse:
async def health(request: Request) -> JSONResponse:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the import fix on lines 18-26: app is in module scope, so health() needs no request.

Suggested change
async def health(request: Request) -> JSONResponse:
async def health() -> JSONResponse:

Apply this together with the suggestions on lines 18-26 and 349 (Add suggestion to batch).

"""Health check endpoint."""
logging.debug("Health check requested.")
return JSONResponse(
status_code=200,
headers={"Cache-Control": "no-store"},

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#254 put all of the header logic in _security_headers (line 64) so that it applies to every response, including the 500 that unhandled_exception_handler renders. Because the header is set here instead, a 500 from /health goes out without Cache-Control. Drop it here:

Suggested change
headers={"Cache-Control": "no-store"},

and add /health to the check in _security_headers. That function isn't part of this diff, so it can't be a suggestion:

    Returns:
        dict[str, str]: The general security headers, plus `Cache-Control: no-store` for /authenticate and /health.
    """
    headers = dict(SECURITY_HEADERS)
    if request.url.path in ("/authenticate", "/health"):
        headers["Cache-Control"] = "no-store"

Also update the security_headers_middleware docstring ("prevent caching for /authenticate") and the README's "Response headers" section, which still says only /authenticate gets no-store.

content={
"status": True,
"message": "ok",
"timestamp": datetime.datetime.now(IST).isoformat(),
"version": version("pesu-auth"),

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

version() reads the package metadata from disk on every call, and /health is called a lot. app.version already holds the same value, computed once at import:

Suggested change
"version": version("pesu-auth"),
"version": app.version,

"environment": request.app.state.environment,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the import fix on lines 18-26:

Suggested change
"environment": request.app.state.environment,
"environment": app.state.environment,

Apply this together with the suggestions on lines 18-26 and 338 (Add suggestion to batch).

},
)

Expand Down
12 changes: 9 additions & 3 deletions app/docs/health.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
"""Custom docs for the /health PESUAuth endpoint."""

from app.docs.base import ApiDocs
from app.models import ResponseModel
from app.models import HealthModel, ResponseModel

health_docs = ApiDocs(
request_examples={},
response_examples={
200: {
"description": "Successful Health Check.",
"model": ResponseModel,
"model": HealthModel,
"content": {
"application/json": {
"example": {"status": True, "message": "ok", "timestamp": "2024-07-28T22:30:10.103368+05:30"}
"example": {
"status": True,
"message": "ok",
"timestamp": "2024-07-28T22:30:10.103368+05:30",
"version": "5.0.0",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this PR bumps the package to 5.5.0, while /health returns the installed package version. Please update the example to match the current version

"environment": "development",
}
}
},
},
Expand Down
1 change: 1 addition & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Custom models for the PESUAuth API."""

from .health import HealthModel as HealthModel
from .metrics import AuthenticationCountsModel as AuthenticationCountsModel
from .metrics import LatencyModel as LatencyModel
from .metrics import MetricsModel as MetricsModel
Expand Down
53 changes: 53 additions & 0 deletions app/models/health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Model representing response from health endpoint."""

from datetime import datetime
from typing import Literal

from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel


class HealthModel(BaseModel):
"""Model representing response from health endpoint."""

model_config = ConfigDict(strict=True, alias_generator=to_camel, populate_by_name=True)

status: bool = Field(
...,
title="Health status",
description="indicates whether the service is alive",
json_schema_extra={"example": True},
)

message: str = Field(
...,
title="Health message",
description="A string indicating the service's health",
json_schema_extra={"example": "ok"},
)

timestamp: datetime = Field(
...,
# Relaxed from the model-wide strict=True for this field alone. The API builds this model
# from a datetime but serializes an ISO string onto the wire, so a strict model could not
# parse its own responses -- which made the published schema unusable to a client wanting
# to validate with it, and forced the documentation tests into JSON mode to compensate.
strict=False,
title="timestamp",
description="returns the timestamp of when /health was used",
Comment on lines +36 to +37

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Titles and descriptions here are cased inconsistently ("Health status" vs "timestamp" and "version"), and this one reads oddly. Match ResponseModel:

Suggested change
title="timestamp",
description="returns the timestamp of when /health was used",
title="Health Check Timestamp",
description="Time of the health check, in IST (UTC+05:30).",

The same goes for the other fields: e.g. "Version" for version's title, and capitalised descriptions for status and message.

json_schema_extra={"example": "2024-07-28T22:30:10.103368+05:30"},
)

version: str = Field(
...,
title="version",
description="The version of pesu-auth that is running",
json_schema_extra={"example": "5.0.0"},
)

environment: Literal["development", "staging", "production"] = Field(
default="development",
title="environment",
description="an environment variable",
json_schema_extra={"example": "developement"},
Comment on lines +49 to +52

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few problems with this field:

  • default="development" takes environment out of required in the published schema, though every response includes it. Use ... like the other fields; the default belongs in the lifespan, which already has one.
  • The example "developement" is misspelled, and isn't one of the field's own allowed values.
  • "an environment variable" doesn't tell a reader anything. Say what it means and where it comes from.
Suggested change
default="development",
title="environment",
description="an environment variable",
json_schema_extra={"example": "developement"},
...,
title="Environment",
description="The deployment this instance is running as, set by `PESU_AUTH_ENVIRONMENT`.",
json_schema_extra={"example": "development"},

)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "pesu-auth"
version = "5.4.0"
version = "5.5.0"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bump to 5.5.0 is right, but uv.lock still records 5.4.0. The version check fails with pyproject.toml says 5.5.0 but uv.lock says 5.4.0, and every other job stops at uv sync --locked. Run uv lock and commit uv.lock along with this.

description = "A simple API to authenticate PESU credentials using PESU Academy."
readme = "README.md"
requires-python = ">=3.14"
Expand Down
1 change: 0 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ def check(profile, fields):
assert list(profile) == [field for field in PROFILE_VARIABLES if field in fields], "the fields are out of order"
mismatched = [field for field in fields if profile[field] != expected_profile[field]]
assert not mismatched, f"these fields do not match their TEST_* values: {mismatched}"

return check

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This blank line deletion isn't related to this issue. Please put it back:

Suggested change
return check
return check



Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_app_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ def assert_security_headers(response):

def test_integration_security_headers_on_general_endpoints(client):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing /health from the test below means nothing checks any more that /health sends the standard security headers. Since it now sends no-store, it needs its own test, for both GET and HEAD:

Suggested change
def test_integration_security_headers_on_general_endpoints(client):
def test_integration_health_security_headers(client):
"""/health must carry the standard security headers and Cache-Control: no-store, on GET and HEAD."""
for response in (client.get("/health"), client.head("/health")):
assert response.status_code == 200
assert_security_headers(response)
assert response.headers["Cache-Control"] == "no-store"
def test_integration_security_headers_on_general_endpoints(client):

"""General endpoints must carry standard security headers and not carry Cache-Control: no-store."""
for path in ["/", "/health", "/metrics"]:
for path in ["/", "/metrics"]:
response = client.get(path)
assert response.status_code == 200
assert_security_headers(response)
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
status_code_conformance,
)

from app.models import MetricsModel, ResponseModel
from app.models import HealthModel, MetricsModel, ResponseModel
from app.pesu import DISPATCHER_URL, LOGIN_URL

REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
Expand All @@ -27,7 +27,7 @@
CREDENTIAL_VARIABLES = ("TEST_EMAIL", "TEST_PRN", "TEST_SRN", "TEST_MOBILE", "TEST_PASSWORD")
METRICS_TOKEN = "test-metrics-token"
# The models the OpenAPI docs refer to by name
DOCUMENTED_MODELS = {"ResponseModel": ResponseModel, "MetricsModel": MetricsModel}
DOCUMENTED_MODELS = {"HealthModel": HealthModel, "ResponseModel": ResponseModel, "MetricsModel": MetricsModel}
# Login responses' accessToken values that give no token: None stands for the key being absent
UNUSABLE_TOKENS = {"missing": None, "object": {"token": "x"}, "list": ["x"]}
AGENT_FRONTMATTER = '---\nname: reviewer\ndescription: Reviews diffs. Read-only.\ntools: ["read"]\n---\n'
Expand Down
51 changes: 51 additions & 0 deletions tests/unit/test_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import os
from importlib.metadata import version

import pytest
from fastapi.testclient import TestClient

from app.app import app, lifespan
from app.models import HealthModel
Comment on lines +1 to +8

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ruff doesn't check tests/, but please match the style of the other test files. Add a module docstring, and drop the imports nothing uses (lifespan and HealthModel, and os once the environment tests no longer need it):

Suggested change
import os
from importlib.metadata import version
import pytest
from fastapi.testclient import TestClient
from app.app import app, lifespan
from app.models import HealthModel
"""Tests for /health: the version and environment fields, HEAD, and Cache-Control."""
from importlib.metadata import version
import pytest
from fastapi.testclient import TestClient
from app.app import app

For the rest, run uv run ruff format tests/unit/test_health.py once. It adds the two blank lines between top-level functions, removes the blank lines between @pytest.mark.parametrize and its function (lines 36 and 45), and adds spaces after commas.


@pytest.fixture()
def client():
with TestClient(app, raise_server_exceptions=False) as client:
yield client

def test_version(client):
response = client.get("/health")
if response.status_code == 200:
json_data = response.json()
actual_version = json_data["version"]
assert actual_version == version("pesu-auth")
Comment on lines +17 to +20

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If /health returns anything other than 200, this test asserts nothing and passes. Assert the status first:

Suggested change
if response.status_code == 200:
json_data = response.json()
actual_version = json_data["version"]
assert actual_version == version("pesu-auth")
assert response.status_code == 200
assert response.json()["version"] == version("pesu-auth")



def test_head_method_for_health(client):
head_response = client.head("/health")
assert head_response.status_code == 200
assert head_response.text == ""

def test_get_cache_no_store_header(client):
get_response = client.get("/health")
assert get_response.headers.get("Cache-Control") == "no-store"
def test_head_cache_no_store_header(client):
head_response = client.head("/health")
assert head_response.headers.get("Cache-Control") == "no-store"

@pytest.mark.parametrize("valid_cases", ("development","production","staging"))

def test_valid_cases_for_PESU_AUTH_ENVIRONMENT(monkeypatch, valid_cases):
monkeypatch.setenv("PESU_AUTH_ENVIRONMENT",valid_cases)
assert os.environ.get("PESU_AUTH_ENVIRONMENT") == valid_cases
Comment on lines +35 to +39

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test never starts the app: it sets the variable and then checks that os.environ holds what was just set, so it passes even if the lifespan ignores PESU_AUTH_ENVIRONMENT entirely. The issue asks for each valid value to be tested, so start the app and check what /health reports:

Suggested change
@pytest.mark.parametrize("valid_cases", ("development","production","staging"))
def test_valid_cases_for_PESU_AUTH_ENVIRONMENT(monkeypatch, valid_cases):
monkeypatch.setenv("PESU_AUTH_ENVIRONMENT",valid_cases)
assert os.environ.get("PESU_AUTH_ENVIRONMENT") == valid_cases
@pytest.mark.parametrize("valid_cases", ("development", "production", "staging"))
def test_valid_cases_for_PESU_AUTH_ENVIRONMENT(monkeypatch, valid_cases):
monkeypatch.setenv("PESU_AUTH_ENVIRONMENT", valid_cases)
with TestClient(app) as client:
response = client.get("/health")
assert response.json()["environment"] == valid_cases


def test_default_fallback_PESU_AUTH_ENVIRONMENT():
assert app.state.environment == "development"
Comment on lines +41 to +42

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only passes because an earlier test's lifespan has already set app.state.environment. Run on its own (pytest tests/unit/test_health.py::test_default_fallback_PESU_AUTH_ENVIRONMENT) it fails, and it also fails if the variable is set in the developer's shell. Clear the variable, start the app, and check the response:

Suggested change
def test_default_fallback_PESU_AUTH_ENVIRONMENT():
assert app.state.environment == "development"
def test_default_fallback_PESU_AUTH_ENVIRONMENT(monkeypatch):
monkeypatch.delenv("PESU_AUTH_ENVIRONMENT", raising=False)
with TestClient(app) as client:
response = client.get("/health")
assert response.json()["environment"] == "development"


@pytest.mark.parametrize("invalid_cases", ("","prod","Development"))

def test_invalid_cases_for_PESU_AUTH_ENVIRONMENT(monkeypatch, invalid_cases):
monkeypatch.setenv("PESU_AUTH_ENVIRONMENT", invalid_cases)
with pytest.raises(ValueError) as exc_info:
with TestClient(app):
pass
assert "Invaild enivronment value. Value must be development, staging or production" in str(exc_info.value)
Comment on lines +48 to +51

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Goes with the suggestion on lines 89-91 of app/app.py, which fixes the typos in this message; apply both together. match= is the usual way to check an exception's message:

Suggested change
with pytest.raises(ValueError) as exc_info:
with TestClient(app):
pass
assert "Invaild enivronment value. Value must be development, staging or production" in str(exc_info.value)
with pytest.raises(ValueError, match="PESU_AUTH_ENVIRONMENT must be development, staging or production"):
with TestClient(app):
pass

2 changes: 1 addition & 1 deletion tests/unit/test_openapi_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
ProfileParseError,
UpstreamError,
)
from app.models import MetricsModel, ProfileModel, RequestModel, ResponseModel
from app.models import HealthModel, MetricsModel, ProfileModel, RequestModel, ResponseModel

@aditeyabaral aditeyabaral Oct 22, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HealthModel is imported but never used. Step 7 asks for the tests in this file to be updated: test_a_real_response_can_be_parsed_with_the_published_model and test_the_model_still_rejects_a_wrong_type_elsewhere still validate the /health response against ResponseModel. Those lines aren't part of this diff, so they can't be a suggestion. Switch them to HealthModel:

    body = client.get("/health").json()
    assert HealthModel.model_validate(body).status is True
    assert HealthModel.model_validate_json(json.dumps(body)).status is True
    with pytest.raises(Exception, match="status"):
        HealthModel.model_validate({**body, "status": "not-a-bool"})

from app.pesu import PESUAcademy


Expand Down
Loading